Migrate usage of the Hash trait for cache invalidation to the dedicated CacheHash trait (#4051)

* WIP start migrating usages of hash for cache invalidadion to dedicated trait

* Finish migrating usages

* Code review

* Add comments clearifying the reasoning for using random ids in the VectorModification cach hash impl

* Fix some remaining hash violations

* Finish migration and fix compilation

* Fix import ordering

* Cleanup

* Fix code review stuff

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Dennis Kobert
2026-04-27 07:18:47 +02:00
committed by GitHub
parent 7bb01c9651
commit 3d84e63ef9
64 changed files with 828 additions and 448 deletions

View File

@@ -164,6 +164,12 @@ impl<Io> Hash for EditorApi<Io> {
}
}
impl<Io> core_types::graphene_hash::CacheHash for EditorApi<Io> {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(self, state);
}
}
impl<Io> PartialEq for EditorApi<Io> {
fn eq(&self, other: &Self) -> bool {
self.font_cache == other.font_cache

View File

@@ -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 }

View File

@@ -163,6 +163,12 @@ bitflags! {
}
}
impl graphene_hash::CacheHash for ContextFeatures {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(self, state);
}
}
impl ContextFeatures {
pub fn name(&self) -> &'static str {
match *self {
@@ -182,7 +188,7 @@ impl ContextFeatures {
// CONTEXT DEPENDENCIES
// ====================
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, Default)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, graphene_hash::CacheHash, dyn_any::DynAny, serde::Serialize, serde::Deserialize, Default)]
pub struct ContextDependencies {
pub extract: ContextFeatures,
pub inject: ContextFeatures,
@@ -536,14 +542,14 @@ impl Default for OwnedContextImpl {
}
}
impl Hash for OwnedContextImpl {
fn hash<H: std::hash::Hasher>(&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<H: core::hash::Hasher>(&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<H: Hash + ?Sized> DynHash for H {
impl<H: graphene_hash::CacheHash + ?Sized> DynHash for H {
fn dyn_hash(&self, mut state: &mut dyn Hasher) {
self.hash(&mut state);
graphene_hash::CacheHash::cache_hash(self, &mut state);
}
}

View File

@@ -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;

View File

@@ -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<I, O> {
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct MemoHash<T: Hash> {
pub struct MemoHash<T: CacheHash> {
hash: u64,
value: Arc<T>,
}
impl<'de, T: serde::Deserialize<'de> + Hash> serde::Deserialize<'de> for MemoHash<T> {
impl<'de, T: serde::Deserialize<'de> + CacheHash> serde::Deserialize<'de> for MemoHash<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
@@ -25,7 +26,7 @@ impl<'de, T: serde::Deserialize<'de> + Hash> serde::Deserialize<'de> for MemoHas
}
}
impl<T: Hash + serde::Serialize> serde::Serialize for MemoHash<T> {
impl<T: CacheHash + serde::Serialize> serde::Serialize for MemoHash<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
@@ -34,7 +35,7 @@ impl<T: Hash + serde::Serialize> serde::Serialize for MemoHash<T> {
}
}
impl<T: Hash> MemoHash<T> {
impl<T: CacheHash> MemoHash<T> {
pub fn new(value: T) -> Self {
let hash = Self::calc_hash(&value);
Self { hash, value: value.into() }
@@ -45,7 +46,7 @@ impl<T: Hash> MemoHash<T> {
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<T: Hash> MemoHash<T> {
self.hash
}
}
impl<T: Hash> From<T> for MemoHash<T> {
impl<T: CacheHash> From<T> for MemoHash<T> {
fn from(value: T) -> Self {
Self::new(value)
}
}
impl<T: Hash> Hash for MemoHash<T> {
impl<T: CacheHash> Hash for MemoHash<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.hash.hash(state)
}
}
impl<T: Hash> Deref for MemoHash<T> {
impl<T: CacheHash> CacheHash for MemoHash<T> {
fn cache_hash<H: Hasher>(&self, state: &mut H) {
self.hash.hash(state);
}
}
impl<T: CacheHash> Deref for MemoHash<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
@@ -79,18 +87,18 @@ impl<T: Hash> Deref for MemoHash<T> {
}
}
pub struct MemoHashGuard<'a, T: Hash> {
pub struct MemoHashGuard<'a, T: CacheHash> {
inner: &'a mut MemoHash<T>,
}
impl<T: Hash> Drop for MemoHashGuard<'_, T> {
impl<T: CacheHash> Drop for MemoHashGuard<'_, T> {
fn drop(&mut self) {
let hash = MemoHash::<T>::calc_hash(&self.inner.value);
self.inner.hash = hash;
}
}
impl<T: Hash> Deref for MemoHashGuard<'_, T> {
impl<T: CacheHash> Deref for MemoHashGuard<'_, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
@@ -98,7 +106,7 @@ impl<T: Hash> Deref for MemoHashGuard<'_, T> {
}
}
impl<T: Hash + Clone> std::ops::DerefMut for MemoHashGuard<'_, T> {
impl<T: CacheHash + Clone> std::ops::DerefMut for MemoHashGuard<'_, T> {
fn deref_mut(&mut self) -> &mut Self::Target {
Arc::make_mut(&mut self.inner.value)
}

View File

@@ -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<T> {
@@ -198,16 +197,16 @@ impl<T> Default for Table<T> {
}
}
impl<T: Hash> Hash for Table<T> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
impl<T: graphene_hash::CacheHash> graphene_hash::CacheHash for Table<T> {
fn cache_hash<H: core::hash::Hasher>(&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);
}
}
}

View File

@@ -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<H: std::hash::Hasher>(&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);

View File

@@ -77,7 +77,7 @@ macro_rules! fn_type_fut {
}
// TODO: Rename to NodeSignatureMonomorphization
#[derive(Clone, PartialEq, Eq, Hash, Default, serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, Eq, Hash, graphene_hash::CacheHash, Default, serde::Serialize, serde::Deserialize)]
pub struct NodeIOTypes {
pub call_argument: Type,
pub return_value: Type,
@@ -126,7 +126,7 @@ impl std::fmt::Debug for NodeIOTypes {
}
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, PartialEq, Eq, Hash, graphene_hash::CacheHash, serde::Serialize, serde::Deserialize)]
pub struct ProtoNodeIdentifier {
name: Cow<'static, str>,
}
@@ -200,6 +200,12 @@ impl std::hash::Hash for TypeDescriptor {
}
}
impl graphene_hash::CacheHash for TypeDescriptor {
fn cache_hash<H: ::core::hash::Hasher>(&self, state: &mut H) {
graphene_hash::CacheHash::cache_hash(&self.name, state);
}
}
impl std::fmt::Display for TypeDescriptor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let text = make_type_user_readable(&simplify_identifier_name(&self.name));
@@ -222,7 +228,7 @@ impl PartialEq for TypeDescriptor {
/// Graph runtime type information used for type inference.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
#[derive(Clone, PartialEq, Eq, Hash, graphene_hash::CacheHash, 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

@@ -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 {

View File

@@ -0,0 +1,17 @@
[package]
name = "graphene-hash"
version = "0.0.0"
edition = "2024"
authors = ["Graphite Authors <contact@graphite.art>"]
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 }

View File

@@ -0,0 +1,19 @@
[package]
name = "graphene-hash-derive"
version = "0.0.0"
edition = "2024"
authors = ["Graphite Authors <contact@graphite.art>"]
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"] }

View File

@@ -0,0 +1,129 @@
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! {
impl #impl_generics graphene_hash::CacheHash for #name #ty_generics #where_clause {
fn cache_hash<H: ::core::hash::Hasher>(&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::<syn::Ident>().map(|id| id == "skip").unwrap_or(false)
})
}

