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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
64 changed files with 828 additions and 448 deletions

31
Cargo.lock generated
View File

@ -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",
@ -1916,6 +1918,7 @@ dependencies = [
"graph-craft",
"graphene-application-io",
"graphene-core",
"graphene-hash",
"graphic-types",
"iai-callgrind",
"js-sys",
@ -1996,6 +1999,7 @@ dependencies = [
"core-types",
"dyn-any",
"glam",
"graphene-hash",
"graphic-types",
"log",
"node-macro",
@ -2005,6 +2009,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 +2087,7 @@ dependencies = [
"core-types",
"dyn-any",
"glam",
"graphene-hash",
"node-macro",
"raster-types",
"serde",
@ -2182,6 +2205,7 @@ dependencies = [
"futures",
"glam",
"graph-craft",
"graphene-hash",
"graphene-std",
"graphite-proc-macros",
"image",
@ -3296,6 +3320,7 @@ dependencies = [
"core-types",
"dyn-any",
"glam",
"graphene-hash",
"half",
"log",
"node-macro",
@ -4316,6 +4341,7 @@ dependencies = [
"fastnoise-lite",
"futures",
"glam",
"graphene-hash",
"image",
"kurbo",
"ndarray",
@ -4361,6 +4387,7 @@ dependencies = [
"core-types",
"dyn-any",
"glam",
"graphene-hash",
"image",
"node-macro",
"serde",
@ -4501,6 +4528,7 @@ dependencies = [
"core-types",
"dyn-any",
"glam",
"graphene-hash",
"graphic-types",
"kurbo",
"log",
@ -5513,6 +5541,7 @@ dependencies = [
"dyn-any",
"fancy-regex",
"glam",
"graphene-hash",
"log",
"node-macro",
"parley",
@ -6157,6 +6186,7 @@ dependencies = [
"futures",
"glam",
"graphene-core",
"graphene-hash",
"graphic-types",
"kurbo",
"log",
@ -6182,6 +6212,7 @@ dependencies = [
"dyn-any",
"fixedbitset",
"glam",
"graphene-hash",
"kurbo",
"log",
"lyon_geom",

View File

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

View File

@ -19,6 +19,7 @@ gpu = ["interpreted-executor/gpu", "dep:wgpu-executor"]
# Local dependencies
graphite-proc-macros = { workspace = true }
graph-craft = { workspace = true }
graphene-hash = { workspace = true }
interpreted-executor = { workspace = true }
graphene-std = { workspace = true } # NOTE: `core-types` should not be added here because `graphene-std` re-exports its contents
preprocessor = { workspace = true }

View File

@ -1,6 +1,5 @@
use graph_craft::document::NodeNetwork;
use std::cell::Cell;
use std::hash::{Hash, Hasher};
#[derive(Debug, Default, Clone, PartialEq)]
pub struct MemoNetwork {
@ -26,9 +25,9 @@ impl serde::Serialize for MemoNetwork {
}
}
impl Hash for MemoNetwork {
fn hash<H: Hasher>(&self, state: &mut H) {
self.current_hash().hash(state);
impl graphene_hash::CacheHash for MemoNetwork {
fn cache_hash<H: ::core::hash::Hasher>(&self, state: &mut H) {
self.current_hash().cache_hash(state);
}
}

View File

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

View File

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

View File

@ -22,6 +22,7 @@ wasm = [
[dependencies]
# Local dependencies
dyn-any = { workspace = true }
graphene-hash = { workspace = true }
core-types = { workspace = true }
brush-nodes = { workspace = true }
graphene-core = { workspace = true }

View File

@ -9,7 +9,7 @@ use core_types::{Context, ContextDependencies, Cow, MemoHash, ProtoNodeIdentifie
use dyn_any::DynAny;
use glam::IVec2;
use log::Metadata;
use rustc_hash::{FxBuildHasher, FxHashMap};
use rustc_hash::FxHashMap;
use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
@ -32,7 +32,7 @@ fn return_true() -> bool {
/// An instance of a [`DocumentNodeDefinition`] that has been instantiated in a [`NodeNetwork`].
/// Currently, when an instance is made, it lives all on its own without any lasting connection to the definition.
/// But we will want to change it in the future so it merely references its definition.
#[derive(Clone, Debug, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, PartialEq, DynAny, serde::Serialize, serde::Deserialize)]
pub struct DocumentNode {
/// The inputs to a node, which are either:
/// - From other nodes within this graph [`NodeInput::Node`],
@ -172,7 +172,7 @@ impl DocumentNode {
}
/// Represents the possible inputs to a node.
#[derive(Debug, Clone, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, Hash, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
pub enum NodeInput {
/// A reference to another node in the same network from which this node can receive its input.
Node { node_id: NodeId, output_index: usize },
@ -196,7 +196,7 @@ pub enum NodeInput {
Inline(InlineRust),
}
#[derive(Debug, Clone, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, Hash, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
pub struct InlineRust {
pub expr: String,
pub ty: Type,
@ -208,7 +208,7 @@ impl InlineRust {
}
}
#[derive(Debug, Clone, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Debug, Clone, PartialEq, Hash, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
pub enum DocumentNodeMetadata {
DocumentNodePath,
}
@ -292,7 +292,7 @@ pub enum OldDocumentNodeImplementation {
Extract,
}
#[derive(Clone, Debug, PartialEq, Hash, DynAny, serde::Serialize, serde::Deserialize)]
#[derive(Clone, Debug, PartialEq, core_types::CacheHash, DynAny, serde::Serialize, serde::Deserialize)]
/// Represents the implementation of a node, which can be a nested [`NodeNetwork`], a proto [`ProtoNodeIdentifier`], or `Extract`.
pub enum DocumentNodeImplementation {
/// This describes a (document) node built out of a subgraph of other (document) nodes.
@ -546,29 +546,42 @@ pub struct NodeNetwork {
pub generated: bool,
}
impl Hash for NodeNetwork {
fn hash<H: Hasher>(&self, state: &mut H) {
self.exports.hash(state);
impl core_types::CacheHash for NodeNetwork {
fn cache_hash<H: ::core::hash::Hasher>(&self, state: &mut H) {
self.exports.cache_hash(state);
let mut nodes: Vec<_> = self.nodes.iter().collect();
nodes.sort_by_key(|(id, _)| *id);
for (id, node) in nodes {
id.hash(state);
node.hash(state);
id.cache_hash(state);
node.cache_hash(state);
}
let mut scope_injections: Vec<_> = self.scope_injections.iter().collect();
scope_injections.sort_by_key(|(key, _)| key.as_str());
for (key, (node_id, ty)) in scope_injections {
key.cache_hash(state);
node_id.cache_hash(state);
ty.cache_hash(state);
}
}
}
impl PartialEq for NodeNetwork {
fn eq(&self, other: &Self) -> bool {
self.exports == other.exports
self.exports == other.exports && self.nodes == other.nodes && self.scope_injections == other.scope_injections
}
}
/// Graph modification functions
impl NodeNetwork {
pub fn current_hash(&self) -> u64 {
use std::hash::BuildHasher;
FxBuildHasher.hash_one(self)
use core_types::graphene_hash::CacheHash;
use rustc_hash::FxHasher;
use std::hash::Hasher;
let mut hasher = FxHasher::default();
self.cache_hash(&mut hasher);
hasher.finish()
}
pub fn value_network(node: DocumentNode) -> Self {
@ -1136,6 +1149,17 @@ fn migrate_call_argument<'de, D: serde::Deserializer<'de>>(deserializer: D) -> R
})
}
impl core_types::graphene_hash::CacheHash for DocumentNode {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.inputs.cache_hash(state);
self.call_argument.cache_hash(state);
self.implementation.cache_hash(state);
self.visible.cache_hash(state);
self.skip_deduplication.cache_hash(state);
self.context_features.cache_hash(state);
}
}
#[cfg(test)]
mod test {
use super::*;
@ -1254,11 +1278,11 @@ mod test {
};
network.populate_dependants();
network.flatten_with_fns(NodeId(1), |self_id, inner_id| NodeId(self_id.0 * 10 + inner_id.0), gen_node_id);
let flat_network = flat_network();
println!("{flat_network:#?}");
let expected = flatten_add_expected();
println!("{expected:#?}");
println!("{network:#?}");
assert_eq!(flat_network, network);
assert_eq!(expected, network);
}
#[test]
@ -1345,6 +1369,55 @@ mod test {
pretty_assertions::assert_eq!(resolved_network[0], construction_network);
}
fn flatten_add_expected() -> NodeNetwork {
NodeNetwork {
exports: vec![NodeInput::node(NodeId(11), 0)],
nodes: [
(
NodeId(10),
DocumentNode {
inputs: vec![NodeInput::import(concrete!(u32), 0), NodeInput::node(NodeId(14), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("core_types::structural::ConsNode")),
original_location: OriginalLocation {
inputs_source: [(Source { node: vec![], index: 0 }, 1)].into(),
dependants: vec![vec![NodeId(11)]],
..Default::default()
},
..Default::default()
},
),
(
NodeId(14),
DocumentNode {
inputs: vec![NodeInput::value(TaggedValue::U32(2), false)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("core_types::value::ClonedNode")),
original_location: OriginalLocation {
path: Some(vec![NodeId(4)]),
dependants: vec![vec![NodeId(1), NodeId(10)]],
..Default::default()
},
..Default::default()
},
),
(
NodeId(11),
DocumentNode {
inputs: vec![NodeInput::node(NodeId(10), 0)],
implementation: DocumentNodeImplementation::ProtoNode(ProtoNodeIdentifier::new("core_types::ops::AddPairNode")),
original_location: OriginalLocation {
dependants: vec![vec![]],
..Default::default()
},
..Default::default()
},
),
]
.into_iter()
.collect(),
..Default::default()
}
}
fn flat_network() -> NodeNetwork {
NodeNetwork {
exports: vec![NodeInput::node(NodeId(11), 0)],

View File

@ -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<PlatformEditorApi>)
}
// 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<H: std::hash::Hasher>(&self, state: &mut H) {
impl CacheHash for TaggedValue {
fn cache_hash<H: core::hash::Hasher>(&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<dyn DynAny>
pub fn to_dynany(self) -> DAny<'a> {
@ -495,96 +494,33 @@ pub enum RenderOutputType {
},
}
impl Hash for RenderOutputType {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
impl CacheHash for RenderOutputType {
fn cache_hash<H: core::hash::Hasher>(&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 {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.data.hash(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<H: core::hash::Hasher>(&self, state: &mut H);
}
mod fake_hash {
use super::*;
impl FakeHash for f64 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_bits().hash(state)
}
}
impl FakeHash for f32 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_bits().hash(state)
}
}
impl FakeHash for DVec2 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_array().iter().for_each(|x| x.to_bits().hash(state))
}
}
impl FakeHash for Vec2 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_array().iter().for_each(|x| x.to_bits().hash(state))
}
}
impl FakeHash for DAffine2 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_cols_array().iter().for_each(|x| x.to_bits().hash(state))
}
}
impl FakeHash for Affine2 {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.to_cols_array().iter().for_each(|x| x.to_bits().hash(state))
}
}
impl<T: FakeHash> FakeHash for Option<T> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
if let Some(x) = self {
1.hash(state);
x.hash(state);
} else {
0.hash(state);
}
}
}
impl<T: FakeHash> FakeHash for Vec<T> {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.len().hash(state);
self.iter().for_each(|x| x.hash(state))
}
}
impl<T: FakeHash, const N: usize> FakeHash for [T; N] {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.iter().for_each(|x| x.hash(state))
}
}
impl FakeHash for (f64, Color) {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.0.to_bits().hash(state);
self.1.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<H: core::hash::Hasher>(&self, state: &mut H) {
self.data.cache_hash(state);
}
}

View File

@ -69,7 +69,7 @@ pub async fn export_document(
}
RenderOutputType::Texture(image_texture) => {
// Convert GPU texture to CPU buffer
let gpu_raster = Raster::<GPU>::new_gpu(image_texture.texture.as_ref().clone());
let gpu_raster = Raster::<GPU>::new_gpu(image_texture.as_ref().clone());
let cpu_raster: Raster<CPU> = gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor).await;
let (data, width, height) = cpu_raster.to_flat_u8();
// Explicitly drop texture to make sure it lives long enough
@ -202,7 +202,7 @@ pub async fn export_gif(
let (data, img_width, img_height) = match result {
TaggedValue::RenderOutput(output) => match output.data {
RenderOutputType::Texture(image_texture) => {
let gpu_raster = Raster::<GPU>::new_gpu(image_texture.texture.as_ref().clone());
let gpu_raster = Raster::<GPU>::new_gpu(image_texture.as_ref().clone());
let cpu_raster: Raster<CPU> = gpu_raster.convert(Footprint::BOUNDLESS, wgpu_executor).await;
// Explicitly drop texture to make sure it lives long enough
std::mem::drop(image_texture);

View File

@ -4,12 +4,12 @@ use clap::{Args, Parser, Subcommand};
use fern::colors::{Color, ColoredLevelConfig};
use futures::executor::block_on;
use graph_craft::application_io::EditorPreferences;
use graph_craft::application_io::{PlatformApplicationIo, PlatformEditorApi};
use graph_craft::document::*;
use graph_craft::graphene_compiler::Compiler;
use graph_craft::proto::ProtoNetwork;
use graph_craft::util::load_network;
use graphene_std::application_io::{ApplicationIo, NodeGraphUpdateMessage, NodeGraphUpdateSender};
use graphene_std::application_io::{PlatformEditorApi, WasmApplicationIo};
use graphene_std::text::FontCache;
use interpreted_executor::dynamic_executor::DynamicExecutor;
use interpreted_executor::util::wrap_network_in_scope;
@ -121,7 +121,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
let document_string = std::fs::read_to_string(document_path).expect("Failed to read document");
log::info!("Creating GPU context");
let mut application_io = block_on(WasmApplicationIo::new());
let mut application_io = block_on(PlatformApplicationIo::new());
if let Command::Export { image: Some(ref image_path), .. } = app.command {
application_io.resources.insert("null".to_string(), Arc::from(std::fs::read(image_path).expect("Failed to read image")));

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

View File

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

View File

@ -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<BrushStyle, Raster<CPU>>,
brush_texture_cache: HashMap<CacheHashWrapper<BrushStyle>, Raster<CPU>>,
}
impl BrushCacheImpl {
@ -165,6 +166,12 @@ impl Hash for BrushCache {
}
}
impl graphene_hash::CacheHash for BrushCache {
fn cache_hash<H: core::hash::Hasher>(&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<Raster<CPU>>, 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<Raster<CPU>> {
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<CPU>) {
let mut inner = self.0.lock().unwrap();
inner.brush_texture_cache.insert(style, brush);
inner.brush_texture_cache.insert(CacheHashWrapper(style), brush);
}
}

View File

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

View File

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

View File

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

View File

@ -51,17 +51,17 @@ async fn context_modification<T>(
#[cfg(test)]
mod tests {
use super::*;
use core_types::graphene_hash::CacheHash;
use core_types::transform::Footprint;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::hash::Hasher;
/// Test that the hash of a nullified context remains stable even when nullified inputs change
/// Verifies that nullified context fields don't affect the cache hash — only the kept features matter.
#[test]
fn test_nullified_context_hash_stability() {
use core_types::Context;
use std::sync::Arc;
// Create original contexts using the Context type (Option<Arc<OwnedContextImpl>>)
let original_ctx: Context = Some(Arc::new(
OwnedContextImpl::empty()
.with_footprint(Footprint::default())
@ -71,53 +71,48 @@ mod tests {
.with_animation_time(20.25),
));
// Test nullifying different features - hash should remain stable for each nullification
let features_to_keep = ContextFeatures::empty(); // Nullify everything
// Create nullified context - this should only keep features specified in features_to_keep
let nullified_ctx = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), features_to_keep);
// Calculate hash of nullified context
let mut hasher1 = DefaultHasher::new();
nullified_ctx.hash(&mut hasher1);
let hash1 = hasher1.finish();
// Create a different original context with changed values
// A second context with different values for the nullified fields
let changed_ctx: Context = Some(Arc::new(
OwnedContextImpl::empty()
.with_footprint(Footprint::default()) // Same footprint
.with_footprint(Footprint::default())
.with_index(2)
.with_real_time(999.9) // Different real time
.with_real_time(999.9)
.with_vararg(Box::new("test"))
.with_animation_time(888.8), // Different animation time
.with_animation_time(888.8),
));
// Create nullified context from the changed original - should have same hash since everything is nullified
let nullified_changed_ctx = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), features_to_keep);
// Nullify everything — both should hash the same regardless of their field values
let features_to_keep = ContextFeatures::empty();
let nullified1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), features_to_keep);
let nullified2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), features_to_keep);
let mut hasher1 = DefaultHasher::new();
nullified1.cache_hash(&mut hasher1);
let mut hasher2 = DefaultHasher::new();
nullified_changed_ctx.hash(&mut hasher2);
let hash2 = hasher2.finish();
nullified2.cache_hash(&mut hasher2);
// Hash should be the same because all features were nullified
assert_eq!(hash1, hash2, "Hash of nullified context should remain stable regardless of input changes when features are nullified");
assert_eq!(
hasher1.finish(),
hasher2.finish(),
"Hash of nullified context should remain stable regardless of input changes when features are nullified"
);
// Test partial nullification - keep only footprint
// Keep only footprint and varargs — both have the same footprint and vararg, so hash should still match
let partial_features = ContextFeatures::FOOTPRINT | ContextFeatures::VARARGS;
let partial_nullified1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), partial_features);
let partial_nullified2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), partial_features);
let partial1 = OwnedContextImpl::from_flags(original_ctx.clone().unwrap(), partial_features);
let partial2 = OwnedContextImpl::from_flags(changed_ctx.clone().unwrap(), partial_features);
let mut hasher3 = DefaultHasher::new();
partial_nullified1.hash(&mut hasher3);
let hash3 = hasher3.finish();
partial1.cache_hash(&mut hasher3);
let mut hasher4 = DefaultHasher::new();
partial_nullified2.hash(&mut hasher4);
let hash4 = hasher4.finish();
partial2.cache_hash(&mut hasher4);
// These should be the same because both have the same footprint (Footprint::default()) and varargs
// and other features are nullified
assert_eq!(hash3, hash4, "Hash should be stable when keeping only footprint and footprint values are the same");
assert_eq!(
hasher3.finish(),
hasher4.finish(),
"Hash should be stable when keeping only footprint and varargs and their values are the same"
);
}
}

View File

@ -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<T: Into<DVec2>>(_: impl Ctx, #[implementations(DVec2, IVec2, UVec2
/// The X or Y component of a vec2.
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, 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]

View File

@ -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<I: Hash + Send + 'n, T: Clone + WasmNotSend>(input: I, #[data] cache: Arc<Mutex<Option<(u64, T)>>>, node: impl Node<I, Output = T>) -> T {
async fn memo<I: CacheHash + Send + 'n, T: Clone + WasmNotSend>(input: I, #[data] cache: Arc<Mutex<Option<(u64, T)>>>, node: impl Node<I, Output = T>) -> 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())) {

View File

@ -80,7 +80,7 @@ pub fn omit_element<T: graphic_types::graphic::OmitIndex + Clone + Default>(
}
#[node_macro::node(category("General"))]
async fn map<Item: AnyHash + Send + Sync + std::hash::Hash>(
async fn map<Item: AnyHash + Send + Sync + core_types::CacheHash>(
ctx: impl Ctx + CloneVarArgs + ExtractAll,
#[implementations(
Table<Graphic>,

View File

@ -120,7 +120,7 @@ fn string_to_bytes(_: impl Ctx, string: String) -> Vec<u8> {
#[node_macro::node(category("Web Request"), name("Image to Bytes"))]
fn image_to_bytes(_: impl Ctx, image: Table<Raster<CPU>>) -> Vec<u8> {
let Some(image) = image.iter().next() else { return vec![] };
image.element.data.iter().flat_map(|color| color.to_rgb8_srgb().into_iter()).collect::<Vec<u8>>()
image.element.data.iter().flat_map(|color| color.to_rgba8_srgb().into_iter()).collect::<Vec<u8>>()
}
/// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue.

View File

@ -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<Image<Color>, u64>;
type ImageData = HashMap<core_types::graphene_hash::CacheHashWrapper<Image<Color>>, 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)) => {

View File

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

View File

@ -1015,3 +1015,20 @@ fn exposure<T: Adjust<Color>>(
});
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
);
}

View File

@ -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<CurveManipulatorGroup>,
@ -25,28 +24,13 @@ impl Default for Curve {
}
}
impl Hash for Curve {
fn hash<H: Hasher>(&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<H: Hasher>(&self, state: &mut H) {
for c in self.handles.iter().chain([&self.anchor]).flatten() {
c.to_bits().hash(state);
}
}
}
pub struct ValueMapperNode<C> {
lut: Vec<C>,
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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<T: AsU64>(
}
#[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.