Implement content-addressed resource storage for raster images (#4148)

* Implement content-addressed resource storage

* Implement OPFS resource storage

* Remove ResourceStorage::read

* Review
This commit is contained in:
Timon
2026-05-21 19:31:07 +00:00
committed by GitHub
parent 21d2994059
commit 2770df7567
50 changed files with 1535 additions and 355 deletions
Generated
+63 -7
View File
@@ -304,6 +304,20 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "blake3"
version = "1.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
dependencies = [
"arrayref",
"arrayvec",
"cc",
"cfg-if",
"constant_time_eq",
"cpufeatures 0.3.0",
]
[[package]] [[package]]
name = "blending-nodes" name = "blending-nodes"
version = "0.1.0" version = "0.1.0"
@@ -812,6 +826,12 @@ dependencies = [
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
[[package]]
name = "constant_time_eq"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
[[package]] [[package]]
name = "convert_case" name = "convert_case"
version = "0.8.0" version = "0.8.0"
@@ -928,6 +948,15 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "crate-hierarchy-viz" name = "crate-hierarchy-viz"
version = "0.0.0" version = "0.0.0"
@@ -1527,7 +1556,7 @@ checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905"
dependencies = [ dependencies = [
"fontconfig-parser", "fontconfig-parser",
"log", "log",
"memmap2", "memmap2 0.9.10",
"slotmap", "slotmap",
"tinyvec", "tinyvec",
"ttf-parser", "ttf-parser",
@@ -1541,7 +1570,7 @@ checksum = "7c20b425addb8661e97fe1d51c4d8bcec3ec29ed6ad0db983976a7521276b8f7"
dependencies = [ dependencies = [
"hashbrown 0.17.1", "hashbrown 0.17.1",
"linebender_resource_handle", "linebender_resource_handle",
"memmap2", "memmap2 0.9.10",
"parlance", "parlance",
"read-fonts", "read-fonts",
"smallvec", "smallvec",
@@ -1853,6 +1882,7 @@ dependencies = [
"gungraun", "gungraun",
"js-sys", "js-sys",
"log", "log",
"mmap-io",
"pretty_assertions", "pretty_assertions",
"raster-nodes", "raster-nodes",
"rendering", "rendering",
@@ -1866,6 +1896,7 @@ dependencies = [
"url", "url",
"vector-nodes", "vector-nodes",
"wasm-bindgen", "wasm-bindgen",
"wasm-bindgen-futures",
"web-sys", "web-sys",
"wgpu-executor", "wgpu-executor",
"winit", "winit",
@@ -1875,6 +1906,7 @@ dependencies = [
name = "graphene-application-io" name = "graphene-application-io"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"blake3",
"core-types", "core-types",
"dyn-any", "dyn-any",
"glam", "glam",
@@ -3105,6 +3137,15 @@ version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[package]]
name = "memmap2"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "memmap2" name = "memmap2"
version = "0.9.10" version = "0.9.10"
@@ -3141,6 +3182,21 @@ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.59.0",
] ]
[[package]]
name = "mmap-io"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a34b926fd173419d630eb9d8ee3a485345237bf699d6f7ea6e5c1ee97cf1fa47"
dependencies = [
"anyhow",
"cfg-if",
"libc",
"log",
"memmap2 0.7.1",
"parking_lot",
"thiserror 1.0.69",
]
[[package]] [[package]]
name = "muda" name = "muda"
version = "0.17.1" version = "0.17.1"
@@ -4852,7 +4908,7 @@ checksum = "1dd3accc0f3f4bbaf2c9e1957a030dc582028130c67660d44c0a0345a22ca69b"
dependencies = [ dependencies = [
"ab_glyph", "ab_glyph",
"log", "log",
"memmap2", "memmap2 0.9.10",
"smithay-client-toolkit", "smithay-client-toolkit",
"tiny-skia", "tiny-skia",
] ]
@@ -5034,7 +5090,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"cpufeatures", "cpufeatures 0.2.17",
"digest", "digest",
] ]
@@ -5120,7 +5176,7 @@ dependencies = [
"cursor-icon", "cursor-icon",
"libc", "libc",
"log", "log",
"memmap2", "memmap2 0.9.10",
"rustix", "rustix",
"thiserror 2.0.18", "thiserror 2.0.18",
"wayland-backend", "wayland-backend",
@@ -7230,7 +7286,7 @@ name = "winit-common"
version = "0.30.12" version = "0.30.12"
source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b" source = "git+https://github.com/rust-windowing/winit.git#bd6fef1d80ba063cbe91e150b3fb343927cdc72b"
dependencies = [ dependencies = [
"memmap2", "memmap2 0.9.10",
"objc2 0.6.4", "objc2 0.6.4",
"objc2-core-foundation", "objc2-core-foundation",
"smol_str", "smol_str",
@@ -7302,7 +7358,7 @@ dependencies = [
"cursor-icon", "cursor-icon",
"dpi", "dpi",
"libc", "libc",
"memmap2", "memmap2 0.9.10",
"raw-window-handle", "raw-window-handle",
"rustix", "rustix",
"sctk-adwaita", "sctk-adwaita",
+2
View File
@@ -171,6 +171,8 @@ glam = { version = "0.32.1", default-features = false, features = [
"bytemuck", "bytemuck",
] } ] }
base64 = "0.22" base64 = "0.22"
blake3 = "1.5"
mmap-io = { version = "0.9", features = ["hugepages"] }
image = { version = "0.25", default-features = false, features = [ image = { version = "0.25", default-features = false, features = [
"png", "png",
"jpeg", "jpeg",
+4 -2
View File
@@ -16,13 +16,14 @@ use winit::window::WindowId;
use crate::cef; use crate::cef;
use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS; use crate::consts::CEF_MESSAGE_LOOP_MAX_ITERATIONS;
use crate::dirs;
use crate::event::{AppEvent, AppEventScheduler}; use crate::event::{AppEvent, AppEventScheduler};
use crate::persist; use crate::persist;
use crate::preferences; use crate::preferences;
use crate::render::{RenderError, RenderState}; use crate::render::{RenderError, RenderState};
use crate::window::Window; use crate::window::Window;
use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Preferences}; use crate::wrapper::messages::{DesktopFrontendMessage, DesktopWrapperMessage, InputMessage, MouseKeys, MouseState, Preferences};
use crate::wrapper::{DesktopWrapper, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages}; use crate::wrapper::{DesktopWrapper, MmapResourceStorage, NodeGraphExecutionResult, WgpuContext, serialize_frontend_messages};
pub(crate) struct App { pub(crate) struct App {
render_state: Option<RenderState>, render_state: Option<RenderState>,
@@ -91,7 +92,8 @@ impl App {
} }
}); });
let desktop_wrapper = DesktopWrapper::new(rand::rng().random()); let resource_storage = MmapResourceStorage::new(dirs::app_resources_dir()).expect("Failed to initialize on-disk resource storage");
let desktop_wrapper = DesktopWrapper::new(rand::rng().random(), Box::new(resource_storage));
Self { Self {
render_state: None, render_state: None,
+1
View File
@@ -11,6 +11,7 @@ pub(crate) const APP_SOCKET_FILE_NAME: &str = "instance.sock";
pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron"; pub(crate) const APP_STATE_FILE_NAME: &str = "state.ron";
pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron"; pub(crate) const APP_PREFERENCES_FILE_NAME: &str = "preferences.ron";
pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents"; pub(crate) const APP_DOCUMENTS_DIRECTORY_NAME: &str = "documents";
pub(crate) const APP_RESOURCES_DIRECTORY_NAME: &str = "resources";
// CEF configuration constants // CEF configuration constants
pub(crate) const CEF_WINDOWLESS_FRAME_RATE: i32 = 60; pub(crate) const CEF_WINDOWLESS_FRAME_RATE: i32 = 60;
+7 -1
View File
@@ -2,7 +2,7 @@ use std::fs;
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use crate::consts::{APP_DIRECTORY_NAME, APP_DOCUMENTS_DIRECTORY_NAME}; use crate::consts::{APP_DIRECTORY_NAME, APP_DOCUMENTS_DIRECTORY_NAME, APP_RESOURCES_DIRECTORY_NAME};
pub(crate) fn ensure_dir_exists(path: &PathBuf) { pub(crate) fn ensure_dir_exists(path: &PathBuf) {
if !path.exists() { if !path.exists() {
@@ -51,6 +51,12 @@ pub(crate) fn app_autosave_documents_dir() -> PathBuf {
path path
} }
pub(crate) fn app_resources_dir() -> PathBuf {
let path = app_data_dir().join(APP_RESOURCES_DIRECTORY_NAME);
ensure_dir_exists(&path);
path
}
/// Temporary directory that is automatically deleted when dropped. /// Temporary directory that is automatically deleted when dropped.
pub struct TempDir { pub struct TempDir {
path: PathBuf, path: PathBuf,
@@ -7,6 +7,10 @@ use super::messages::{DesktopFrontendMessage, FileFilter, OpenFileDialogContext,
pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option<FrontendMessage> { pub(super) fn intercept_frontend_message(dispatcher: &mut DesktopWrapperMessageDispatcher, message: FrontendMessage) -> Option<FrontendMessage> {
match message { match message {
FrontendMessage::Await { future } => {
let message = futures::executor::block_on(async move { future.await });
return intercept_frontend_message(dispatcher, message);
}
FrontendMessage::RenderOverlays { context } => { FrontendMessage::RenderOverlays { context } => {
dispatcher.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene())); dispatcher.respond(DesktopFrontendMessage::UpdateOverlays(context.take_scene()));
} }
+6 -5
View File
@@ -1,9 +1,10 @@
use graph_craft::application_io::PlatformApplicationIo; use graph_craft::application_io::{PlatformApplicationIo, ResourceStorage};
use graphite_editor::application::{Editor, Environment, Host, Platform}; use graphite_editor::application::{Editor, Environment, Host, Platform};
use graphite_editor::messages::prelude::{FrontendMessage, Message}; use graphite_editor::messages::prelude::{FrontendMessage, Message};
use message_dispatcher::DesktopWrapperMessageDispatcher; use message_dispatcher::DesktopWrapperMessageDispatcher;
use messages::{DesktopFrontendMessage, DesktopWrapperMessage}; use messages::{DesktopFrontendMessage, DesktopWrapperMessage};
pub use graph_craft::application_io::MmapResourceStorage;
pub use graphite_editor::consts::{DOUBLE_CLICK_MILLISECONDS, FILE_EXTENSION}; pub use graphite_editor::consts::{DOUBLE_CLICK_MILLISECONDS, FILE_EXTENSION};
pub use wgpu_executor::WgpuContext; pub use wgpu_executor::WgpuContext;
pub use wgpu_executor::WgpuContextBuilder; pub use wgpu_executor::WgpuContextBuilder;
@@ -22,7 +23,7 @@ pub struct DesktopWrapper {
} }
impl DesktopWrapper { impl DesktopWrapper {
pub fn new(uuid_random_seed: u64) -> Self { pub fn new(uuid_random_seed: u64, resource_storage: Box<dyn ResourceStorage>) -> Self {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
let host = Host::Windows; let host = Host::Windows;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
@@ -32,13 +33,13 @@ impl DesktopWrapper {
let env = Environment { platform: Platform::Desktop, host }; let env = Environment { platform: Platform::Desktop, host };
Self { Self {
editor: Editor::new(env, uuid_random_seed), editor: Editor::new(env, uuid_random_seed, resource_storage),
} }
} }
pub fn init(&self, wgpu_context: WgpuContext) { pub fn init(&mut self, wgpu_context: WgpuContext) {
let application_io = PlatformApplicationIo::new_with_context(wgpu_context); let application_io = PlatformApplicationIo::new_with_context(wgpu_context);
futures::executor::block_on(graphite_editor::node_graph_executor::replace_application_io(application_io)); self.editor.replace_application_io(application_io);
} }
pub fn dispatch(&mut self, message: DesktopWrapperMessage) -> Vec<DesktopFrontendMessage> { pub fn dispatch(&mut self, message: DesktopWrapperMessage) -> Vec<DesktopFrontendMessage> {
+15 -3
View File
@@ -1,5 +1,6 @@
use crate::dispatcher::Dispatcher; use crate::dispatcher::Dispatcher;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graph_craft::application_io::{PlatformApplicationIo, ResourceStorage};
pub use graphene_std::uuid::*; pub use graphene_std::uuid::*;
use std::sync::OnceLock; use std::sync::OnceLock;
@@ -8,11 +9,13 @@ pub struct Editor {
} }
impl Editor { impl Editor {
pub fn new(environment: Environment, uuid_random_seed: u64) -> Self { pub fn new(environment: Environment, uuid_random_seed: u64, resource_storage: Box<dyn ResourceStorage>) -> Self {
ENVIRONMENT.set(environment).expect("Editor shoud only be initialized once"); ENVIRONMENT.set(environment).expect("Editor shoud only be initialized once");
graphene_std::uuid::set_uuid_seed(uuid_random_seed); graphene_std::uuid::set_uuid_seed(uuid_random_seed);
Self { dispatcher: Dispatcher::new() } Self {
dispatcher: Dispatcher::new(resource_storage),
}
} }
#[cfg(test)] #[cfg(test)]
@@ -20,11 +23,15 @@ impl Editor {
let _ = ENVIRONMENT.set(*Editor::environment()); let _ = ENVIRONMENT.set(*Editor::environment());
graphene_std::uuid::set_uuid_seed(0); graphene_std::uuid::set_uuid_seed(0);
let (runtime, executor) = crate::node_graph_executor::NodeGraphExecutor::new_with_local_runtime(); let (mut runtime, executor) = crate::node_graph_executor::NodeGraphExecutor::new_with_local_runtime();
let editor = Self { let editor = Self {
dispatcher: Dispatcher::with_executor(executor), dispatcher: Dispatcher::with_executor(executor),
}; };
let mut application_io = PlatformApplicationIo::default();
application_io.inject_resource_proxy(editor.dispatcher.message_handlers.resource_message_handler.resources());
runtime.replace_application_io(application_io);
(editor, runtime) (editor, runtime)
} }
@@ -37,6 +44,11 @@ impl Editor {
pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque<Message>) -> Result<(), String> { pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque<Message>) -> Result<(), String> {
self.dispatcher.poll_node_graph_evaluation(responses) self.dispatcher.poll_node_graph_evaluation(responses)
} }
pub fn replace_application_io(&mut self, mut application_io: PlatformApplicationIo) {
application_io.inject_resource_proxy(self.dispatcher.message_handlers.resource_message_handler.resources());
crate::node_graph_executor::replace_application_io(application_io)
}
} }
static ENVIRONMENT: OnceLock<Environment> = OnceLock::new(); static ENVIRONMENT: OnceLock<Environment> = OnceLock::new();
+21 -7
View File
@@ -6,6 +6,7 @@ use crate::messages::portfolio::utility_types::PanelType;
use crate::messages::preferences::preferences_message_handler::PreferencesMessageContext; use crate::messages::preferences::preferences_message_handler::PreferencesMessageContext;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed; use crate::messages::tool::common_functionality::utility_functions::make_path_editable_is_allowed;
use graph_craft::application_io::ResourceStorage;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct Dispatcher { pub struct Dispatcher {
@@ -31,15 +32,23 @@ pub struct DispatcherMessageHandlers {
menu_bar_message_handler: MenuBarMessageHandler, menu_bar_message_handler: MenuBarMessageHandler,
pub(crate) portfolio_message_handler: PortfolioMessageHandler, pub(crate) portfolio_message_handler: PortfolioMessageHandler,
preferences_message_handler: PreferencesMessageHandler, preferences_message_handler: PreferencesMessageHandler,
pub(crate) resource_message_handler: ResourceMessageHandler,
tool_message_handler: ToolMessageHandler, tool_message_handler: ToolMessageHandler,
viewport_message_handler: ViewportMessageHandler, viewport_message_handler: ViewportMessageHandler,
} }
impl DispatcherMessageHandlers { impl DispatcherMessageHandlers {
pub fn with_resource_storage(resource_storage: Box<dyn ResourceStorage>) -> Self {
Self {
resource_message_handler: ResourceMessageHandler::new(resource_storage),
..Self::default()
}
}
pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self { pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self {
Self { Self {
portfolio_message_handler: PortfolioMessageHandler::with_executor(executor), portfolio_message_handler: PortfolioMessageHandler::with_executor(executor),
..Default::default() ..Self::default()
} }
} }
} }
@@ -78,15 +87,16 @@ const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &["PointerMove", "PointerOutsideViewport", "Overlays", "Draw", "CurrentTime", "Time"]; const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &["PointerMove", "PointerOutsideViewport", "Overlays", "Draw", "CurrentTime", "Time"];
impl Dispatcher { impl Dispatcher {
pub fn new() -> Self { pub fn new(resource_storage: Box<dyn ResourceStorage>) -> Self {
Self::default() let mut s = Self::default();
s.message_handlers.resource_message_handler = ResourceMessageHandler::new(resource_storage);
s
} }
pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self { pub fn with_executor(executor: crate::node_graph_executor::NodeGraphExecutor) -> Self {
Self { let mut s = Self::default();
message_handlers: DispatcherMessageHandlers::with_executor(executor), s.message_handlers.portfolio_message_handler = PortfolioMessageHandler::with_executor(executor);
..Default::default() s
}
} }
// If the deepest queues (higher index in queues list) are now empty (after being popped from) then remove them // If the deepest queues (higher index in queues list) are now empty (after being popped from) then remove them
@@ -218,6 +228,9 @@ impl Dispatcher {
self.message_handlers.layout_message_handler.process_message(message, &mut queue, context); self.message_handlers.layout_message_handler.process_message(message, &mut queue, context);
} }
Message::Resource(message) => {
self.message_handlers.resource_message_handler.process_message(message, &mut queue, ResourceMessageContext {});
}
Message::Portfolio(message) => { Message::Portfolio(message) => {
self.message_handlers.portfolio_message_handler.process_message( self.message_handlers.portfolio_message_handler.process_message(
message, message,
@@ -230,6 +243,7 @@ impl Dispatcher {
timing_information: self.message_handlers.animation_message_handler.timing_information(), timing_information: self.message_handlers.animation_message_handler.timing_information(),
animation: &self.message_handlers.animation_message_handler, animation: &self.message_handlers.animation_message_handler,
viewport: &self.message_handlers.viewport_message_handler, viewport: &self.message_handlers.viewport_message_handler,
resources: &self.message_handlers.resource_message_handler,
}, },
); );
} }
@@ -3,7 +3,7 @@ use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle; use crate::messages::portfolio::document::utility_types::wires::GraphWireStyle;
use crate::messages::preferences::SelectionMode; use crate::messages::preferences::SelectionMode;
use crate::messages::prelude::*; use crate::messages::prelude::*;
use graphene_std::render_node::{EditorPreferences, wgpu_available}; use graph_craft::application_io::EditorPreferences;
#[derive(ExtractField)] #[derive(ExtractField)]
pub struct PreferencesDialogMessageContext<'a> { pub struct PreferencesDialogMessageContext<'a> {
@@ -307,7 +307,7 @@ impl PreferencesDialogMessageHandler {
// COMPATIBILITY // COMPATIBILITY
// ============= // =============
{ {
let wgpu_available = wgpu_available().unwrap_or(false); let wgpu_available = graph_craft::application_io::wgpu_available().unwrap_or(false);
let is_desktop = cfg!(not(target_family = "wasm")); let is_desktop = cfg!(not(target_family = "wasm"));
if wgpu_available || is_desktop { if wgpu_available || is_desktop {
let header = vec![TextLabel::new("Compatibility").italic(true).widget_instance()]; let header = vec![TextLabel::new("Compatibility").italic(true).widget_instance()];
@@ -1,7 +1,7 @@
use super::IconName; use super::IconName;
use super::utility_types::{MouseCursorIcon, PersistedState}; use super::utility_types::{MouseCursorIcon, PersistedState};
use crate::messages::app_window::app_window_message_handler::AppWindowPlatform; use crate::messages::app_window::app_window_message_handler::AppWindowPlatform;
use crate::messages::frontend::utility_types::{DocumentInfo, EyedropperPreviewImage}; use crate::messages::frontend::utility_types::{DocumentInfo, EyedropperPreviewImage, FrontendMessageFuture};
use crate::messages::input_mapper::utility_types::misc::ActionShortcut; use crate::messages::input_mapper::utility_types::misc::ActionShortcut;
use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::layout::utility_types::widget_prelude::*;
use crate::messages::portfolio::document::node_graph::utility_types::{ use crate::messages::portfolio::document::node_graph::utility_types::{
@@ -27,6 +27,13 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex
#[derive(derivative::Derivative, Clone, serde::Serialize, serde::Deserialize)] #[derive(derivative::Derivative, Clone, serde::Serialize, serde::Deserialize)]
#[derivative(Debug, PartialEq)] #[derivative(Debug, PartialEq)]
pub enum FrontendMessage { pub enum FrontendMessage {
Await {
#[serde(skip, default)]
#[derivative(Debug = "ignore", PartialEq = "ignore")]
#[cfg_attr(feature = "wasm", tsify(type = "unknown"))]
future: FrontendMessageFuture,
},
// Display prefix: make the frontend show something, like a dialog // Display prefix: make the frontend show something, like a dialog
DisplayDialog { DisplayDialog {
title: String, title: String,
+1
View File
@@ -4,6 +4,7 @@ pub mod utility_types;
#[doc(inline)] #[doc(inline)]
pub use frontend_message::{FrontendMessage, FrontendMessageDiscriminant}; pub use frontend_message::{FrontendMessage, FrontendMessageDiscriminant};
pub use utility_types::FrontendMessageFuture;
// TODO: Make this an enum with the actual icon names, somehow derived from or tied to the frontend icon set. // TODO: Make this an enum with the actual icon names, somehow derived from or tied to the frontend icon set.
// TODO: Then remove `#[widget_builder(string)]` from all icon fields. // TODO: Then remove `#[widget_builder(string)]` from all icon fields.
@@ -1,4 +1,9 @@
use std::future::{Future, IntoFuture};
use std::path::PathBuf; use std::path::PathBuf;
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use graph_craft::application_io::ResourceHash;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::utility_types::WorkspacePanelLayout; use crate::messages::portfolio::utility_types::WorkspacePanelLayout;
@@ -10,6 +15,9 @@ pub struct DocumentInfo {
pub id: DocumentId, pub id: DocumentId,
pub name: String, pub name: String,
#[serde(default)] #[serde(default)]
#[cfg_attr(feature = "wasm", tsify(type = "unknown"))]
pub resources: Option<Box<[ResourceHash]>>,
#[serde(default)]
pub path: Option<PathBuf>, pub path: Option<PathBuf>,
pub is_saved: bool, pub is_saved: bool,
} }
@@ -77,3 +85,31 @@ pub struct EyedropperPreviewImage {
pub width: u32, pub width: u32,
pub height: u32, pub height: u32,
} }
#[derive(Clone, Default)]
pub struct FrontendMessageFuture {
inner: Arc<Mutex<Option<InnerFrontendMessageFuture>>>,
}
impl FrontendMessageFuture {
pub fn new(future: impl Future<Output = FrontendMessage> + Send + 'static) -> Self {
Self {
inner: Arc::new(Mutex::new(Some(Box::pin(future)))),
}
}
}
type InnerFrontendMessageFuture = Pin<Box<dyn Future<Output = FrontendMessage> + Send + 'static>>;
impl IntoFuture for FrontendMessageFuture {
type Output = FrontendMessage;
type IntoFuture = InnerFrontendMessageFuture;
fn into_future(self) -> Self::IntoFuture {
self.inner
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner())
.take()
.expect("FrontendMessageFuture can only be awaited once")
}
}
+2
View File
@@ -36,6 +36,8 @@ pub enum Message {
#[child] #[child]
Preferences(PreferencesMessage), Preferences(PreferencesMessage),
#[child] #[child]
Resource(ResourceMessage),
#[child]
Tool(ToolMessage), Tool(ToolMessage),
#[child] #[child]
Viewport(ViewportMessage), Viewport(ViewportMessage),
+1
View File
@@ -17,5 +17,6 @@ pub mod message;
pub mod portfolio; pub mod portfolio;
pub mod preferences; pub mod preferences;
pub mod prelude; pub mod prelude;
pub mod resource;
pub mod tool; pub mod tool;
pub mod viewport; pub mod viewport;
@@ -19,6 +19,7 @@ use crate::messages::portfolio::document::overlays::grid_overlays::{grid_overlay
use crate::messages::portfolio::document::overlays::utility_types::{OverlaysType, OverlaysVisibilitySettings, Pivot}; use crate::messages::portfolio::document::overlays::utility_types::{OverlaysType, OverlaysVisibilitySettings, Pivot};
use crate::messages::portfolio::document::properties_panel::properties_panel_message_handler::PropertiesPanelMessageContext; use crate::messages::portfolio::document::properties_panel::properties_panel_message_handler::PropertiesPanelMessageContext;
use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier}; use crate::messages::portfolio::document::utility_types::document_metadata::{DocumentMetadata, LayerNodeIdentifier};
use crate::messages::portfolio::document::utility_types::embedded_resources::EmbeddedResources;
use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, PTZ}; use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, PTZ};
use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate, OutputConnector}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, InputConnector, NodeTemplate, OutputConnector};
use crate::messages::portfolio::utility_types::{CachedData, PanelType}; use crate::messages::portfolio::utility_types::{CachedData, PanelType};
@@ -29,13 +30,13 @@ use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::ToolType; use crate::messages::tool::utility_types::ToolType;
use crate::node_graph_executor::NodeGraphExecutor; use crate::node_graph_executor::NodeGraphExecutor;
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graph_craft::application_io::{ResourceHash, wgpu_available};
use graph_craft::descriptor; use graph_craft::descriptor;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork}; use graph_craft::document::{NodeId, NodeInput, NodeNetwork, OldNodeNetwork};
use graphene_std::math::quad::Quad; use graphene_std::math::quad::Quad;
use graphene_std::path_bool_nodes::boolean_intersect; use graphene_std::path_bool_nodes::boolean_intersect;
use graphene_std::raster::BlendMode; use graphene_std::raster::BlendMode;
use graphene_std::render_node::wgpu_available;
use graphene_std::subpath::Subpath; use graphene_std::subpath::Subpath;
use graphene_std::vector::PointId; use graphene_std::vector::PointId;
use graphene_std::vector::click_target::{ClickTarget, ClickTargetType}; use graphene_std::vector::click_target::{ClickTarget, ClickTargetType};
@@ -59,6 +60,7 @@ pub struct DocumentMessageContext<'a> {
pub layers_panel_open: bool, pub layers_panel_open: bool,
pub properties_panel_open: bool, pub properties_panel_open: bool,
pub viewport: &'a ViewportMessageHandler, pub viewport: &'a ViewportMessageHandler,
pub resources: &'a ResourceMessageHandler,
} }
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, ExtractField)] #[derive(Clone, Debug, serde::Serialize, serde::Deserialize, ExtractField)]
@@ -107,6 +109,9 @@ pub struct DocumentMessageHandler {
pub graph_view_overlay_open: bool, pub graph_view_overlay_open: bool,
/// The current opacity of the faded node graph background that covers up the artwork. /// The current opacity of the faded node graph background that covers up the artwork.
pub graph_fade_artwork_percentage: f64, pub graph_fade_artwork_percentage: f64,
/// Resources embedded in the document. Only propagated if saving to an external file and never for autosaved documents.
#[serde(rename = "resources", default, skip_serializing_if = "EmbeddedResources::is_empty")]
pub embedded_resources: EmbeddedResources,
// ============================================= // =============================================
// Fields omitted from the saved document format // Fields omitted from the saved document format
@@ -169,6 +174,7 @@ impl Default for DocumentMessageHandler {
graph_view_overlay_open: false, graph_view_overlay_open: false,
snapping_state: SnappingState::default(), snapping_state: SnappingState::default(),
graph_fade_artwork_percentage: 80., graph_fade_artwork_percentage: 80.,
embedded_resources: EmbeddedResources::default(),
// ============================================= // =============================================
// Fields omitted from the saved document format // Fields omitted from the saved document format
// ============================================= // =============================================
@@ -200,6 +206,7 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
data_panel_open, data_panel_open,
layers_panel_open, layers_panel_open,
properties_panel_open, properties_panel_open,
resources,
} = context; } = context;
match message { match message {
@@ -916,15 +923,34 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
if path.is_some() { if path.is_some() {
responses.add(DocumentMessage::MarkAsSaved); responses.add(DocumentMessage::MarkAsSaved);
} }
let folder = self.path.as_ref().and_then(|path| path.parent()).map(|parent| parent.to_path_buf()); let folder = self.path.as_ref().and_then(|path| path.parent()).map(|parent| parent.to_path_buf());
responses.add(FrontendMessage::TriggerSaveDocument { let resource_hashes = Vec::from_iter(self.used_resources(false)).into_boxed_slice();
document_id, let resources = resources.resources();
name: format!("{}.{}", self.name.clone(), FILE_EXTENSION), let mut document = self.clone();
path, let name = format!("{}.{}", self.name.clone(), FILE_EXTENSION);
folder,
content: self.serialize_document().into_bytes().into(), responses.add(FrontendMessage::Await {
future: FrontendMessageFuture::new(async move {
let loads = resource_hashes
.into_iter()
.map(|hash| {
let resource = resources.load(hash);
async move { resource.await.map(|resource| (hash, resource)) }
})
.collect::<Vec<_>>();
document.embedded_resources = EmbeddedResources::from_iter(futures::future::join_all(loads).await.into_iter().flatten());
let content = document.serialize_document();
FrontendMessage::TriggerSaveDocument {
document_id,
name,
path,
folder,
content: content.into_bytes().into(),
}
}),
}); });
} }
DocumentMessage::SavedDocument { path } => { DocumentMessage::SavedDocument { path } => {
@@ -1306,11 +1332,10 @@ impl MessageHandler<DocumentMessage, DocumentMessageContext<'_>> for DocumentMes
let layer_text_frames = text_frames let layer_text_frames = text_frames
.into_iter() .into_iter()
.filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id)) .filter(|(node_id, _)| self.network_interface.document_network().nodes.contains_key(node_id))
.filter_map(|(node_id, frame)| { .filter(|&(node_id, _)| self.network_interface.is_layer(&node_id, &[]))
self.network_interface.is_layer(&node_id, &[]).then(|| { .map(|(node_id, frame)| {
let layer = LayerNodeIdentifier::new(node_id, &self.network_interface); let layer = LayerNodeIdentifier::new(node_id, &self.network_interface);
(layer, frame) (layer, frame)
})
}) })
.collect(); .collect();
self.network_interface.update_text_frames(layer_text_frames); self.network_interface.update_text_frames(layer_text_frames);
@@ -2546,6 +2571,7 @@ impl DocumentMessageHandler {
/// Helper method for NudgeSelectedLayers message. /// Helper method for NudgeSelectedLayers message.
/// Handles keyboard nudging of selected layers with optional resize mode. /// Handles keyboard nudging of selected layers with optional resize mode.
#[allow(clippy::too_many_arguments)]
fn handle_nudge_selected_layers( fn handle_nudge_selected_layers(
&mut self, &mut self,
delta_x: f64, delta_x: f64,
@@ -3444,6 +3470,16 @@ impl DocumentMessageHandler {
pub fn graph_view_overlay_open(&self) -> bool { pub fn graph_view_overlay_open(&self) -> bool {
self.graph_view_overlay_open self.graph_view_overlay_open
} }
pub fn used_resources(&self, include_history: bool) -> HashSet<ResourceHash> {
let mut resources = HashSet::new();
self.network_interface.collect_used_resources(&mut resources);
if include_history {
self.document_undo_history.iter().for_each(|interface| interface.collect_used_resources(&mut resources));
self.document_redo_history.iter().for_each(|interface| interface.collect_used_resources(&mut resources));
}
resources
}
} }
/// Create a network interface with a single export /// Create a network interface with a single export
@@ -298,13 +298,18 @@ impl<'a> ModifyInputsContext<'a> {
let transform = resolve_proto_node_type(graphene_std::transform_nodes::transform::IDENTIFIER) let transform = resolve_proto_node_type(graphene_std::transform_nodes::transform::IDENTIFIER)
.expect("Transform node does not exist") .expect("Transform node does not exist")
.default_node_template(); .default_node_template();
let png_bytes: std::sync::Arc<[u8]> = image.to_png().into();
let hash = graphene_std::application_io::ResourceHash::from(png_bytes.as_ref());
self.responses.add(ResourceMessage::Store { data: png_bytes });
let image_node = resolve_proto_node_type(graphene_std::raster_nodes::std_nodes::image::IDENTIFIER) let image_node = resolve_proto_node_type(graphene_std::raster_nodes::std_nodes::image::IDENTIFIER)
.expect("Image node does not exist") .expect("Image node does not exist")
.node_template_input_override([Some(NodeInput::value(TaggedValue::None, false)), Some(NodeInput::value(TaggedValue::ImageData(image), false))]); .node_template_input_override([Some(NodeInput::value(TaggedValue::Resource(hash), false))]);
let image_id = NodeId::new(); let image_node_id = NodeId::new();
self.network_interface.insert_node(image_id, image_node, &[]); self.network_interface.insert_node(image_node_id, image_node, &[]);
self.network_interface.move_node_to_chain_start(&image_id, layer, &[], self.import); self.network_interface.move_node_to_chain_start(&image_node_id, layer, &[], self.import);
let transform_id = NodeId::new(); let transform_id = NodeId::new();
self.network_interface.insert_node(transform_id, transform, &[]); self.network_interface.insert_node(transform_id, transform, &[]);
@@ -0,0 +1,92 @@
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use graph_craft::application_io::{Resource, ResourceHash};
use std::collections::HashMap;
use std::fmt;
#[derive(Clone, Default, Debug, PartialEq)]
pub struct EmbeddedResources {
resources: HashMap<ResourceHash, Resource>,
}
impl EmbeddedResources {
pub fn is_empty(&self) -> bool {
self.resources.is_empty()
}
pub fn store(&mut self, resource: Resource) -> ResourceHash {
let hash = ResourceHash::from(resource.as_ref());
self.resources.insert(hash, resource);
hash
}
}
impl FromIterator<(ResourceHash, Resource)> for EmbeddedResources {
fn from_iter<T: IntoIterator<Item = (ResourceHash, Resource)>>(iter: T) -> Self {
Self {
resources: iter.into_iter().collect(),
}
}
}
impl IntoIterator for EmbeddedResources {
type Item = (ResourceHash, Resource);
type IntoIter = std::collections::hash_map::IntoIter<ResourceHash, Resource>;
fn into_iter(self) -> Self::IntoIter {
self.resources.into_iter()
}
}
impl serde::Serialize for EmbeddedResources {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let human_readable = serializer.is_human_readable();
let mut map = serializer.serialize_map(Some(self.resources.len()))?;
for (hash, resource) in &self.resources {
let bytes: &[u8] = resource.as_ref();
if human_readable {
map.serialize_entry(hash, &BASE64.encode(bytes))?;
} else {
map.serialize_entry(hash, serde_bytes::Bytes::new(bytes))?;
}
}
map.end()
}
}
impl<'de> serde::Deserialize<'de> for EmbeddedResources {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct EmbeddedResourcesVisitor {
human_readable: bool,
}
impl<'de> serde::de::Visitor<'de> for EmbeddedResourcesVisitor {
type Value = EmbeddedResources;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a map of ResourceHash to resource bytes")
}
fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let mut resources = HashMap::with_capacity(map.size_hint().unwrap_or(0));
while let Some(hash) = map.next_key::<ResourceHash>()? {
let resource = if self.human_readable {
let encoded: String = map.next_value()?;
let bytes = BASE64.decode(&encoded).map_err(serde::de::Error::custom)?;
Resource::new(bytes)
} else {
let bytes: serde_bytes::ByteBuf = map.next_value()?;
Resource::new(bytes.into_vec())
};
resources.insert(hash, resource);
}
Ok(EmbeddedResources { resources })
}
}
let human_readable = deserializer.is_human_readable();
deserializer.deserialize_map(EmbeddedResourcesVisitor { human_readable })
}
}
@@ -1,5 +1,6 @@
pub mod clipboards; pub mod clipboards;
pub mod document_metadata; pub mod document_metadata;
pub mod embedded_resources;
pub mod error; pub mod error;
pub mod misc; pub mod misc;
pub mod network_interface; pub mod network_interface;
@@ -20,6 +20,7 @@ use crate::messages::tool::tool_messages::tool_prelude::NumberInputMode;
use deserialization::deserialize_node_persistent_metadata; use deserialization::deserialize_node_persistent_metadata;
use glam::{DAffine2, DVec2, IVec2}; use glam::{DAffine2, DVec2, IVec2};
use graph_craft::Type; use graph_craft::Type;
use graph_craft::application_io::ResourceHash;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork}; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput, NodeNetwork, OldDocumentNodeImplementation, OldNodeNetwork};
use graphene_std::ContextDependencies; use graphene_std::ContextDependencies;
@@ -510,6 +511,10 @@ impl NodeNetworkInterface {
}) })
} }
pub fn collect_used_resources(&self, target: &mut HashSet<ResourceHash>) {
collect_network_resources(self.document_network(), target);
}
pub fn frontend_imports(&mut self, network_path: &[NodeId]) -> Vec<Option<FrontendGraphOutput>> { pub fn frontend_imports(&mut self, network_path: &[NodeId]) -> Vec<Option<FrontendGraphOutput>> {
match network_path.split_last() { match network_path.split_last() {
Some((node_id, encapsulating_network_path)) => { Some((node_id, encapsulating_network_path)) => {
@@ -6769,6 +6774,21 @@ pub enum TransactionStatus {
Finished, Finished,
} }
fn collect_network_resources(network: &NodeNetwork, out: &mut HashSet<ResourceHash>) {
for node in network.nodes.values() {
for input in &node.inputs {
if let NodeInput::Value { tagged_value, .. } = input
&& let TaggedValue::Resource(hash) = &**tagged_value
{
out.insert(*hash);
}
}
if let DocumentNodeImplementation::Network(nested) = &node.implementation {
collect_network_resources(nested, out);
}
}
}
#[cfg(test)] #[cfg(test)]
mod network_interface_tests { mod network_interface_tests {
use crate::test_utils::test_prelude::*; use crate::test_utils::test_prelude::*;
@@ -1588,12 +1588,24 @@ fn migrate_node(node_id: &NodeId, node: &DocumentNode, network_path: &[NodeId],
document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[3].clone(), network_path); document.network_interface.set_input(&InputConnector::node(*node_id, 4), old_inputs[3].clone(), network_path);
} }
if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::image::IDENTIFIER) && inputs_count == 1 { // Upgrade `image` nodes that stored image data as `Image<Color>`` to `image` nodes that store a reference to the image data as a resource.
let mut node_template = resolve_document_node_type(&reference)?.default_node_template(); // Encodes the image data as PNG and stores it in the document's embedded resources and rewires the node to reference that resource.
document.network_interface.replace_implementation(node_id, network_path, &mut node_template); if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::image::IDENTIFIER) {
let image = node.inputs.iter().find_map(|input| match input.as_value()? {
TaggedValue::ImageData(image) => Some(image.clone()),
_ => None,
});
// Insert a new empty input for the image if let Some(image) = image {
document.network_interface.add_import(TaggedValue::None, false, 0, "Empty", "", &[*node_id]); let hash = document.embedded_resources.store(graphene_std::application_io::Resource::new(image.to_png()));
let mut node_template = resolve_document_node_type(&reference)?.default_node_template();
document.network_interface.replace_implementation(node_id, network_path, &mut node_template);
let _ = document.network_interface.replace_inputs(node_id, network_path, &mut node_template);
document
.network_interface
.set_input(&InputConnector::node(*node_id, 0), NodeInput::value(TaggedValue::Resource(hash), false), network_path);
}
} }
if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::noise_pattern::IDENTIFIER) && inputs_count == 15 { if reference == DefinitionIdentifier::ProtoNode(graphene_std::raster_nodes::std_nodes::noise_pattern::IDENTIFIER) && inputs_count == 15 {
@@ -50,6 +50,7 @@ pub enum PortfolioMessage {
}, },
DestroyAllDocuments, DestroyAllDocuments,
EditorPreferences, EditorPreferences,
GarbageCollectResources,
FontCatalogLoaded { FontCatalogLoaded {
catalog: FontCatalog, catalog: FontCatalog,
}, },
@@ -26,6 +26,7 @@ use crate::messages::tool::utility_types::{HintData, ToolType};
use crate::messages::viewport::ToPhysical; use crate::messages::viewport::ToPhysical;
use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor}; use crate::node_graph_executor::{ExportConfig, NodeGraphExecutor};
use glam::{DAffine2, DVec2}; use glam::{DAffine2, DVec2};
use graph_craft::application_io::ResourceHash;
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_std::Color; use graphene_std::Color;
use graphene_std::raster_types::Image; use graphene_std::raster_types::Image;
@@ -35,6 +36,7 @@ use graphene_std::text::Font;
use graphene_std::vector::misc::HandleId; use graphene_std::vector::misc::HandleId;
use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType}; use graphene_std::vector::{PointId, SegmentId, Vector, VectorModificationType};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc;
use std::vec; use std::vec;
#[derive(ExtractField)] #[derive(ExtractField)]
@@ -46,6 +48,7 @@ pub struct PortfolioMessageContext<'a> {
pub reset_node_definitions_on_open: bool, pub reset_node_definitions_on_open: bool,
pub timing_information: TimingInformation, pub timing_information: TimingInformation,
pub viewport: &'a ViewportMessageHandler, pub viewport: &'a ViewportMessageHandler,
pub resources: &'a ResourceMessageHandler,
} }
#[derive(Debug, Default, ExtractField)] #[derive(Debug, Default, ExtractField)]
@@ -74,6 +77,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
reset_node_definitions_on_open, reset_node_definitions_on_open,
timing_information, timing_information,
viewport, viewport,
resources,
} = context; } = context;
match message { match message {
@@ -90,6 +94,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
current_tool, current_tool,
preferences, preferences,
viewport, viewport,
resources,
data_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Data) && !self.workspace_panel_layout.focus_document, data_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Data) && !self.workspace_panel_layout.focus_document,
layers_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Layers) && !self.workspace_panel_layout.focus_document, layers_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Layers) && !self.workspace_panel_layout.focus_document,
properties_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Properties) && !self.workspace_panel_layout.focus_document, properties_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Properties) && !self.workspace_panel_layout.focus_document,
@@ -159,6 +164,7 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
current_tool, current_tool,
preferences, preferences,
viewport, viewport,
resources,
data_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Data) && !self.workspace_panel_layout.focus_document, data_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Data) && !self.workspace_panel_layout.focus_document,
layers_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Layers) && !self.workspace_panel_layout.focus_document, layers_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Layers) && !self.workspace_panel_layout.focus_document,
properties_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Properties) && !self.workspace_panel_layout.focus_document, properties_panel_open: self.workspace_panel_layout.is_panel_visible(PanelType::Properties) && !self.workspace_panel_layout.focus_document,
@@ -183,6 +189,8 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
responses.add(PortfolioMessage::AutoSaveDocument { document_id: *document_id }); responses.add(PortfolioMessage::AutoSaveDocument { document_id: *document_id });
} }
} }
responses.add(PortfolioMessage::GarbageCollectResources);
} }
PortfolioMessage::AutoSaveDocument { document_id } => { PortfolioMessage::AutoSaveDocument { document_id } => {
let Some(document) = self.document(document_id) else { return }; let Some(document) = self.document(document_id) else { return };
@@ -437,6 +445,23 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
} }
} }
PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()), PortfolioMessage::EditorPreferences => self.executor.update_editor_preferences(preferences.editor_preferences()),
PortfolioMessage::GarbageCollectResources => {
let mut used_resources = HashSet::new();
for (id, info) in self.unloaded_documents.iter() {
if let Some(resources) = &info.resources {
used_resources.extend(resources.iter());
} else {
responses.add(PersistentStateMessage::ReadDocument { document_id: *id });
return;
}
}
for document in self.documents.values() {
used_resources.extend(document.used_resources(true));
}
responses.add(ResourceMessage::GarbageCollect {
used: Vec::from_iter(used_resources).into_boxed_slice(),
});
}
PortfolioMessage::LoadDocumentResources { document_id } => { PortfolioMessage::LoadDocumentResources { document_id } => {
let catalog = &self.cached_data.font_catalog; let catalog = &self.cached_data.font_catalog;
@@ -758,6 +783,16 @@ impl MessageHandler<PortfolioMessage, PortfolioMessageContext<'_>> for Portfolio
// Upgrade the document's nodes to be compatible with the latest version // Upgrade the document's nodes to be compatible with the latest version
document_migration_upgrades(&mut document, reset_node_definitions_on_open); document_migration_upgrades(&mut document, reset_node_definitions_on_open);
// Load the document's embedded resources into the resource storage
std::mem::take(&mut document.embedded_resources).into_iter().for_each(|(hash, resource)| {
let data: Arc<[u8]> = Arc::from(resource.as_ref());
if ResourceHash::from(data.as_ref()) != hash {
log::error!("Resource hash mismatch for resource with hash {hash}");
return;
}
responses.add(ResourceMessage::Store { data });
});
// Ensure each node has the metadata for its inputs // Ensure each node has the metadata for its inputs
for (node_id, node, path) in document.network_interface.document_network().clone().recursive_nodes() { for (node_id, node, path) in document.network_interface.document_network().clone().recursive_nodes() {
document.network_interface.validate_input_metadata(node_id, node, &path); document.network_interface.validate_input_metadata(node_id, node, &path);
@@ -1869,6 +1904,7 @@ impl PortfolioMessageHandler {
name: document.name.clone(), name: document.name.clone(),
path: document.path.clone(), path: document.path.clone(),
is_saved: document.is_saved(), is_saved: document.is_saved(),
resources: Some(document.used_resources(false).into_iter().collect()),
}) })
} else { } else {
self.unloaded_documents.get(&document_id).cloned() self.unloaded_documents.get(&document_id).cloned()
+2 -1
View File
@@ -15,7 +15,7 @@ pub use crate::messages::dialog::export_dialog::{ExportDialogMessage, ExportDial
pub use crate::messages::dialog::new_document_dialog::{NewDocumentDialogMessage, NewDocumentDialogMessageDiscriminant, NewDocumentDialogMessageHandler}; pub use crate::messages::dialog::new_document_dialog::{NewDocumentDialogMessage, NewDocumentDialogMessageDiscriminant, NewDocumentDialogMessageHandler};
pub use crate::messages::dialog::preferences_dialog::{PreferencesDialogMessage, PreferencesDialogMessageContext, PreferencesDialogMessageDiscriminant, PreferencesDialogMessageHandler}; pub use crate::messages::dialog::preferences_dialog::{PreferencesDialogMessage, PreferencesDialogMessageContext, PreferencesDialogMessageDiscriminant, PreferencesDialogMessageHandler};
pub use crate::messages::dialog::{DialogMessage, DialogMessageContext, DialogMessageDiscriminant, DialogMessageHandler}; pub use crate::messages::dialog::{DialogMessage, DialogMessageContext, DialogMessageDiscriminant, DialogMessageHandler};
pub use crate::messages::frontend::{FrontendMessage, FrontendMessageDiscriminant}; pub use crate::messages::frontend::{FrontendMessage, FrontendMessageDiscriminant, FrontendMessageFuture};
pub use crate::messages::input_mapper::key_mapping::{KeyMappingMessage, KeyMappingMessageContext, KeyMappingMessageDiscriminant, KeyMappingMessageHandler}; pub use crate::messages::input_mapper::key_mapping::{KeyMappingMessage, KeyMappingMessageContext, KeyMappingMessageDiscriminant, KeyMappingMessageHandler};
pub use crate::messages::input_mapper::{InputMapperMessage, InputMapperMessageContext, InputMapperMessageDiscriminant, InputMapperMessageHandler}; pub use crate::messages::input_mapper::{InputMapperMessage, InputMapperMessageContext, InputMapperMessageDiscriminant, InputMapperMessageHandler};
pub use crate::messages::input_preprocessor::{InputPreprocessorMessage, InputPreprocessorMessageContext, InputPreprocessorMessageDiscriminant, InputPreprocessorMessageHandler}; pub use crate::messages::input_preprocessor::{InputPreprocessorMessage, InputPreprocessorMessageContext, InputPreprocessorMessageDiscriminant, InputPreprocessorMessageHandler};
@@ -31,6 +31,7 @@ pub use crate::messages::portfolio::document::{DocumentMessage, DocumentMessageC
pub use crate::messages::portfolio::persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageDiscriminant, PersistentStateMessageHandler}; pub use crate::messages::portfolio::persistent_state::{PersistentStateMessage, PersistentStateMessageContext, PersistentStateMessageDiscriminant, PersistentStateMessageHandler};
pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageContext, PortfolioMessageDiscriminant, PortfolioMessageHandler}; pub use crate::messages::portfolio::{PortfolioMessage, PortfolioMessageContext, PortfolioMessageDiscriminant, PortfolioMessageHandler};
pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler}; pub use crate::messages::preferences::{PreferencesMessage, PreferencesMessageDiscriminant, PreferencesMessageHandler};
pub use crate::messages::resource::{ResourceMessage, ResourceMessageContext, ResourceMessageDiscriminant, ResourceMessageHandler};
pub use crate::messages::tool::transform_layer::{TransformLayerMessage, TransformLayerMessageDiscriminant, TransformLayerMessageHandler}; pub use crate::messages::tool::transform_layer::{TransformLayerMessage, TransformLayerMessageDiscriminant, TransformLayerMessageHandler};
pub use crate::messages::tool::{ToolMessage, ToolMessageContext, ToolMessageDiscriminant, ToolMessageHandler}; pub use crate::messages::tool::{ToolMessage, ToolMessageContext, ToolMessageDiscriminant, ToolMessageHandler};
pub use crate::messages::viewport::{ViewportMessage, ViewportMessageDiscriminant, ViewportMessageHandler}; pub use crate::messages::viewport::{ViewportMessage, ViewportMessageDiscriminant, ViewportMessageHandler};
+7
View File
@@ -0,0 +1,7 @@
mod resource_message;
mod resource_message_handler;
#[doc(inline)]
pub use resource_message::{ResourceMessage, ResourceMessageDiscriminant};
#[doc(inline)]
pub use resource_message_handler::{ResourceMessageContext, ResourceMessageHandler, ResourcesHandle};
@@ -0,0 +1,10 @@
use crate::messages::prelude::*;
use graph_craft::application_io::ResourceHash;
use std::sync::Arc;
#[impl_message(Message, Resource)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize)]
pub enum ResourceMessage {
Store { data: Arc<[u8]> },
GarbageCollect { used: Box<[ResourceHash]> },
}
@@ -0,0 +1,79 @@
use crate::messages::prelude::*;
use graph_craft::application_io::{LoadResource, ResourceFuture, ResourceHash, ResourceStorage};
use std::sync::{Arc, RwLock};
#[derive(Clone)]
pub struct ResourcesHandle {
inner: Arc<RwLock<Box<dyn ResourceStorage>>>,
}
impl LoadResource for ResourcesHandle {
fn load(&self, hash: ResourceHash) -> ResourceFuture {
let guard = self.inner.read().unwrap();
guard.load(hash)
}
}
#[derive(ExtractField)]
pub struct ResourceMessageHandler {
storage: Option<Arc<RwLock<Box<dyn ResourceStorage>>>>,
}
impl ResourceMessageHandler {
pub fn new(resource_storage: Box<dyn ResourceStorage>) -> Self {
Self {
storage: Some(Arc::new(RwLock::new(resource_storage))),
}
}
pub fn resources(&self) -> Box<dyn LoadResource> {
Box::new(ResourcesHandle {
inner: self.storage.clone().expect("Resource storage not initialized"),
})
}
}
impl std::fmt::Debug for ResourceMessageHandler {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ResourceMessageHandler").finish_non_exhaustive()
}
}
impl Default for ResourceMessageHandler {
#[cfg(not(test))]
fn default() -> Self {
Self { storage: None }
}
#[cfg(test)]
fn default() -> Self {
Self {
storage: Some(Arc::new(RwLock::new(Box::new(graph_craft::application_io::HashMapResourceStorage::new())))),
}
}
}
#[derive(ExtractField)]
pub struct ResourceMessageContext {}
#[message_handler_data]
impl MessageHandler<ResourceMessage, ResourceMessageContext> for ResourceMessageHandler {
fn process_message(&mut self, message: ResourceMessage, _responses: &mut VecDeque<Message>, _context: ResourceMessageContext) {
let Some(storage) = &self.storage else {
log::error!("Received resource message but storage is not initialized");
return;
};
let mut storage = storage.write().unwrap();
match message {
ResourceMessage::Store { data } => {
let _hash = storage.store(data.as_ref());
}
ResourceMessage::GarbageCollect { used } => {
storage.garbage_collect(&used);
}
}
}
advertise_actions!(ResourceMessageDiscriminant;);
}
+18 -21
View File
@@ -97,7 +97,7 @@ pub struct ExportConfig {
struct InternalNodeGraphUpdateSender(Sender<NodeGraphUpdate>); struct InternalNodeGraphUpdateSender(Sender<NodeGraphUpdate>);
impl InternalNodeGraphUpdateSender { impl InternalNodeGraphUpdateSender {
fn send_generation_response(&self, response: CompilationResponse) { fn send_compilation_response(&self, response: CompilationResponse) {
self.0.send(NodeGraphUpdate::CompilationResponse(response)).expect("Failed to send response") self.0.send(NodeGraphUpdate::CompilationResponse(response)).expect("Failed to send response")
} }
@@ -134,7 +134,11 @@ impl NodeRuntime {
editor_preferences: Box::new(EditorPreferences::default()), editor_preferences: Box::new(EditorPreferences::default()),
node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)), node_graph_message_sender: Box::new(InternalNodeGraphUpdateSender(sender)),
#[cfg(not(test))]
application_io: None, application_io: None,
#[cfg(test)]
application_io: Some(PlatformApplicationIo::default().into()),
} }
.into(), .into(),
@@ -154,19 +158,6 @@ impl NodeRuntime {
} }
pub async fn run(&mut self) -> Option<ImageTexture> { pub async fn run(&mut self) -> Option<ImageTexture> {
if self.editor_api.application_io.is_none() {
self.editor_api = PlatformEditorApi {
#[cfg(all(not(test), target_family = "wasm"))]
application_io: Some(PlatformApplicationIo::new().await.into()),
#[cfg(any(test, not(target_family = "wasm")))]
application_io: Some(PlatformApplicationIo::new().await.into()),
font_cache: self.editor_api.font_cache.clone(),
node_graph_message_sender: Box::new(self.sender.clone()),
editor_preferences: Box::new(self.editor_preferences.clone()),
}
.into();
}
let mut font = None; let mut font = None;
let mut preferences = None; let mut preferences = None;
let mut graph = None; let mut graph = None;
@@ -247,7 +238,7 @@ impl NodeRuntime {
self.update_thumbnails = true; self.update_thumbnails = true;
self.sender.send_generation_response(CompilationResponse { result, node_graph_errors }); self.sender.send_compilation_response(CompilationResponse { result, node_graph_errors });
} }
GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, mut render_config, .. }) => { GraphRuntimeRequest::ExecutionRequest(ExecutionRequest { execution_id, mut render_config, .. }) => {
// We may want to render via the SVG pipeline even though raster was requested, if SVG Preview render mode is active or WebGPU/Vello is unavailable // We may want to render via the SVG pipeline even though raster was requested, if SVG Preview render mode is active or WebGPU/Vello is unavailable
@@ -571,18 +562,24 @@ pub async fn run_node_graph() -> (bool, Option<ImageTexture>) {
(false, None) (false, None)
} }
pub async fn replace_node_runtime(runtime: NodeRuntime) -> Option<NodeRuntime> { pub fn replace_node_runtime(runtime: NodeRuntime) -> Option<NodeRuntime> {
let mut node_runtime = NODE_RUNTIME.lock(); let mut node_runtime = NODE_RUNTIME.lock();
node_runtime.replace(runtime) node_runtime.replace(runtime)
} }
pub async fn replace_application_io(application_io: PlatformApplicationIo) { pub(crate) fn replace_application_io(application_io: PlatformApplicationIo) {
let mut node_runtime = NODE_RUNTIME.lock(); let mut node_runtime = NODE_RUNTIME.lock();
if let Some(node_runtime) = &mut *node_runtime { if let Some(node_runtime) = &mut *node_runtime {
node_runtime.editor_api = PlatformEditorApi { node_runtime.replace_application_io(application_io);
font_cache: node_runtime.editor_api.font_cache.clone(), }
}
impl NodeRuntime {
pub(crate) fn replace_application_io(&mut self, application_io: PlatformApplicationIo) {
self.editor_api = PlatformEditorApi {
font_cache: self.editor_api.font_cache.clone(),
application_io: Some(application_io.into()), application_io: Some(application_io.into()),
node_graph_message_sender: Box::new(node_runtime.sender.clone()), node_graph_message_sender: Box::new(self.sender.clone()),
editor_preferences: Box::new(node_runtime.editor_preferences.clone()), editor_preferences: Box::new(self.editor_preferences.clone()),
} }
.into(); .into();
} }
+1 -1
View File
@@ -20,7 +20,7 @@ impl NodeRuntimeIO {
pub fn new() -> Self { pub fn new() -> Self {
let (response_sender, response_receiver) = std::sync::mpsc::channel(); let (response_sender, response_receiver) = std::sync::mpsc::channel();
let (request_sender, request_receiver) = std::sync::mpsc::channel(); let (request_sender, request_receiver) = std::sync::mpsc::channel();
futures::executor::block_on(replace_node_runtime(NodeRuntime::new(request_receiver, response_sender))); replace_node_runtime(NodeRuntime::new(request_receiver, response_sender));
Self { Self {
sender: request_sender, sender: request_sender,
+1 -1
View File
@@ -23,7 +23,7 @@
// Create the editor and subscriptions router // Create the editor and subscriptions router
const randomSeed = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)); const randomSeed = BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER));
subscriptions = createSubscriptionsRouter(); subscriptions = createSubscriptionsRouter();
editor = EditorWrapper.create(operatingSystem(), randomSeed, (messageType: MessageName, messageData: FrontendMessage) => { editor = await EditorWrapper.create(operatingSystem(), randomSeed, (messageType: MessageName, messageData: FrontendMessage) => {
subscriptions?.handleFrontendMessage(messageType, messageData); subscriptions?.handleFrontendMessage(messageType, messageData);
}); });
+33 -15
View File
@@ -11,6 +11,7 @@ use crate::helpers::poll_node_graph_evaluation;
use crate::helpers::{auto_save_all_documents, calculate_hash, render_image_data_to_canvases, request_animation_frame, set_timeout, translate_key, wrapper}; use crate::helpers::{auto_save_all_documents, calculate_hash, render_image_data_to_canvases, request_animation_frame, set_timeout, translate_key, wrapper};
use crate::{EDITOR_HAS_CRASHED, EDITOR_WRAPPER, Error, FRONTEND_READY, MESSAGE_BUFFER, PANIC_DIALOG_MESSAGE_CALLBACK}; use crate::{EDITOR_HAS_CRASHED, EDITOR_WRAPPER, Error, FRONTEND_READY, MESSAGE_BUFFER, PANIC_DIALOG_MESSAGE_CALLBACK};
#[cfg(not(feature = "native"))] #[cfg(not(feature = "native"))]
#[cfg(all(not(feature = "native"), target_family = "wasm"))]
use editor::application::{Editor, Environment, Host, Platform}; use editor::application::{Editor, Environment, Host, Platform};
use editor::consts::FILE_EXTENSION; use editor::consts::FILE_EXTENSION;
use editor::messages::clipboard::utility_types::ClipboardContentRaw; use editor::messages::clipboard::utility_types::ClipboardContentRaw;
@@ -22,6 +23,8 @@ use editor::messages::portfolio::document::utility_types::network_interface::Imp
use editor::messages::portfolio::utility_types::{DockingSplitDirection, FontCatalog, FontCatalogFamily, PanelGroupId, PanelType}; use editor::messages::portfolio::utility_types::{DockingSplitDirection, FontCatalog, FontCatalogFamily, PanelGroupId, PanelType};
use editor::messages::prelude::*; use editor::messages::prelude::*;
use editor::messages::tool::tool_messages::tool_prelude::WidgetId; use editor::messages::tool::tool_messages::tool_prelude::WidgetId;
#[cfg(all(not(feature = "native"), target_family = "wasm"))]
use graph_craft::application_io::OpfsResourceStorage;
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_std::color::SRGBA8; use graphene_std::color::SRGBA8;
use graphene_std::graphene_hash::CacheHashWrapper; use graphene_std::graphene_hash::CacheHashWrapper;
@@ -69,22 +72,29 @@ impl EditorWrapper {
// Editor wrapper machinery // Editor wrapper machinery
// ======================== // ========================
#[cfg(not(feature = "native"))] #[cfg(all(not(feature = "native"), target_family = "wasm"))]
pub fn create(platform: String, uuid_random_seed: u64, frontend_message_handler_callback: js_sys::Function) -> EditorWrapper { pub async fn create(platform: String, uuid_random_seed: u64, frontend_message_handler_callback: js_sys::Function) -> EditorWrapper {
let editor = Editor::new( use graph_craft::application_io::PlatformApplicationIo;
Environment {
platform: Platform::Web,
host: match platform.as_str() {
"Linux" => Host::Linux,
"Mac" => Host::Mac,
"Windows" => Host::Windows,
_ => unreachable!(),
},
},
uuid_random_seed,
);
if EDITOR.with(|wrapper| wrapper.lock().ok().map(|mut guard| *guard = Some(editor))).is_none() { let host = match platform.as_str() {
"Linux" => Host::Linux,
"Mac" => Host::Mac,
"Windows" => Host::Windows,
_ => unreachable!(),
};
let storage: Box<dyn graph_craft::application_io::ResourceStorage> = match OpfsResourceStorage::load("resources").await {
Ok(storage) => Box::new(storage),
Err(error) => {
log::error!("Failed to open OPFS resource storage, falling back to in-memory: {error:?}");
Box::new(graph_craft::application_io::HashMapResourceStorage::new())
}
};
let mut editor = Editor::new(Environment { platform: Platform::Web, host }, uuid_random_seed, storage);
editor.replace_application_io(PlatformApplicationIo::new().await);
if EDITOR.with(|slot| slot.lock().ok().map(|mut guard| *guard = Some(editor))).is_none() {
log::error!("Attempted to initialize the editor more than once"); log::error!("Attempted to initialize the editor more than once");
} }
@@ -133,6 +143,14 @@ impl EditorWrapper {
// Sends a FrontendMessage to JavaScript // Sends a FrontendMessage to JavaScript
pub(crate) fn send_frontend_message_to_js(&self, message: FrontendMessage) { pub(crate) fn send_frontend_message_to_js(&self, message: FrontendMessage) {
if let FrontendMessage::Await { future } = message {
let wrapper = self.clone();
wasm_bindgen_futures::spawn_local(async move {
wrapper.send_frontend_message_to_js(future.await);
});
return;
}
if let FrontendMessage::UpdateImageData { ref image_data } = message { if let FrontendMessage::UpdateImageData { ref image_data } = message {
let new_hash = calculate_hash(&CacheHashWrapper(image_data)); let new_hash = calculate_hash(&CacheHashWrapper(image_data));
let prev_hash = IMAGE_DATA_HASH.load(Ordering::Relaxed); let prev_hash = IMAGE_DATA_HASH.load(Ordering::Relaxed);
+15 -1
View File
@@ -50,12 +50,26 @@ wasm-bindgen = { workspace = true, optional = true }
# Workspace dependencies # Workspace dependencies
[target.'cfg(target_family = "wasm")'.dependencies] [target.'cfg(target_family = "wasm")'.dependencies]
web-sys = { workspace = true, features = ["Navigator", "Gpu"] } web-sys = { workspace = true, features = [
"Navigator",
"DomException",
"Window",
"StorageManager",
"FileSystemDirectoryHandle",
"FileSystemFileHandle",
"FileSystemGetFileOptions",
"FileSystemGetDirectoryOptions",
"FileSystemWritableFileStream",
"WritableStream",
"Blob",
] }
js-sys = { workspace = true } js-sys = { workspace = true }
wasm-bindgen = { workspace = true } wasm-bindgen = { workspace = true }
wasm-bindgen-futures = { workspace = true }
[target.'cfg(not(target_family = "wasm"))'.dependencies] [target.'cfg(not(target_family = "wasm"))'.dependencies]
winit = { workspace = true } winit = { workspace = true }
mmap-io = { workspace = true }
[dev-dependencies] [dev-dependencies]
# Workspace dependencies # Workspace dependencies
+76 -6
View File
@@ -1,14 +1,84 @@
use dyn_any::StaticType; use dyn_any::StaticType;
#[cfg(feature = "wgpu")]
use wgpu_executor::WgpuExecutor;
#[cfg(not(target_family = "wasm"))] pub mod resource;
mod native;
#[cfg(target_family = "wasm")]
mod wasm;
pub use graphene_application_io::{ApplicationIo, LoadResource, Resource, ResourceFuture, ResourceHash, ResourceStorage};
pub use resource::HashMapResourceStorage;
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
pub type PlatformApplicationIo = native::NativeApplicationIo; pub use resource::mmap::MmapResourceStorage;
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
pub type PlatformApplicationIo = wasm::WasmApplicationIo; pub use resource::opfs::OpfsResourceStorage;
#[derive(Default)]
pub struct PlatformApplicationIo {
#[cfg(feature = "wgpu")]
pub(crate) gpu_executor: Option<WgpuExecutor>,
resources: Option<Box<dyn LoadResource>>,
}
impl PlatformApplicationIo {
pub async fn new() -> Self {
#[cfg(feature = "wgpu")]
let executor = WgpuExecutor::new().await;
#[cfg(not(feature = "wgpu"))]
let wgpu_available = false;
#[cfg(feature = "wgpu")]
let wgpu_available = executor.is_some();
set_wgpu_available(wgpu_available);
Self {
#[cfg(feature = "wgpu")]
gpu_executor: executor,
resources: None,
}
}
#[cfg(feature = "wgpu")]
pub fn new_with_context(context: wgpu_executor::WgpuContext) -> Self {
let executor = WgpuExecutor::with_context(context);
let wgpu_available = executor.is_some();
set_wgpu_available(wgpu_available);
Self {
gpu_executor: executor,
resources: None,
}
}
pub fn inject_resource_proxy(&mut self, resources: Box<dyn LoadResource>) {
self.resources = Some(resources);
}
}
impl ApplicationIo for PlatformApplicationIo {
#[cfg(feature = "wgpu")]
type Executor = WgpuExecutor;
#[cfg(not(feature = "wgpu"))]
type Executor = ();
#[cfg(feature = "wgpu")]
fn gpu_executor(&self) -> Option<&Self::Executor> {
self.gpu_executor.as_ref()
}
fn load_resource(&self, hash: ResourceHash) -> graphene_application_io::ResourceFuture {
self.resources.as_ref().expect("Resource storage not initialized").load(hash)
}
}
impl std::fmt::Debug for PlatformApplicationIo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PlatformApplicationIo").finish_non_exhaustive()
}
}
unsafe impl StaticType for PlatformApplicationIo {
type Static = PlatformApplicationIo;
}
pub type PlatformEditorApi = graphene_application_io::EditorApi<PlatformApplicationIo>; pub type PlatformEditorApi = graphene_application_io::EditorApi<PlatformApplicationIo>;
@@ -1,113 +0,0 @@
use dyn_any::StaticType;
use graphene_application_io::{ApplicationError, ApplicationIo, ResourceFuture};
use std::collections::HashMap;
use std::sync::Arc;
#[cfg(feature = "tokio")]
use tokio::io::AsyncReadExt;
#[cfg(target_family = "wasm")]
use wasm_bindgen::JsCast;
#[cfg(feature = "wgpu")]
use wgpu_executor::WgpuExecutor;
#[derive(Debug, Default)]
pub struct NativeApplicationIo {
#[cfg(feature = "wgpu")]
pub(crate) gpu_executor: Option<WgpuExecutor>,
pub resources: HashMap<String, Arc<[u8]>>,
}
impl NativeApplicationIo {
pub async fn new() -> Self {
#[cfg(feature = "wgpu")]
let executor = WgpuExecutor::new().await;
#[cfg(not(feature = "wgpu"))]
let wgpu_available = false;
#[cfg(feature = "wgpu")]
let wgpu_available = executor.is_some();
super::set_wgpu_available(wgpu_available);
let mut io = Self {
#[cfg(feature = "wgpu")]
gpu_executor: executor,
resources: HashMap::new(),
};
io.resources.insert("null".to_string(), Arc::from(include_bytes!("../null.png").to_vec()));
io
}
#[cfg(feature = "wgpu")]
pub fn new_with_context(context: wgpu_executor::WgpuContext) -> Self {
#[cfg(feature = "wgpu")]
let executor = WgpuExecutor::with_context(context);
#[cfg(not(feature = "wgpu"))]
let wgpu_available = false;
#[cfg(feature = "wgpu")]
let wgpu_available = executor.is_some();
super::set_wgpu_available(wgpu_available);
let mut io = Self {
gpu_executor: executor,
resources: HashMap::new(),
};
io.resources.insert("null".to_string(), Arc::from(include_bytes!("../null.png").to_vec()));
io
}
}
impl ApplicationIo for NativeApplicationIo {
#[cfg(feature = "wgpu")]
type Executor = WgpuExecutor;
#[cfg(not(feature = "wgpu"))]
type Executor = ();
#[cfg(feature = "wgpu")]
fn gpu_executor(&self) -> Option<&Self::Executor> {
self.gpu_executor.as_ref()
}
fn load_resource(&self, url: impl AsRef<str>) -> Result<ResourceFuture, ApplicationError> {
let url = url::Url::parse(url.as_ref()).map_err(|_| ApplicationError::InvalidUrl)?;
log::trace!("Loading resource: {url:?}");
match url.scheme() {
#[cfg(feature = "tokio")]
"file" => {
let path = url.to_file_path().map_err(|_| ApplicationError::NotFound)?;
let path = path.to_str().ok_or(ApplicationError::NotFound)?;
let path = path.to_owned();
Ok(Box::pin(async move {
let file = tokio::fs::File::open(path).await.map_err(|_| ApplicationError::NotFound)?;
let mut reader = tokio::io::BufReader::new(file);
let mut data = Vec::new();
reader.read_to_end(&mut data).await.map_err(|_| ApplicationError::NotFound)?;
Ok(Arc::from(data))
}) as ResourceFuture)
}
"http" | "https" => {
let url = url.to_string();
Ok(Box::pin(async move {
let client = reqwest::Client::new();
let response = client.get(url).send().await.map_err(|_| ApplicationError::NotFound)?;
let data = response.bytes().await.map_err(|_| ApplicationError::NotFound)?;
Ok(Arc::from(data.to_vec()))
}) as ResourceFuture)
}
"graphite" => {
let path = url.path();
let path = path.to_owned();
log::trace!("Loading local resource: {path}");
let data = self.resources.get(&path).ok_or(ApplicationError::NotFound)?.clone();
Ok(Box::pin(async move { Ok(data.clone()) }) as ResourceFuture)
}
_ => Err(ApplicationError::NotFound),
}
}
}
unsafe impl StaticType for NativeApplicationIo {
type Static = NativeApplicationIo;
}
@@ -0,0 +1,43 @@
#[cfg(not(target_family = "wasm"))]
pub mod mmap;
#[cfg(target_family = "wasm")]
pub mod opfs;
use graphene_application_io::{LoadResource, Resource, ResourceFuture, ResourceHash, ResourceStorage};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[derive(Debug, Default)]
pub struct HashMapResourceStorage {
resources: Mutex<HashMap<ResourceHash, Resource>>,
}
impl HashMapResourceStorage {
pub fn new() -> Self {
Self::default()
}
}
impl LoadResource for HashMapResourceStorage {
fn load(&self, hash: ResourceHash) -> ResourceFuture {
let result = self.resources.lock().unwrap().get(&hash).cloned();
Box::pin(async move { result })
}
}
impl ResourceStorage for HashMapResourceStorage {
fn store(&mut self, data: &[u8]) -> ResourceHash {
let hash = ResourceHash::from(data);
self.resources.get_mut().unwrap().insert(hash, Resource::new(Arc::<[u8]>::from(data)));
hash
}
fn contains(&mut self, hash: &ResourceHash) -> bool {
self.resources.get_mut().unwrap().contains_key(hash)
}
fn garbage_collect(&mut self, used: &[ResourceHash]) {
let used_set: std::collections::HashSet<&ResourceHash> = used.iter().collect();
self.resources.get_mut().unwrap().retain(|hash, _| used_set.contains(hash));
}
}
@@ -0,0 +1,160 @@
use graphene_application_io::{LoadResource, Resource, ResourceFuture, ResourceHash, ResourceStorage};
use mmap_io::mmap::{MemoryMappedFile, MmapMode};
use std::collections::HashMap;
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::RwLock;
pub struct MmapResourceStorage {
root: PathBuf,
cache: RwLock<HashMap<ResourceHash, Resource>>,
}
impl MmapResourceStorage {
pub fn new(root: impl Into<PathBuf>) -> std::io::Result<Self> {
let root = root.into();
fs::create_dir_all(&root)?;
Ok(Self {
root,
cache: RwLock::new(HashMap::new()),
})
}
fn path_for(&self, hash: &ResourceHash) -> PathBuf {
let hash = String::from(hash);
let mut path = self.root.clone();
path.push(&hash[..2]);
path.push(&hash[2..]);
path
}
fn open_mmap(path: &Path) -> Option<MemoryMappedFile> {
match MemoryMappedFile::builder(path).mode(MmapMode::ReadOnly).huge_pages(true).open() {
Ok(file) => Some(file),
Err(error) => {
log::warn!("Failed to mmap {path:?} retrying without huge pages: {error}");
match MemoryMappedFile::open_ro(path) {
Ok(file) => Some(file),
Err(error) => {
log::error!("Failed to mmap {path:?}: {error}");
None
}
}
}
}
}
fn lookup(&self, hash: &ResourceHash) -> Option<Resource> {
if let Some(resource) = self.cache.read().unwrap_or_else(|poisoned| poisoned.into_inner()).get(hash) {
return Some(resource.clone());
}
let path = self.path_for(hash);
let mmap = Self::open_mmap(&path)?;
let resource = Resource::new(MmappedBytes(mmap));
self.cache.write().unwrap_or_else(|poisoned| poisoned.into_inner()).insert(*hash, resource.clone());
Some(resource)
}
}
impl LoadResource for MmapResourceStorage {
fn load(&self, hash: ResourceHash) -> ResourceFuture {
let result = self.lookup(&hash);
Box::pin(async move { result })
}
}
impl ResourceStorage for MmapResourceStorage {
fn store(&mut self, data: &[u8]) -> ResourceHash {
let hash = ResourceHash::from(data);
let path = self.path_for(&hash);
if path.exists() {
return hash;
}
let Some(parent) = path.parent() else {
log::error!("Resource path {path:?} has no parent directory");
return hash;
};
if let Err(error) = fs::create_dir_all(parent) {
log::error!("Failed to create resource subdirectory {parent:?}: {error}");
return hash;
}
let tmp = parent.join(format!(
"tmp.{}.{}.{}",
path.file_name().and_then(|n| n.to_str()).unwrap_or(""),
std::process::id(),
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0),
));
let write_result = (|| -> std::io::Result<()> {
let mut file = fs::OpenOptions::new().write(true).create_new(true).open(&tmp)?;
file.write_all(data)?;
file.sync_all()?;
fs::rename(&tmp, &path)
})();
if let Err(error) = write_result {
let _ = fs::remove_file(&tmp);
if !path.exists() {
log::error!("Failed to write resource to {path:?}: {error}");
}
}
hash
}
fn contains(&mut self, hash: &ResourceHash) -> bool {
self.cache.get_mut().unwrap_or_else(|poisoned| poisoned.into_inner()).contains_key(hash) || self.path_for(hash).exists()
}
fn garbage_collect(&mut self, used: &[ResourceHash]) {
let used_set: std::collections::HashSet<ResourceHash> = used.iter().cloned().collect();
self.cache.get_mut().unwrap_or_else(|poisoned| poisoned.into_inner()).retain(|hash, _| used_set.contains(hash));
let Ok(top_entries) = fs::read_dir(&self.root) else { return };
for top_entry in top_entries.flatten() {
let top_path = top_entry.path();
if !top_path.is_dir() {
continue;
}
let Ok(entries) = fs::read_dir(&top_path) else { continue };
for entry in entries.flatten() {
let path = entry.path();
let Some(prefix) = top_path.file_name().and_then(|n| n.to_str()) else { continue };
let Some(suffix) = path.file_name().and_then(|n| n.to_str()) else { continue };
if suffix.starts_with("tmp.") {
continue;
}
let hex = format!("{prefix}{suffix}");
let Ok(hash) = ResourceHash::try_from(hex.as_str()) else { continue };
if !used_set.contains(&hash)
&& let Err(error) = fs::remove_file(&path)
{
log::error!("Failed to remove unused resource {path:?}: {error}");
}
}
let _ = fs::remove_dir(&top_path);
}
}
}
struct MmappedBytes(MemoryMappedFile);
impl AsRef<[u8]> for MmappedBytes {
fn as_ref(&self) -> &[u8] {
let len = self.0.len();
match self.0.as_slice(0, len) {
Ok(slice) => slice,
Err(error) => {
log::error!("Failed to obtain mmap slice: {error}");
&[]
}
}
}
}
@@ -0,0 +1,366 @@
use graphene_application_io::{LoadResource, Resource, ResourceFuture, ResourceHash, ResourceStorage};
use js_sys::Uint8Array;
use std::collections::{HashMap, HashSet, VecDeque};
use std::pin::Pin;
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll, Waker};
use wasm_bindgen::JsCast;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::{JsFuture, spawn_local};
use web_sys::{Blob, DomException, FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemGetDirectoryOptions, FileSystemGetFileOptions, FileSystemWritableFileStream, WritableStream};
enum Mutation {
Write { hash: ResourceHash, bytes: Arc<[u8]> },
Delete { hash: ResourceHash },
}
struct Inner {
directory: FileSystemDirectoryHandle,
cache: HashMap<ResourceHash, Resource>,
on_disk: HashSet<ResourceHash>,
queue: VecDeque<Mutation>,
worker_active: bool,
persist_requested: bool,
}
pub struct OpfsResourceStorage {
inner: Arc<Mutex<Inner>>,
}
// SAFETY: This is only compiled for browser wasm, where JS handles remain on the main thread.
unsafe impl Send for OpfsResourceStorage {}
unsafe impl Sync for OpfsResourceStorage {}
impl OpfsResourceStorage {
pub async fn load(directory_name: &str) -> Result<Self, JsValue> {
let directory = open_resource_directory(directory_name).await?;
let on_disk = enumerate_hashes(&directory).await?;
Ok(Self {
inner: Arc::new(Mutex::new(Inner {
directory,
cache: HashMap::new(),
on_disk,
queue: VecDeque::new(),
worker_active: false,
persist_requested: false,
})),
})
}
}
impl LoadResource for OpfsResourceStorage {
fn load(&self, hash: ResourceHash) -> ResourceFuture {
let inner = self.inner.clone();
{
let guard = inner.lock().unwrap();
if let Some(resource) = guard.cache.get(&hash) {
let resource = resource.clone();
return Box::pin(async move { Some(resource) });
}
if !guard.on_disk.contains(&hash) {
return Box::pin(async move { None });
}
}
let (sender, receiver) = oneshot();
spawn_local(async move {
sender.send(read_from_opfs(inner, hash).await);
});
Box::pin(receiver)
}
}
impl ResourceStorage for OpfsResourceStorage {
fn store(&mut self, data: &[u8]) -> ResourceHash {
let hash = ResourceHash::from(data);
let mut guard = self.inner.lock().unwrap();
let mut bytes = None;
if !guard.cache.contains_key(&hash) {
let resource_bytes = Arc::<[u8]>::from(data);
guard.cache.insert(hash, Resource::new(resource_bytes.clone()));
bytes = Some(resource_bytes);
}
if !guard.on_disk.contains(&hash) {
let bytes = bytes.unwrap_or_else(|| Arc::<[u8]>::from(data));
guard.on_disk.insert(hash);
guard.queue.push_back(Mutation::Write { hash, bytes });
kick_worker(&self.inner, &mut guard);
}
hash
}
fn contains(&mut self, hash: &ResourceHash) -> bool {
let guard = self.inner.lock().unwrap();
guard.cache.contains_key(hash) || guard.on_disk.contains(hash)
}
fn garbage_collect(&mut self, used: &[ResourceHash]) {
let used: HashSet<ResourceHash> = used.iter().copied().collect();
let mut guard = self.inner.lock().unwrap();
guard.cache.retain(|hash, _| used.contains(hash));
let unused: Vec<ResourceHash> = guard.on_disk.iter().copied().filter(|hash| !used.contains(hash)).collect();
for hash in unused {
guard.on_disk.remove(&hash);
guard.queue.push_back(Mutation::Delete { hash });
}
if !guard.queue.is_empty() {
kick_worker(&self.inner, &mut guard);
}
}
}
fn kick_worker(inner: &Arc<Mutex<Inner>>, guard: &mut Inner) {
if guard.worker_active {
return;
}
guard.worker_active = true;
let inner = inner.clone();
spawn_local(drain_queue(inner));
}
async fn drain_queue(inner: Arc<Mutex<Inner>>) {
loop {
let (directory, mutation, persist_requested) = {
let mut guard = inner.lock().unwrap();
let Some(mutation) = guard.queue.pop_front() else {
guard.worker_active = false;
return;
};
let persist_requested = matches!(mutation, Mutation::Write { .. }) && !guard.persist_requested;
if persist_requested {
guard.persist_requested = true;
}
(guard.directory.clone(), mutation, persist_requested)
};
if persist_requested {
request_persistence().await;
}
match mutation {
Mutation::Write { hash, bytes } => {
if let Err(error) = write_file(&directory, &hash, &bytes).await {
log::error!("OPFS write for {hash} failed: {error:?}");
}
}
Mutation::Delete { hash } => {
if let Err(error) = delete_file(&directory, &hash).await {
log::error!("OPFS delete for {hash} failed: {error:?}");
}
}
}
}
}
async fn request_persistence() {
let Some(window) = web_sys::window() else {
log::warn!("OPFS persist() skipped: no window");
return;
};
let storage = window.navigator().storage();
match storage.persist() {
Ok(promise) => match JsFuture::from(promise).await {
Ok(value) if value.as_bool() == Some(true) => {}
Ok(_) => log::warn!("OPFS persistence was not granted; browser may evict resources under storage pressure"),
Err(error) => log::warn!("OPFS persist() rejected: {error:?}"),
},
Err(error) => log::warn!("OPFS persist() threw: {error:?}"),
}
}
async fn open_resource_directory(directory_name: &str) -> Result<FileSystemDirectoryHandle, JsValue> {
let storage = web_sys::window().ok_or_else(|| JsValue::from_str("no window"))?.navigator().storage();
let root: FileSystemDirectoryHandle = JsFuture::from(storage.get_directory()).await?.dyn_into()?;
let options = FileSystemGetDirectoryOptions::new();
options.set_create(true);
JsFuture::from(root.get_directory_handle_with_options(directory_name, &options)).await?.dyn_into()
}
async fn enumerate_hashes(directory: &FileSystemDirectoryHandle) -> Result<HashSet<ResourceHash>, JsValue> {
let iterator = directory.keys();
let mut hashes = HashSet::new();
loop {
let next: js_sys::IteratorNext = JsFuture::from(iterator.next()?).await?.unchecked_into();
if next.done() {
break;
}
let Some(name) = next.value().as_string() else {
log::warn!("Skipping non-string OPFS resource entry");
continue;
};
match ResourceHash::try_from(name.as_str()) {
Ok(hash) => {
hashes.insert(hash);
}
Err(error) => log::warn!("Skipping non-resource OPFS entry {name:?}: {error}"),
}
}
Ok(hashes)
}
async fn write_file(directory: &FileSystemDirectoryHandle, hash: &ResourceHash, bytes: &[u8]) -> Result<(), JsValue> {
let options = FileSystemGetFileOptions::new();
options.set_create(true);
let name = file_name(hash);
let handle: FileSystemFileHandle = JsFuture::from(directory.get_file_handle_with_options(&name, &options)).await?.dyn_into()?;
let writable: FileSystemWritableFileStream = JsFuture::from(handle.create_writable()).await?.dyn_into()?;
let stream: WritableStream = writable.clone().unchecked_into();
let bytes = Uint8Array::from(bytes);
if let Err(error) = JsFuture::from(writable.write_with_js_u8_array(&bytes)?).await {
let _ = JsFuture::from(stream.abort()).await;
return Err(error);
}
JsFuture::from(stream.close()).await?;
Ok(())
}
async fn delete_file(directory: &FileSystemDirectoryHandle, hash: &ResourceHash) -> Result<(), JsValue> {
let name = file_name(hash);
match JsFuture::from(directory.remove_entry(&name)).await {
Ok(_) => Ok(()),
Err(error) if is_not_found(&error) => Ok(()),
Err(error) => Err(error),
}
}
async fn read_from_opfs(inner: Arc<Mutex<Inner>>, hash: ResourceHash) -> Option<Resource> {
let directory = {
let guard = inner.lock().unwrap();
if let Some(resource) = guard.cache.get(&hash) {
return Some(resource.clone());
}
if !guard.on_disk.contains(&hash) {
return None;
}
guard.directory.clone()
};
let name = file_name(&hash);
let handle: FileSystemFileHandle = match JsFuture::from(directory.get_file_handle(&name)).await {
Ok(value) => match value.dyn_into() {
Ok(handle) => handle,
Err(value) => {
log::error!("OPFS returned non-file handle for {hash}: {value:?}");
return None;
}
},
Err(error) if is_not_found(&error) => return None,
Err(error) => {
log::error!("OPFS getFileHandle for {hash} failed: {error:?}");
return None;
}
};
let file = match JsFuture::from(handle.get_file()).await {
Ok(value) => value,
Err(error) => {
log::error!("OPFS getFile for {hash} failed: {error:?}");
return None;
}
};
let blob: Blob = match file.dyn_into() {
Ok(blob) => blob,
Err(value) => {
log::error!("OPFS getFile returned non-Blob for {hash}: {value:?}");
return None;
}
};
let buffer = match JsFuture::from(blob.array_buffer()).await {
Ok(buffer) => buffer,
Err(error) => {
log::error!("OPFS arrayBuffer for {hash} failed: {error:?}");
return None;
}
};
let bytes: Arc<[u8]> = Uint8Array::new(&buffer).to_vec().into();
let actual = ResourceHash::from(bytes.as_ref());
if actual != hash {
log::error!("OPFS content-integrity failure: file {hash} hashes to {actual}");
return None;
}
let resource = Resource::new(bytes);
let mut guard = inner.lock().unwrap();
if let Some(resource) = guard.cache.get(&hash) {
return Some(resource.clone());
}
if !guard.on_disk.contains(&hash) {
return None;
}
guard.cache.insert(hash, resource.clone());
Some(resource)
}
fn file_name(hash: &ResourceHash) -> String {
String::from(hash)
}
fn is_not_found(error: &JsValue) -> bool {
error.dyn_ref::<DomException>().is_some_and(|error| error.name() == "NotFoundError")
}
fn oneshot() -> (OneshotSender, OneshotReceiver) {
let state = Arc::new(Mutex::new(OneshotState::default()));
(OneshotSender { state: state.clone() }, OneshotReceiver { state })
}
#[derive(Default)]
struct OneshotState {
value: Option<Option<Resource>>,
waker: Option<Waker>,
}
struct OneshotSender {
state: Arc<Mutex<OneshotState>>,
}
impl OneshotSender {
fn send(self, value: Option<Resource>) {
let mut guard = self.state.lock().unwrap();
guard.value = Some(value);
if let Some(waker) = guard.waker.take() {
waker.wake();
}
}
}
struct OneshotReceiver {
state: Arc<Mutex<OneshotState>>,
}
impl std::future::Future for OneshotReceiver {
type Output = Option<Resource>;
fn poll(self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Self::Output> {
let mut guard = self.state.lock().unwrap();
if let Some(value) = guard.value.take() {
return Poll::Ready(value);
}
guard.waker = Some(context.waker().clone());
Poll::Pending
}
}
@@ -1,105 +0,0 @@
use dyn_any::StaticType;
use graphene_application_io::{ApplicationError, ApplicationIo, ResourceFuture};
use std::collections::HashMap;
use std::sync::Arc;
#[cfg(feature = "tokio")]
use tokio::io::AsyncReadExt;
#[cfg(target_family = "wasm")]
use wasm_bindgen::JsCast;
#[cfg(feature = "wgpu")]
use wgpu_executor::WgpuExecutor;
#[derive(Debug, Default)]
pub struct WasmApplicationIo {
#[cfg(feature = "wgpu")]
pub(crate) gpu_executor: Option<WgpuExecutor>,
pub resources: HashMap<String, Arc<[u8]>>,
}
impl WasmApplicationIo {
pub async fn new() -> Self {
#[cfg(feature = "wgpu")]
let executor = if let Some(gpu) = web_sys::window().map(|w| w.navigator().gpu()) {
let request_adapter = || {
let request_adapter = js_sys::Reflect::get(&gpu, &wasm_bindgen::JsValue::from_str("requestAdapter")).ok()?;
let function = request_adapter.dyn_ref::<js_sys::Function>()?;
function.call0(&gpu).ok()
};
let result = request_adapter();
match result {
None => None,
Some(_) => WgpuExecutor::new().await,
}
} else {
None
};
#[cfg(not(feature = "wgpu"))]
let wgpu_available = false;
#[cfg(feature = "wgpu")]
let wgpu_available = executor.is_some();
super::set_wgpu_available(wgpu_available);
let mut io = Self {
#[cfg(feature = "wgpu")]
gpu_executor: executor,
resources: HashMap::new(),
};
io.resources.insert("null".to_string(), Arc::from(include_bytes!("../null.png").to_vec()));
io
}
}
impl ApplicationIo for WasmApplicationIo {
#[cfg(feature = "wgpu")]
type Executor = WgpuExecutor;
#[cfg(not(feature = "wgpu"))]
type Executor = ();
#[cfg(feature = "wgpu")]
fn gpu_executor(&self) -> Option<&Self::Executor> {
self.gpu_executor.as_ref()
}
fn load_resource(&self, url: impl AsRef<str>) -> Result<ResourceFuture, ApplicationError> {
let url = url::Url::parse(url.as_ref()).map_err(|_| ApplicationError::InvalidUrl)?;
log::trace!("Loading resource: {url:?}");
match url.scheme() {
#[cfg(feature = "tokio")]
"file" => {
let path = url.to_file_path().map_err(|_| ApplicationError::NotFound)?;
let path = path.to_str().ok_or(ApplicationError::NotFound)?;
let path = path.to_owned();
Ok(Box::pin(async move {
let file = tokio::fs::File::open(path).await.map_err(|_| ApplicationError::NotFound)?;
let mut reader = tokio::io::BufReader::new(file);
let mut data = Vec::new();
reader.read_to_end(&mut data).await.map_err(|_| ApplicationError::NotFound)?;
Ok(Arc::from(data))
}) as ResourceFuture)
}
"http" | "https" => {
let url = url.to_string();
Ok(Box::pin(async move {
let client = reqwest::Client::new();
let response = client.get(url).send().await.map_err(|_| ApplicationError::NotFound)?;
let data = response.bytes().await.map_err(|_| ApplicationError::NotFound)?;
Ok(Arc::from(data.to_vec()))
}) as ResourceFuture)
}
"graphite" => {
let path = url.path();
let path = path.to_owned();
log::trace!("Loading local resource: {path}");
let data = self.resources.get(&path).ok_or(ApplicationError::NotFound)?.clone();
Ok(Box::pin(async move { Ok(data.clone()) }) as ResourceFuture)
}
_ => Err(ApplicationError::NotFound),
}
}
}
unsafe impl StaticType for WasmApplicationIo {
type Static = WasmApplicationIo;
}
@@ -396,6 +396,7 @@ tagged_value! {
Footprint(Footprint), Footprint(Footprint),
VectorModification(Box<VectorModification>), VectorModification(Box<VectorModification>),
ImageData(Image<Color>), ImageData(Image<Color>),
Resource(graphene_application_io::ResourceHash),
// ========== // ==========
// ENUM TYPES // ENUM TYPES
// ========== // ==========
+1 -9
View File
@@ -55,10 +55,6 @@ enum Command {
#[clap(long, short = 'o')] #[clap(long, short = 'o')]
output: PathBuf, output: PathBuf,
/// Optional input image resource
#[clap(long)]
image: Option<PathBuf>,
/// Scale factor for export (default: 1.0) /// Scale factor for export (default: 1.0)
#[clap(long, default_value = "1.0")] #[clap(long, default_value = "1.0")]
scale: f64, scale: f64,
@@ -121,11 +117,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
let document_string = std::fs::read_to_string(document_path).expect("Failed to read document"); let document_string = std::fs::read_to_string(document_path).expect("Failed to read document");
log::info!("Creating GPU context"); log::info!("Creating GPU context");
let mut application_io = block_on(PlatformApplicationIo::new()); let 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")));
}
// Convert application_io to Arc first // Convert application_io to Arc first
let application_io_arc = Arc::new(application_io); let application_io_arc = Arc::new(application_io);
@@ -129,6 +129,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u64]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => u64]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => BlendMode]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => BlendMode]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ImageTexture]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => ImageTexture]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::application_io::Resource]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::transform::ReferencePoint]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::BooleanOperation]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::BooleanOperation]),
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Fill]), async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::style::Fill]),
@@ -20,6 +20,7 @@ vector-types = { workspace = true }
text-nodes = { workspace = true } text-nodes = { workspace = true }
# Workspace dependencies # Workspace dependencies
blake3 = { workspace = true }
glam = { workspace = true } glam = { workspace = true }
log = { workspace = true } log = { workspace = true }
+9 -18
View File
@@ -2,15 +2,17 @@ use core_types::transform::Footprint;
use dyn_any::{DynAny, StaticType, StaticTypeSized}; use dyn_any::{DynAny, StaticType, StaticTypeSized};
use glam::DVec2; use glam::DVec2;
use std::fmt::Debug; use std::fmt::Debug;
use std::future::Future;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::pin::Pin;
use std::ptr::addr_of; use std::ptr::addr_of;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use text_nodes::FontCache; use text_nodes::FontCache;
use vector_types::vector::style::RenderMode; use vector_types::vector::style::RenderMode;
pub mod resource;
pub use core_types::resource::Resource;
pub use resource::{LoadResource, ResourceFuture, ResourceHash, ResourceStorage};
#[cfg(feature = "wgpu")] #[cfg(feature = "wgpu")]
#[derive(Debug, Clone, Hash, PartialEq, Eq, DynAny)] #[derive(Debug, Clone, Hash, PartialEq, Eq, DynAny)]
pub struct ImageTexture(Arc<wgpu::Texture>); pub struct ImageTexture(Arc<wgpu::Texture>);
@@ -42,17 +44,12 @@ impl From<ImageTexture> for Arc<wgpu::Texture> {
#[derive(Debug, Clone, Hash, PartialEq, Eq, DynAny)] #[derive(Debug, Clone, Hash, PartialEq, Eq, DynAny)]
pub struct ImageTexture; pub struct ImageTexture;
#[cfg(target_family = "wasm")]
pub type ResourceFuture = Pin<Box<dyn Future<Output = Result<Arc<[u8]>, ApplicationError>>>>;
#[cfg(not(target_family = "wasm"))]
pub type ResourceFuture = Pin<Box<dyn Future<Output = Result<Arc<[u8]>, ApplicationError>> + Send>>;
pub trait ApplicationIo { pub trait ApplicationIo {
type Executor; type Executor;
fn gpu_executor(&self) -> Option<&Self::Executor> { fn gpu_executor(&self) -> Option<&Self::Executor> {
None None
} }
fn load_resource(&self, url: impl AsRef<str>) -> Result<ResourceFuture, ApplicationError>; fn load_resource(&self, hash: ResourceHash) -> resource::ResourceFuture;
} }
impl<T: ApplicationIo> ApplicationIo for &T { impl<T: ApplicationIo> ApplicationIo for &T {
@@ -62,17 +59,11 @@ impl<T: ApplicationIo> ApplicationIo for &T {
(**self).gpu_executor() (**self).gpu_executor()
} }
fn load_resource<'a>(&self, url: impl AsRef<str>) -> Result<ResourceFuture, ApplicationError> { fn load_resource(&self, hash: ResourceHash) -> resource::ResourceFuture {
(**self).load_resource(url) (**self).load_resource(hash)
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ApplicationError {
NotFound,
InvalidUrl,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum NodeGraphUpdateMessage {} pub enum NodeGraphUpdateMessage {}
@@ -139,7 +130,7 @@ impl GetEditorPreferences for DummyPreferences {
pub struct EditorApi<Io> { pub struct EditorApi<Io> {
/// Font data (for rendering text) made available to the graph through the `PlatformEditorApi`. /// Font data (for rendering text) made available to the graph through the `PlatformEditorApi`.
pub font_cache: FontCache, pub font_cache: FontCache,
/// Gives access to APIs like a rendering surface (native window handle or HTML5 canvas) and WGPU (which becomes WebGPU on web). /// Gives access to APIs like resources.
pub application_io: Option<Arc<Io>>, pub application_io: Option<Arc<Io>>,
pub node_graph_message_sender: Box<dyn NodeGraphUpdateSender + Send + Sync>, pub node_graph_message_sender: Box<dyn NodeGraphUpdateSender + Send + Sync>,
/// Editor preferences made available to the graph through the `PlatformEditorApi`. /// Editor preferences made available to the graph through the `PlatformEditorApi`.
@@ -162,7 +153,7 @@ impl<Io: Default> Default for EditorApi<Io> {
impl<Io> Hash for EditorApi<Io> { impl<Io> Hash for EditorApi<Io> {
fn hash<H: Hasher>(&self, state: &mut H) { fn hash<H: Hasher>(&self, state: &mut H) {
self.font_cache.hash(state); self.font_cache.hash(state);
self.application_io.as_ref().map_or(0, |io| io.as_ref() as *const _ as usize).hash(state); self.application_io.as_ref().map_or(0, |io| io as *const _ as usize).hash(state);
(self.node_graph_message_sender.as_ref() as *const dyn NodeGraphUpdateSender).hash(state); (self.node_graph_message_sender.as_ref() as *const dyn NodeGraphUpdateSender).hash(state);
(self.editor_preferences.as_ref() as *const dyn GetEditorPreferences).hash(state); (self.editor_preferences.as_ref() as *const dyn GetEditorPreferences).hash(state);
} }
@@ -0,0 +1,168 @@
use core_types::CacheHash;
use core_types::resource::Resource;
use dyn_any::DynAny;
use std::fmt;
use std::future::Future;
use std::hash::Hash;
use std::pin::Pin;
pub trait LoadResource: Send + Sync {
fn load(&self, hash: ResourceHash) -> ResourceFuture;
}
pub type ResourceFuture = Pin<Box<dyn Future<Output = Option<Resource>> + Send + 'static>>;
pub trait ResourceStorage: LoadResource {
fn store(&mut self, data: &[u8]) -> ResourceHash;
fn contains(&mut self, hash: &ResourceHash) -> bool;
fn garbage_collect(&mut self, used: &[ResourceHash]);
}
/// Blake3 content hash of a resource, represented as 32 bytes
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord, DynAny)]
pub struct ResourceHash([u8; 32]);
impl From<&[u8]> for ResourceHash {
fn from(data: &[u8]) -> Self {
Self(blake3::hash(data).into())
}
}
impl From<[u8; 32]> for ResourceHash {
fn from(bytes: [u8; 32]) -> Self {
Self(bytes)
}
}
impl From<&ResourceHash> for [u8; 32] {
fn from(hash: &ResourceHash) -> Self {
hash.0
}
}
impl From<&ResourceHash> for String {
fn from(hash: &ResourceHash) -> Self {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(hash.0.len() * 2);
for byte in &hash.0 {
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
out
}
}
impl fmt::Display for ResourceHash {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&String::from(self))
}
}
impl std::str::FromStr for ResourceHash {
type Err = ResourceHashParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
fn decode_hex_nibble(byte: u8, position: usize) -> Result<u8, ResourceHashParseError> {
match byte {
b'0'..=b'9' => Ok(byte - b'0'),
b'a'..=b'f' => Ok(byte - b'a' + 10),
b'A'..=b'F' => Ok(byte - b'A' + 10),
_ => Err(ResourceHashParseError::InvalidCharacter { byte, position }),
}
}
let bytes = s.as_bytes();
if bytes.len() != 64 {
return Err(ResourceHashParseError::InvalidLength { found: bytes.len() });
}
let mut out = [0u8; 32];
for (index, chunk) in bytes.chunks_exact(2).enumerate() {
let high = decode_hex_nibble(chunk[0], index * 2)?;
let low = decode_hex_nibble(chunk[1], index * 2 + 1)?;
out[index] = (high << 4) | low;
}
Ok(Self(out))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResourceHashParseError {
InvalidLength { found: usize },
InvalidCharacter { byte: u8, position: usize },
}
impl fmt::Display for ResourceHashParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidLength { found } => write!(f, "resource hash must be 64 hex characters, got {found}"),
Self::InvalidCharacter { byte, position } => write!(f, "resource hash contains non-hex byte {byte:#04x} at position {position}"),
}
}
}
impl std::error::Error for ResourceHashParseError {}
impl TryFrom<&str> for ResourceHash {
type Error = ResourceHashParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for ResourceHash {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
if serializer.is_human_readable() {
serializer.serialize_str(&String::from(self))
} else {
serializer.serialize_bytes(&self.0)
}
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for ResourceHash {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct ResourceHashVisitor;
impl<'de> serde::de::Visitor<'de> for ResourceHashVisitor {
type Value = ResourceHash;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a 64-character hex string or 32 raw bytes")
}
fn visit_str<E: serde::de::Error>(self, value: &str) -> Result<Self::Value, E> {
ResourceHash::try_from(value).map_err(E::custom)
}
fn visit_bytes<E: serde::de::Error>(self, value: &[u8]) -> Result<Self::Value, E> {
let bytes: [u8; 32] = value.try_into().map_err(|_| E::invalid_length(value.len(), &"32 bytes"))?;
Ok(ResourceHash(bytes))
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let mut bytes = [0u8; 32];
for (i, slot) in bytes.iter_mut().enumerate() {
*slot = seq.next_element()?.ok_or_else(|| serde::de::Error::invalid_length(i, &"32 bytes"))?;
}
Ok(ResourceHash(bytes))
}
}
if deserializer.is_human_readable() {
deserializer.deserialize_str(ResourceHashVisitor)
} else {
deserializer.deserialize_bytes(ResourceHashVisitor)
}
}
}
impl CacheHash for ResourceHash {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
core::hash::Hash::hash(self, state);
}
}
@@ -11,6 +11,7 @@ pub mod misc;
pub mod ops; pub mod ops;
pub mod registry; pub mod registry;
pub mod render_complexity; pub mod render_complexity;
pub mod resource;
pub mod transform; pub mod transform;
pub mod uuid; pub mod uuid;
pub mod value; pub mod value;
@@ -0,0 +1,54 @@
use dyn_any::DynAny;
use graphene_hash::CacheHash;
use std::hash::Hash;
use std::ops::Deref;
use std::sync::Arc;
#[derive(Clone, DynAny)]
pub struct Resource {
inner: Arc<dyn AsRef<[u8]> + Send + Sync>,
}
impl Resource {
pub fn new<T: AsRef<[u8]> + Send + Sync + 'static>(data: T) -> Self {
Self { inner: Arc::new(data) }
}
}
impl From<&Resource> for Arc<dyn AsRef<[u8]> + Send + Sync> {
fn from(val: &Resource) -> Self {
val.inner.clone()
}
}
impl Deref for Resource {
type Target = [u8];
fn deref(&self) -> &[u8] {
(*self.inner).as_ref()
}
}
impl AsRef<[u8]> for Resource {
fn as_ref(&self) -> &[u8] {
(*self.inner).as_ref()
}
}
impl std::fmt::Debug for Resource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Resource").field("len", &self.len()).finish()
}
}
impl PartialEq for Resource {
fn eq(&self, other: &Self) -> bool {
self.as_ref() == other.as_ref()
}
}
impl CacheHash for Resource {
fn cache_hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.as_ref().hash(state);
}
}
@@ -13,7 +13,6 @@ use core_types::{ATTR_EDITOR_MERGED_LAYERS, ATTR_TRANSFORM, WasmNotSend};
use core_types::{Color, Ctx}; use core_types::{Color, Ctx};
pub use graph_craft::application_io::*; pub use graph_craft::application_io::*;
pub use graph_craft::document::value::RenderOutputType; pub use graph_craft::document::value::RenderOutputType;
use graphene_application_io::ApplicationIo;
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
pub use graphene_canvas_utils as canvas_utils; pub use graphene_canvas_utils as canvas_utils;
#[cfg(target_family = "wasm")] #[cfg(target_family = "wasm")]
@@ -137,18 +136,24 @@ fn image_to_bytes(_: impl Ctx, image: List<Raster<CPU>>) -> List<u8> {
/// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue. /// Loads binary from URLs and local asset paths. Returns a transparent placeholder if the resource fails to load, allowing rendering to continue.
#[node_macro::node(category("Web Request"))] #[node_macro::node(category("Web Request"))]
async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[scope("editor-api")] editor_resources: &'a PlatformEditorApi, #[name("URL")] url: String) -> Arc<[u8]> { async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[scope("editor-api")] _editor: &'a PlatformEditorApi, #[name("URL")] url: String) -> Arc<[u8]> {
let Some(api) = editor_resources.application_io.as_ref() else { let placeholder = || -> Arc<[u8]> { Arc::from(Vec::<u8>::new()) };
return Arc::from(include_bytes!("../../../graph-craft/src/null.png").to_vec());
}; let response = match reqwest::Client::new().get(&url).send().await {
let Ok(data) = api.load_resource(url) else { Ok(response) => response,
return Arc::from(include_bytes!("../../../graph-craft/src/null.png").to_vec()); Err(error) => {
}; log::error!("HTTP request for `{url}` failed: {error}");
let Ok(data) = data.await else { return placeholder();
return Arc::from(include_bytes!("../../../graph-craft/src/null.png").to_vec()); }
}; };
data match response.bytes().await {
Ok(bytes) => Arc::from(bytes.to_vec()),
Err(error) => {
log::error!("Failed to read HTTP response for `{url}`: {error}");
placeholder()
}
}
} }
/// Converts raw binary data to a raster image. /// Converts raw binary data to a raster image.
@@ -254,3 +259,11 @@ where
.with_attribute(ATTR_EDITOR_MERGED_LAYERS, upstream_graphic_list), .with_attribute(ATTR_EDITOR_MERGED_LAYERS, upstream_graphic_list),
) )
} }
#[node_macro::node(category(""))]
pub async fn resource<'a: 'n>(_: impl Ctx, hash: ResourceHash, #[scope("editor-api")] editor_api: &'a PlatformEditorApi) -> Resource {
let application_io = editor_api.application_io.as_ref().expect("ApplicationIo must be available when using resources");
application_io.load_resource(hash).await.unwrap_or_else(|| {
panic!("Resource {hash} not found");
})
}
+1 -1
View File
@@ -3,7 +3,7 @@ use core_types::transform::{Footprint, Transform};
use core_types::uuid::generate_uuid; use core_types::uuid::generate_uuid;
use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs}; use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs};
use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNotSend}; use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNotSend};
pub use graph_craft::application_io::*; use graph_craft::application_io::PlatformEditorApi;
use graph_craft::document::value::RenderOutput; use graph_craft::document::value::RenderOutput;
pub use graph_craft::document::value::RenderOutputType; pub use graph_craft::document::value::RenderOutputType;
use graphene_application_io::{ApplicationIo, ExportFormat, RenderConfig}; use graphene_application_io::{ApplicationIo, ExportFormat, RenderConfig};
+20 -1
View File
@@ -5,6 +5,7 @@ use core_types::color::{Alpha, AlphaMut, Channel, LinearChannel, Luminance, RGBM
use core_types::context::{Ctx, ExtractFootprint}; use core_types::context::{Ctx, ExtractFootprint};
use core_types::list::{Item, List}; use core_types::list::{Item, List};
use core_types::math::bbox::Bbox; use core_types::math::bbox::Bbox;
use core_types::resource::Resource;
use core_types::transform::Transform; use core_types::transform::Transform;
use dyn_any::DynAny; use dyn_any::DynAny;
use fastnoise_lite; use fastnoise_lite;
@@ -289,7 +290,25 @@ pub fn empty_image(_: impl Ctx, transform: DAffine2, color: List<Color>) -> List
} }
#[node_macro::node(category(""))] #[node_macro::node(category(""))]
pub fn image(_: impl Ctx, _primary: (), image: Image<Color>) -> List<Raster<CPU>> { pub fn image<'a: 'n>(_: impl Ctx, resource: Resource) -> List<Raster<CPU>> {
let image_data = resource.as_ref();
let Some(image) = ::image::load_from_memory(image_data).ok() else {
return List::new();
};
let image = image.to_rgba32f();
let image = Image {
data: image
.chunks(4)
.map(|pixel| {
let alpha = pixel[3];
Color::from_gamma_srgb_channels(pixel[0] * alpha, pixel[1] * alpha, pixel[2] * alpha, alpha)
})
.collect(),
width: image.width(),
height: image.height(),
..Default::default()
};
List::new_from_element(Raster::new_cpu(image)) List::new_from_element(Raster::new_cpu(image))
} }
+45 -1
View File
@@ -10,13 +10,57 @@ use graphene_std::*;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
pub fn expand_network(network: &mut NodeNetwork, substitutions: &HashMap<ProtoNodeIdentifier, DocumentNode>) { pub fn expand_network(network: &mut NodeNetwork, substitutions: &HashMap<ProtoNodeIdentifier, DocumentNode>) {
replace_resource_inputs(network);
expand_network_inner(network, substitutions);
}
/// Replace every `TaggedValue::Resource(hash)` input with a reference to a freshly inserted `resource` proto node.
fn replace_resource_inputs(network: &mut NodeNetwork) {
let mut hash_to_node_id: HashMap<application_io::ResourceHash, NodeId> = HashMap::new();
let mut new_resource_nodes: Vec<(NodeId, DocumentNode)> = Vec::new();
for node in network.nodes.values_mut() {
if let DocumentNodeImplementation::Network(nested) = &mut node.implementation {
replace_resource_inputs(nested);
continue;
}
if matches!(&node.implementation, DocumentNodeImplementation::ProtoNode(identifier) if *identifier == platform_application_io::resource::IDENTIFIER) {
continue;
}
for input in node.inputs.iter_mut() {
let NodeInput::Value { tagged_value, .. } = input else { continue };
let TaggedValue::Resource(hash) = **tagged_value else { continue };
let resource_id = *hash_to_node_id.entry(hash).or_insert_with(|| {
let id = NodeId::new();
let resource_node = DocumentNode {
inputs: vec![NodeInput::value(TaggedValue::Resource(hash), false), NodeInput::scope("editor-api")],
implementation: DocumentNodeImplementation::ProtoNode(platform_application_io::resource::IDENTIFIER),
..Default::default()
};
new_resource_nodes.push((id, resource_node));
id
});
*input = NodeInput::node(resource_id, 0);
}
}
for (id, node) in new_resource_nodes {
network.nodes.insert(id, node);
}
}
fn expand_network_inner(network: &mut NodeNetwork, substitutions: &HashMap<ProtoNodeIdentifier, DocumentNode>) {
if network.generated { if network.generated {
return; return;
} }
for node in network.nodes.values_mut() { for node in network.nodes.values_mut() {
match &mut node.implementation { match &mut node.implementation {
DocumentNodeImplementation::Network(node_network) => expand_network(node_network, substitutions), DocumentNodeImplementation::Network(node_network) => expand_network_inner(node_network, substitutions),
DocumentNodeImplementation::ProtoNode(proto_node_identifier) => { DocumentNodeImplementation::ProtoNode(proto_node_identifier) => {
if let Some(new_node) = substitutions.get(proto_node_identifier) { if let Some(new_node) = substitutions.get(proto_node_identifier) {
// Reconcile the document node's inputs with what the current node definition expects, // Reconcile the document node's inputs with what the current node definition expects,