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:
parent
7bb01c9651
commit
3d84e63ef9
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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" }
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)],
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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")));
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>),
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
@ -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"] }
|
||||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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>)>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>),
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>),
|
||||
|
|
|
|||
|
|
@ -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> {}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>>,
|
||||
|
|
|
|||
|
|
@ -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 { .. })
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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())) {
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)) => {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue