Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ members = [
"desktop/platform/mac",
"desktop/platform/win",
"document/container",
"document/graph-storage",
"editor",
"frontend/wrapper",
"libraries/dyn-any",
Expand Down Expand Up @@ -87,6 +88,7 @@ repeat-nodes = { path = "node-graph/nodes/repeat" }
math-nodes = { path = "node-graph/nodes/math" }
path-bool-nodes = { path = "node-graph/nodes/path-bool" }
graph-craft = { path = "node-graph/graph-craft" }
graph-storage = { path = "document/graph-storage" }
raster-nodes = { path = "node-graph/nodes/raster" }
graphene-std = { path = "node-graph/nodes/gstd" }
interpreted-executor = { path = "node-graph/interpreted-executor" }
Expand Down Expand Up @@ -167,6 +169,7 @@ color = "0.3"
# Linebender ecosystem (END)
rand = { version = "0.9", default-features = false, features = ["std_rng"] }
rand_chacha = "0.9"
rmp-serde = "1.3"
glam = { version = "0.32.1", default-features = false, features = [
"nostd-libm",
"scalar-math",
Expand Down
26 changes: 26 additions & 0 deletions document/graph-storage/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "graph-storage"
description = "Provides a delta based graph representation used in the Graphite file format"
edition.workspace = true
version.workspace = true
license.workspace = true
authors.workspace = true

[features]
conversion = ["dep:graph-craft"]
default = ["conversion"]

[dependencies]
graph-craft = { workspace = true, optional = true }
graphene-resource = { workspace = true }
core-types = { workspace = true }

thiserror = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
blake3 = { workspace = true }
rustc-hash = { workspace = true }
rmp-serde = { workspace = true }

[dev-dependencies]
graph-craft = { workspace = true, features = ["loading"] }
71 changes: 71 additions & 0 deletions document/graph-storage/src/attributes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use crate::TimeStamp;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

/// Attribute keys. Glob-import (`use crate::attr::*`) at conversion sites.
///
/// `ui::*` keys are namespaced per CRDT design so each value gets its own LWW timestamp. Per-input
/// keys live on `Node.inputs_attributes[i]`; per-network keys live on `Network.attributes`.
pub mod attr;

/// A type-erased attribute value paired with the timestamp at which it was last set.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Value {
pub value: serde_json::Value,
pub timestamp: TimeStamp,
}

impl Value {
pub fn new(value: serde_json::Value, timestamp: TimeStamp) -> Self {
Self { value, timestamp }
}
}

pub type Attributes = BTreeMap<String, Value>;

/// Write helpers for `Attributes`.
pub trait AttributesWrite {
/// Inserts a JSON value under `key`.
fn set(&mut self, key: &str, value: serde_json::Value, timestamp: TimeStamp);

/// Serializes `value` and inserts it under `key`.
fn set_serialized<T: serde::Serialize>(&mut self, key: &str, value: &T, timestamp: TimeStamp) -> Result<(), serde_json::Error> {
self.set(key, serde_json::to_value(value)?, timestamp);
Ok(())
}
/// Inserts only when `value != default`, so the read side falls back to the same default.
fn set_if_not_default<T: serde::Serialize + PartialEq>(&mut self, key: &str, value: &T, default: &T, timestamp: TimeStamp) -> Result<(), serde_json::Error> {
if value != default {
self.set_serialized(key, value, timestamp)?;
}
Ok(())
}
}

impl AttributesWrite for Attributes {
fn set(&mut self, key: &str, value: serde_json::Value, timestamp: TimeStamp) {
self.insert(key.to_string(), Value { value, timestamp });
}
}

/// Typed read helpers for `Attributes`.
pub trait AttributesRead {
/// Deserializes the value under `key`, or `None` if missing or undecodable.
fn get_typed<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T>;

/// Same as `get_typed`, falling back to `default`.
fn get_or<T: serde::de::DeserializeOwned>(&self, key: &str, default: T) -> T {
self.get_typed(key).unwrap_or(default)
}

/// Same as `get_typed`, falling back to `T::default()`.
fn get_or_default<T: serde::de::DeserializeOwned + Default>(&self, key: &str) -> T {
self.get_typed(key).unwrap_or_default()
}
}

impl AttributesRead for Attributes {
fn get_typed<T: serde::de::DeserializeOwned>(&self, key: &str) -> Option<T> {
self.get(key).and_then(|v| serde_json::from_value(v.value.clone()).ok())
}
}
68 changes: 68 additions & 0 deletions document/graph-storage/src/attributes/attr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
pub mod node {
pub const CALL_ARGUMENT: &str = "call_argument";
pub const CONTEXT_FEATURES: &str = "context_features";
pub const VISIBLE: &str = "visible";
pub const SKIP_DEDUPLICATION: &str = "skip_deduplication";
pub const REFLECTION_METADATA: &str = "reflection_metadata";
pub const ORIGINAL_NODE_ID: &str = "original_node_id";

pub mod input {
pub const IMPORT_TYPE: &str = "import_type";

pub mod ui {
pub const NAME: &str = "ui::name";
pub const DESCRIPTION: &str = "ui::description";
pub const WIDGET_OVERRIDE: &str = "ui::widget_override";
/// Prefix for `InputPersistentMetadata::data` entries. Full key: `ui::data::<sub_key>`.
pub const DATA_PREFIX: &str = "ui::data::"; // TODO: Remove and make runtime strongly typed again
}
}

pub mod ui {
pub const POSITION: &str = "ui::position";
pub const IS_LAYER: &str = "ui::is_layer";
pub const DISPLAY_NAME: &str = "ui::display_name";
pub const LOCKED: &str = "ui::locked";
pub const PINNED: &str = "ui::pinned";
pub const OUTPUT_NAMES: &str = "ui::output_names";
pub const REFERENCE: &str = "ui::reference"; // TODO: Remove?
}
}

pub mod session {
pub mod network {
pub const PREVIEWING: &str = "ui::previewing";

// TODO: Remove these graph ui nav-specific attributes
pub const NAV_PTZ: &str = "ui::nav::ptz";
pub const NAV_TRANSFORM: &str = "ui::nav::transform";
pub const NAV_WIDTH: &str = "ui::nav::width";
}

pub mod doc {
// Document-level editor chrome, stored in `Registry.attributes` (document scope). Each setting is
// its own key so concurrent edits to one don't clobber another.
pub const PTZ: &str = "ui::ptz";
pub const RENDER_MODE: &str = "ui::render_mode";
pub const OVERLAYS: &str = "ui::overlays";
pub const RULERS_VISIBLE: &str = "ui::rulers_visible";
pub const SNAPPING: &str = "ui::snapping";
pub const COLLAPSED: &str = "ui::collapsed";
}
}

pub mod registry {
pub const EXPORTED_NODES: &str = "exported_nodes";
}

pub mod network {
/// Whole-map LWW of a network's `scope_injections` (`key -> (storage NodeId, Type)`), stored as a
/// serialized blob so its shape can evolve (e.g. dropping the `Type`) without a model change. The
/// node references use stable storage IDs, resolved back to runtime-local IDs on conversion.
pub const SCOPE_INJECTIONS: &str = "scope_injections";
}

pub mod delta {
/// Marks the last delta of a user interaction, so the undo cursor steps per-interaction, not per-delta.
pub const INTERACTION_END: &str = "interaction_end";
}
Loading
Loading