View File

@@ -0,0 +1,237 @@
#![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<H: core::hash::Hasher>(&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<T>(pub T);
impl<T: CacheHash> core::hash::Hash for CacheHashWrapper<T> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.0.cache_hash(state);
}
}
impl<T: CacheHash> CacheHash for core::ops::RangeInclusive<T> {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.start().cache_hash(state);
self.end().cache_hash(state);
}
}
impl<T> core::ops::Deref for CacheHashWrapper<T> {
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<H: core::hash::Hasher>(&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<'a> CacheHash for std::borrow::Cow<'a, str> {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(self, state);
}
}
impl CacheHash for str {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(self, state);
}
}
impl CacheHash for () {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, _state: &mut H) {}
}
// f32 and f64: hash via bit pattern so NaN is handled deterministically.
impl CacheHash for f32 {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(&self.to_bits(), state);
}
}
impl CacheHash for f64 {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&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<H: core::hash::Hasher>(&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<H: core::hash::Hasher>(&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<T: CacheHash> CacheHash for Option<T> {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&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<T: CacheHash> CacheHash for [T] {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(&self.len(), state);
for item in self {
item.cache_hash(state);
}
}
}
impl<T: CacheHash, const N: usize> CacheHash for [T; N] {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
for item in self {
item.cache_hash(state);
}
}
}
#[cfg(feature = "std")]
impl<T: CacheHash> CacheHash for Vec<T> {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.as_slice().cache_hash(state);
}
}
#[cfg(feature = "std")]
impl<T: CacheHash + ?Sized> CacheHash for Box<T> {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
(**self).cache_hash(state);
}
}
#[cfg(feature = "std")]
impl<T: CacheHash + ?Sized> CacheHash for std::sync::Arc<T> {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
(**self).cache_hash(state);
}
}
impl<T: CacheHash + ?Sized> CacheHash for &T {
#[inline]
fn cache_hash<H: core::hash::Hasher>(&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<H: core::hash::Hasher>(&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);

View File

@@ -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 }

View File

@@ -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<Graphic>,
pub label: String,
@@ -76,7 +76,7 @@ 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, 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<NodeId>)>,
}

View File

@@ -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<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, Hash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, CacheHash, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub enum Graphic {
Graphic(Table<Graphic>),
Vector(Table<Vector>),

View File

@@ -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 }

View File

@@ -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<H: Hasher>(&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

View File

@@ -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<H: core::hash::Hasher>(&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)]

View File

@@ -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

View File

@@ -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<P: Pixel + PartialEq> PartialEq for Image<P> {
#[derive(Debug, Clone, dyn_any::DynAny, Default, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct TransformImage(pub DAffine2);
impl Hash for TransformImage {
fn hash<H: std::hash::Hasher>(&self, _: &mut H) {}
impl core_types::CacheHash for TransformImage {
fn cache_hash<H: ::core::hash::Hasher>(&self, state: &mut H) {
core_types::CacheHash::cache_hash(&self.0, state);
}
}
impl<P: Pixel + std::fmt::Debug> std::fmt::Debug for Image<P> {
@@ -109,11 +110,11 @@ impl<P: Copy + Pixel> BitmapMut for Image<P> {
}
}
impl<P: Hash + Pixel> Hash for Image<P> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.width.hash(state);
self.height.hash(state);
self.data.hash(state);
impl<P: core_types::CacheHash + Pixel> core_types::CacheHash for Image<P> {
fn cache_hash<H: ::core::hash::Hasher>(&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<P: Pixel> IntoIterator for Image<P> {
pub fn migrate_image_frame<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Table<Raster<CPU>>, D::Error> {
use serde::Deserialize;
#[derive(Clone, Debug, Hash, PartialEq, DynAny)]
#[derive(Clone, Debug, core_types::CacheHash, PartialEq, DynAny)]
enum RasterFrame {
ImageFrame(Table<Image<Color>>),
}
@@ -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<GraphicElement>),
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<TableRow<Raster<CPU>>, 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 <image> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image
ImageFrame(Table<Image<Color>>),
@@ -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 <g> tag: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g
GraphicGroup(Table<GraphicElement>),

View File

@@ -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<T>
where
Raster<T>: Storage,
@@ -60,13 +60,23 @@ where
}
}
impl<T> core_types::CacheHash for Raster<T>
where
Raster<T>: Storage,
T: core_types::CacheHash,
{
fn cache_hash<H: ::core::hash::Hasher>(&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<Color>);
impl Sealed for Raster<CPU> {}
@@ -140,6 +150,13 @@ mod gpu {
pub texture: wgpu::Texture,
}
impl core_types::CacheHash for GPU {
fn cache_hash<H: ::core::hash::Hasher>(&self, state: &mut H) {
use ::core::hash::Hash;
self.texture.hash(state);
}
}
impl Sealed for Raster<GPU> {}
impl Storage for Raster<GPU> {
@@ -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<GPU> {}

View File

@@ -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 }

View File

@@ -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<SvgSegment>,
pub svg_defs: String,
pub transform: DAffine2,
pub image_data: HashMap<Image<Color>, u64>,
pub image_data: HashMap<CacheHashWrapper<Image<Color>>, 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<H: Hasher>(&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<Raster<CPU>> {
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",

View File

@@ -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

View File

@@ -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<f64>,
@@ -60,17 +60,6 @@ impl<'de> serde::Deserialize<'de> for GradientStops {
}
}
impl std::hash::Hash for GradientStops {
fn hash<H: std::hash::Hasher>(&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<H: std::hash::Hasher>(&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;

View File

@@ -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<PointId: Identifier> {
manipulator_groups: Vec<ManipulatorGroup<PointId>>,

View File

@@ -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<PointId: Identifier> {
pub anchor: DVec2,
@@ -20,22 +20,6 @@ pub struct ManipulatorGroup<PointId: Identifier> {
pub id: PointId,
}
// TODO: Remove once we no longer need to hash floats in Graphite
impl<PointId: Identifier> Hash for ManipulatorGroup<PointId> {
fn hash<H: core::hash::Hasher>(&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<PointId: Identifier> Debug for ManipulatorGroup<PointId> {
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<H: std::hash::Hasher>(&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 { .. })

View File

@@ -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
);

View File

@@ -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,

View File

@@ -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<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, 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<Fill> 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<H: std::hash::Hasher>(&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<Color>, 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<Stroke>,
pub fill: Fill,
}
impl std::hash::Hash for PathStyle {
fn hash<H: std::hash::Hasher>(&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]

View File

@@ -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<PointId>,
@@ -87,13 +87,6 @@ pub struct PointDomain {
pub(crate) position: Vec<DVec2>,
}
impl Hash for PointDomain {
fn hash<H: Hasher>(&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<usize>,
}
#[derive(Debug, Clone, PartialEq, Hash)]
#[derive(Debug, Clone, PartialEq)]
pub struct FaceIterator<'a, Upstream> {
vector: &'a Vector<Upstream>,
faces: Faces,

View File

@@ -17,12 +17,6 @@ pub struct PointModification {
delta: HashMap<PointId, DVec2>,
}
impl Hash for PointModification {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
generate_uuid().hash(state)
}
}
impl PointModification {
/// Apply this modification to the specified [`PointDomain`].
pub fn apply(&self, point_domain: &mut PointDomain, segment_domain: &mut SegmentDomain) {
@@ -511,9 +505,13 @@ impl VectorModification {
}
}
impl Hash for VectorModification {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
generate_uuid().hash(state)
// Intentionally non-deterministic: fields contain HashMaps with non-deterministic iteration order,
// so we use a UUID to always bust the cache and force re-evaluation when any modification is present.
// This will not actually lead to a cache invalidation in most cases due to the
// graph inputs being wrapped in a `MemoHash` wrapper.
impl graphene_hash::CacheHash for VectorModification {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(&generate_uuid(), state);
}
}

View File

@@ -54,13 +54,13 @@ impl<Upstream: Default + 'static> Default for Vector<Upstream> {
}
}
impl<Upstream> std::hash::Hash for Vector<Upstream> {
fn hash<H: std::hash::Hasher>(&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<Upstream> graphene_hash::CacheHash for Vector<Upstream> {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.point_domain.cache_hash(state);
self.segment_domain.cache_hash(state);
self.region_domain.cache_hash(state);
self.style.cache_hash(state);
self.colinear_manipulators.cache_hash(state);
// We don't hash the upstream_data intentionally
}
}