mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
Remove surface and window from ApplicationIo (#3941)
* Remove surface and window from ApplicationIo * Seperate Wasm and Native ApplicationIo * Fix warnings * Fix tests * Remove redundant PlatformApplicationIo::new_offscreen * Fixup * Remove unused From implementaitions for ApplicationIo
This commit is contained in:
55
node-graph/graph-craft/src/application_io.rs
Normal file
55
node-graph/graph-craft/src/application_io.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
use dyn_any::StaticType;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
mod native;
|
||||
#[cfg(target_family = "wasm")]
|
||||
mod wasm;
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub type PlatformApplicationIo = native::NativeApplicationIo;
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub type PlatformApplicationIo = wasm::WasmApplicationIo;
|
||||
|
||||
pub type PlatformEditorApi = graphene_application_io::EditorApi<PlatformApplicationIo>;
|
||||
|
||||
static WGPU_AVAILABLE: std::sync::atomic::AtomicI8 = std::sync::atomic::AtomicI8::new(-1);
|
||||
|
||||
/// Returns:
|
||||
/// - `None` if the availability of WGPU has not been determined yet
|
||||
/// - `Some(true)` if WGPU is available
|
||||
/// - `Some(false)` if WGPU is not available
|
||||
pub fn wgpu_available() -> Option<bool> {
|
||||
match WGPU_AVAILABLE.load(std::sync::atomic::Ordering::SeqCst) {
|
||||
-1 => None,
|
||||
0 => Some(false),
|
||||
_ => Some(true),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_wgpu_available(available: bool) {
|
||||
WGPU_AVAILABLE.store(available as i8, std::sync::atomic::Ordering::SeqCst);
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, Debug, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct EditorPreferences {
|
||||
/// Maximum render region size in pixels along one dimension of the square area.
|
||||
pub max_render_region_size: u32,
|
||||
}
|
||||
|
||||
impl graphene_application_io::GetEditorPreferences for EditorPreferences {
|
||||
fn max_render_region_area(&self) -> u32 {
|
||||
let size = self.max_render_region_size.min(u32::MAX.isqrt());
|
||||
size.pow(2)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EditorPreferences {
|
||||
fn default() -> Self {
|
||||
Self { max_render_region_size: 1280 }
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl StaticType for EditorPreferences {
|
||||
type Static = EditorPreferences;
|
||||
}
|
||||
113
node-graph/graph-craft/src/application_io/native.rs
Normal file
113
node-graph/graph-craft/src/application_io/native.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
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;
|
||||
}
|
||||
105
node-graph/graph-craft/src/application_io/wasm.rs
Normal file
105
node-graph/graph-craft/src/application_io/wasm.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
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;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::DocumentNode;
|
||||
use crate::application_io::PlatformEditorApi;
|
||||
use crate::proto::{Any as DAny, FutureAny};
|
||||
use crate::wasm_application_io::WasmEditorApi;
|
||||
use brush_nodes::brush_cache::BrushCache;
|
||||
use brush_nodes::brush_stroke::BrushStroke;
|
||||
use core_types::table::Table;
|
||||
@@ -10,7 +10,6 @@ use dyn_any::DynAny;
|
||||
pub use dyn_any::StaticType;
|
||||
use glam::{Affine2, Vec2};
|
||||
pub use glam::{DAffine2, DVec2, IVec2, UVec2};
|
||||
use graphene_application_io::{ImageTexture, SurfaceFrame};
|
||||
use graphic_types::Artboard;
|
||||
use graphic_types::Graphic;
|
||||
use graphic_types::Vector;
|
||||
@@ -40,9 +39,8 @@ macro_rules! tagged_value {
|
||||
None,
|
||||
$( $(#[$meta] ) *$identifier( $ty ), )*
|
||||
RenderOutput(RenderOutput),
|
||||
SurfaceFrame(SurfaceFrame),
|
||||
#[serde(skip)]
|
||||
EditorApi(Arc<WasmEditorApi>)
|
||||
EditorApi(Arc<PlatformEditorApi>)
|
||||
}
|
||||
|
||||
// We must manually implement hashing because some values are floats and so do not reproducibly hash (see FakeHash below)
|
||||
@@ -54,7 +52,6 @@ macro_rules! tagged_value {
|
||||
Self::None => {}
|
||||
$( Self::$identifier(x) => {x.hash(state)}),*
|
||||
Self::RenderOutput(x) => x.hash(state),
|
||||
Self::SurfaceFrame(x) => x.hash(state),
|
||||
Self::EditorApi(x) => x.hash(state),
|
||||
}
|
||||
}
|
||||
@@ -66,7 +63,6 @@ macro_rules! tagged_value {
|
||||
Self::None => Box::new(()),
|
||||
$( Self::$identifier(x) => Box::new(x), )*
|
||||
Self::RenderOutput(x) => Box::new(x),
|
||||
Self::SurfaceFrame(x) => Box::new(x),
|
||||
Self::EditorApi(x) => Box::new(x),
|
||||
}
|
||||
}
|
||||
@@ -76,7 +72,6 @@ macro_rules! tagged_value {
|
||||
Self::None => Arc::new(()),
|
||||
$( Self::$identifier(x) => Arc::new(x), )*
|
||||
Self::RenderOutput(x) => Arc::new(x),
|
||||
Self::SurfaceFrame(x) => Arc::new(x),
|
||||
Self::EditorApi(x) => Arc::new(x),
|
||||
}
|
||||
}
|
||||
@@ -86,8 +81,7 @@ macro_rules! tagged_value {
|
||||
Self::None => concrete!(()),
|
||||
$( Self::$identifier(_) => concrete!($ty), )*
|
||||
Self::RenderOutput(_) => concrete!(RenderOutput),
|
||||
Self::SurfaceFrame(_) => concrete!(SurfaceFrame),
|
||||
Self::EditorApi(_) => concrete!(&WasmEditorApi)
|
||||
Self::EditorApi(_) => concrete!(&PlatformEditorApi)
|
||||
}
|
||||
}
|
||||
/// Attempts to downcast the dynamic type to a tagged value
|
||||
@@ -99,8 +93,6 @@ macro_rules! tagged_value {
|
||||
x if x == TypeId::of::<()>() => Ok(TaggedValue::None),
|
||||
$( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(*downcast(input).unwrap())), )*
|
||||
x if x == TypeId::of::<RenderOutput>() => Ok(TaggedValue::RenderOutput(*downcast(input).unwrap())),
|
||||
x if x == TypeId::of::<SurfaceFrame>() => Ok(TaggedValue::SurfaceFrame(*downcast(input).unwrap())),
|
||||
|
||||
|
||||
_ => Err(format!("Cannot convert {:?} to TaggedValue", DynAny::type_name(input.as_ref()))),
|
||||
}
|
||||
@@ -113,8 +105,7 @@ macro_rules! tagged_value {
|
||||
x if x == TypeId::of::<()>() => Ok(TaggedValue::None),
|
||||
$( x if x == TypeId::of::<$ty>() => Ok(TaggedValue::$identifier(<$ty as Clone>::clone(input.downcast_ref().unwrap()))), )*
|
||||
x if x == TypeId::of::<RenderOutput>() => Ok(TaggedValue::RenderOutput(RenderOutput::clone(input.downcast_ref().unwrap()))),
|
||||
x if x == TypeId::of::<SurfaceFrame>() => Ok(TaggedValue::SurfaceFrame(SurfaceFrame::clone(input.downcast_ref().unwrap()))),
|
||||
_ => Err(format!("Cannot convert {:?} to TaggedValue",std::any::type_name_of_val(input))),
|
||||
_ => Err(format!("Cannot convert {:?} to TaggedValue", std::any::type_name_of_val(input))),
|
||||
}
|
||||
}
|
||||
/// Returns a TaggedValue from the type, where that value is its type's `Default::default()`
|
||||
@@ -148,8 +139,7 @@ macro_rules! tagged_value {
|
||||
Self::None => "()".to_string(),
|
||||
$( Self::$identifier(x) => format!("{:?}", x), )*
|
||||
Self::RenderOutput(_) => "RenderOutput".to_string(),
|
||||
Self::SurfaceFrame(_) => "SurfaceFrame".to_string(),
|
||||
Self::EditorApi(_) => "WasmEditorApi".to_string(),
|
||||
Self::EditorApi(_) => "PlatformEditorApi".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,11 +472,10 @@ pub struct RenderOutput {
|
||||
pub metadata: RenderMetadata,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Hash, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, dyn_any::DynAny, serde::Serialize, serde::Deserialize)]
|
||||
pub enum RenderOutputType {
|
||||
CanvasFrame(SurfaceFrame),
|
||||
#[serde(skip)]
|
||||
Texture(ImageTexture),
|
||||
Texture(graphene_application_io::ImageTexture),
|
||||
#[serde(skip)]
|
||||
Buffer {
|
||||
data: Vec<u8>,
|
||||
@@ -497,8 +486,36 @@ pub enum RenderOutputType {
|
||||
svg: String,
|
||||
image_data: Vec<(u64, Image<Color>)>,
|
||||
},
|
||||
#[cfg(target_family = "wasm")]
|
||||
CanvasFrame {
|
||||
canvas_id: u64,
|
||||
resolution: DVec2,
|
||||
},
|
||||
}
|
||||
|
||||
impl Hash for RenderOutputType {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
match self {
|
||||
Self::Texture(texture) => {
|
||||
texture.hash(state);
|
||||
}
|
||||
Self::Buffer { data, width, height } => {
|
||||
data.hash(state);
|
||||
width.hash(state);
|
||||
height.hash(state);
|
||||
}
|
||||
Self::Svg { svg, image_data } => {
|
||||
svg.hash(state);
|
||||
image_data.hash(state);
|
||||
}
|
||||
#[cfg(target_family = "wasm")]
|
||||
Self::CanvasFrame { canvas_id, resolution } => {
|
||||
canvas_id.hash(state);
|
||||
resolution.to_array().iter().for_each(|x| x.to_bits().hash(state));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Hash for RenderOutput {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.data.hash(state)
|
||||
|
||||
@@ -5,9 +5,9 @@ extern crate core_types;
|
||||
|
||||
pub use core_types::{ProtoNodeIdentifier, Type, TypeDescriptor, concrete, generic};
|
||||
|
||||
pub mod application_io;
|
||||
pub mod document;
|
||||
pub mod graphene_compiler;
|
||||
pub mod proto;
|
||||
#[cfg(feature = "loading")]
|
||||
pub mod util;
|
||||
pub mod wasm_application_io;
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
use dyn_any::StaticType;
|
||||
use graphene_application_io::{ApplicationError, ApplicationIo, ResourceFuture, SurfaceHandle, SurfaceId};
|
||||
#[cfg(target_family = "wasm")]
|
||||
use js_sys::{Object, Reflect};
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
use std::sync::Arc;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use std::sync::atomic::AtomicU64;
|
||||
use std::sync::atomic::Ordering;
|
||||
#[cfg(feature = "tokio")]
|
||||
use tokio::io::AsyncReadExt;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use wasm_bindgen::JsCast;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use wasm_bindgen::JsValue;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use web_sys::HtmlCanvasElement;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use web_sys::window;
|
||||
#[cfg(feature = "wgpu")]
|
||||
use wgpu_executor::WgpuExecutor;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct WindowWrapper {
|
||||
#[cfg(target_family = "wasm")]
|
||||
window: SurfaceHandle<HtmlCanvasElement>,
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
window: SurfaceHandle<Arc<dyn winit::window::Window>>,
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
impl Drop for WindowWrapper {
|
||||
fn drop(&mut self) {
|
||||
let window = window().expect("should have a window in this context");
|
||||
let window = Object::from(window);
|
||||
|
||||
let image_canvases_key = JsValue::from_str("imageCanvases");
|
||||
|
||||
let wrapper = || {
|
||||
if let Ok(canvases) = Reflect::get(&window, &image_canvases_key) {
|
||||
// Convert key and value to JsValue
|
||||
let js_key = JsValue::from_str(self.window.window_id.to_string().as_str());
|
||||
|
||||
// Use Reflect API to set property
|
||||
Reflect::delete_property(&canvases.into(), &js_key)?;
|
||||
}
|
||||
Ok::<_, JsValue>(())
|
||||
};
|
||||
|
||||
wrapper().expect("should be able to set canvas in global scope")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
unsafe impl Sync for WindowWrapper {}
|
||||
#[cfg(target_family = "wasm")]
|
||||
unsafe impl Send for WindowWrapper {}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct WasmApplicationIo {
|
||||
#[cfg(target_family = "wasm")]
|
||||
ids: AtomicU64,
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub(crate) gpu_executor: Option<WgpuExecutor>,
|
||||
windows: Vec<WindowWrapper>,
|
||||
pub resources: HashMap<String, Arc<[u8]>>,
|
||||
}
|
||||
|
||||
static WGPU_AVAILABLE: std::sync::atomic::AtomicI8 = std::sync::atomic::AtomicI8::new(-1);
|
||||
|
||||
/// Returns:
|
||||
/// - `None` if the availability of WGPU has not been determined yet
|
||||
/// - `Some(true)` if WGPU is available
|
||||
/// - `Some(false)` if WGPU is not available
|
||||
pub fn wgpu_available() -> Option<bool> {
|
||||
match WGPU_AVAILABLE.load(Ordering::SeqCst) {
|
||||
-1 => None,
|
||||
0 => Some(false),
|
||||
_ => Some(true),
|
||||
}
|
||||
}
|
||||
|
||||
impl WasmApplicationIo {
|
||||
pub async fn new() -> Self {
|
||||
#[cfg(all(feature = "wgpu", target_family = "wasm"))]
|
||||
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>()?;
|
||||
Some(function.call0(&gpu).ok())
|
||||
};
|
||||
let result = request_adapter();
|
||||
match result {
|
||||
None => None,
|
||||
Some(_) => WgpuExecutor::new().await,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
#[cfg(all(feature = "wgpu", not(target_family = "wasm")))]
|
||||
let executor = WgpuExecutor::new().await;
|
||||
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
let wgpu_available = false;
|
||||
#[cfg(feature = "wgpu")]
|
||||
let wgpu_available = executor.is_some();
|
||||
WGPU_AVAILABLE.store(wgpu_available as i8, Ordering::SeqCst);
|
||||
|
||||
let mut io = Self {
|
||||
#[cfg(target_family = "wasm")]
|
||||
ids: AtomicU64::new(0),
|
||||
#[cfg(feature = "wgpu")]
|
||||
gpu_executor: executor,
|
||||
windows: Vec::new(),
|
||||
resources: HashMap::new(),
|
||||
};
|
||||
let window = io.create_window();
|
||||
io.windows.push(WindowWrapper { window });
|
||||
io.resources.insert("null".to_string(), Arc::from(include_bytes!("null.png").to_vec()));
|
||||
|
||||
io
|
||||
}
|
||||
|
||||
pub async fn new_offscreen() -> 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();
|
||||
WGPU_AVAILABLE.store(wgpu_available as i8, Ordering::SeqCst);
|
||||
|
||||
let mut io = Self {
|
||||
#[cfg(target_family = "wasm")]
|
||||
ids: AtomicU64::new(0),
|
||||
#[cfg(feature = "wgpu")]
|
||||
gpu_executor: executor,
|
||||
windows: Vec::new(),
|
||||
resources: HashMap::new(),
|
||||
};
|
||||
|
||||
io.resources.insert("null".to_string(), Arc::from(include_bytes!("null.png").to_vec()));
|
||||
|
||||
io
|
||||
}
|
||||
#[cfg(all(not(target_family = "wasm"), 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();
|
||||
WGPU_AVAILABLE.store(wgpu_available as i8, Ordering::SeqCst);
|
||||
|
||||
let mut io = Self {
|
||||
gpu_executor: executor,
|
||||
windows: Vec::new(),
|
||||
resources: HashMap::new(),
|
||||
};
|
||||
|
||||
io.resources.insert("null".to_string(), Arc::from(include_bytes!("null.png").to_vec()));
|
||||
|
||||
io
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl StaticType for WasmApplicationIo {
|
||||
type Static = WasmApplicationIo;
|
||||
}
|
||||
|
||||
impl<'a> From<&'a WasmEditorApi> for &'a WasmApplicationIo {
|
||||
fn from(editor_api: &'a WasmEditorApi) -> Self {
|
||||
editor_api.application_io.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "wgpu")]
|
||||
impl<'a> From<&'a WasmApplicationIo> for &'a WgpuExecutor {
|
||||
fn from(app_io: &'a WasmApplicationIo) -> Self {
|
||||
app_io.gpu_executor.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub type WasmEditorApi = graphene_application_io::EditorApi<WasmApplicationIo>;
|
||||
|
||||
impl ApplicationIo for WasmApplicationIo {
|
||||
#[cfg(target_family = "wasm")]
|
||||
type Surface = HtmlCanvasElement;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
type Surface = Arc<dyn winit::window::Window>;
|
||||
#[cfg(feature = "wgpu")]
|
||||
type Executor = WgpuExecutor;
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
type Executor = ();
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
fn create_window(&self) -> SurfaceHandle<Self::Surface> {
|
||||
let wrapper = || {
|
||||
let document = window().expect("should have a window in this context").document().expect("window should have a document");
|
||||
|
||||
let canvas: HtmlCanvasElement = document.create_element("canvas")?.dyn_into::<HtmlCanvasElement>()?;
|
||||
let id = self.ids.fetch_add(1, Ordering::SeqCst);
|
||||
// store the canvas in the global scope so it doesn't get garbage collected
|
||||
let window = window().expect("should have a window in this context");
|
||||
let window = Object::from(window);
|
||||
|
||||
let image_canvases_key = JsValue::from_str("imageCanvases");
|
||||
|
||||
let mut canvases = Reflect::get(&window, &image_canvases_key);
|
||||
if canvases.is_err() {
|
||||
Reflect::set(&JsValue::from(web_sys::window().unwrap()), &image_canvases_key, &Object::new()).unwrap();
|
||||
canvases = Reflect::get(&window, &image_canvases_key);
|
||||
}
|
||||
|
||||
// Convert key and value to JsValue
|
||||
let js_key = JsValue::from_str(id.to_string().as_str());
|
||||
let js_value = JsValue::from(canvas.clone());
|
||||
|
||||
let canvases = Object::from(canvases.unwrap());
|
||||
|
||||
// Use Reflect API to set property
|
||||
Reflect::set(&canvases, &js_key, &js_value)?;
|
||||
Ok::<_, JsValue>(SurfaceHandle {
|
||||
window_id: SurfaceId(id),
|
||||
surface: canvas,
|
||||
})
|
||||
};
|
||||
|
||||
wrapper().expect("should be able to set canvas in global scope")
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn create_window(&self) -> SurfaceHandle<Self::Surface> {
|
||||
todo!("winit api changed, calling create_window on EventLoop is deprecated");
|
||||
|
||||
// log::trace!("Spawning window");
|
||||
|
||||
// #[cfg(all(not(test), target_os = "linux", feature = "wayland"))]
|
||||
// use winit::platform::wayland::EventLoopBuilderExtWayland;
|
||||
|
||||
// #[cfg(all(not(test), target_os = "linux", feature = "wayland"))]
|
||||
// let event_loop = winit::event_loop::EventLoopBuilder::new().with_any_thread(true).build().unwrap();
|
||||
// #[cfg(not(all(not(test), target_os = "linux", feature = "wayland")))]
|
||||
// let event_loop = winit::event_loop::EventLoop::new().unwrap();
|
||||
|
||||
// let window = event_loop
|
||||
// .create_window(
|
||||
// winit::window::WindowAttributes::default()
|
||||
// .with_title("Graphite")
|
||||
// .with_inner_size(winit::dpi::PhysicalSize::new(800, 600)),
|
||||
// )
|
||||
// .unwrap();
|
||||
|
||||
// SurfaceHandle {
|
||||
// window_id: SurfaceId(window.id().into()),
|
||||
// surface: Arc::new(window),
|
||||
// }
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
fn destroy_window(&self, surface_id: SurfaceId) {
|
||||
let window = window().expect("should have a window in this context");
|
||||
let window = Object::from(window);
|
||||
|
||||
let image_canvases_key = JsValue::from_str("imageCanvases");
|
||||
|
||||
let wrapper = || {
|
||||
if let Ok(canvases) = Reflect::get(&window, &image_canvases_key) {
|
||||
// Convert key and value to JsValue
|
||||
let js_key = JsValue::from_str(surface_id.0.to_string().as_str());
|
||||
|
||||
// Use Reflect API to set property
|
||||
Reflect::delete_property(&canvases.into(), &js_key)?;
|
||||
}
|
||||
Ok::<_, JsValue>(())
|
||||
};
|
||||
|
||||
wrapper().expect("should be able to set canvas in global scope")
|
||||
}
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn destroy_window(&self, _surface_id: SurfaceId) {}
|
||||
|
||||
#[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),
|
||||
}
|
||||
}
|
||||
|
||||
fn window(&self) -> Option<SurfaceHandle<Self::Surface>> {
|
||||
self.windows.first().map(|wrapper| wrapper.window.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub type WasmSurfaceHandle = SurfaceHandle<wgpu_executor::Window>;
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub type WasmSurfaceHandleFrame = graphene_application_io::SurfaceHandleFrame<wgpu_executor::Window>;
|
||||
|
||||
#[cfg_attr(feature = "wasm", derive(tsify::Tsify))]
|
||||
#[derive(Clone, Debug, PartialEq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct EditorPreferences {
|
||||
/// Maximum render region size in pixels along one dimension of the square area.
|
||||
pub max_render_region_size: u32,
|
||||
}
|
||||
|
||||
impl graphene_application_io::GetEditorPreferences for EditorPreferences {
|
||||
fn max_render_region_area(&self) -> u32 {
|
||||
let size = self.max_render_region_size.min(u32::MAX.isqrt());
|
||||
size.pow(2)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EditorPreferences {
|
||||
fn default() -> Self {
|
||||
Self { max_render_region_size: 1280 }
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl StaticType for EditorPreferences {
|
||||
type Static = EditorPreferences;
|
||||
}
|
||||
@@ -3,14 +3,14 @@ mod export;
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
use futures::executor::block_on;
|
||||
use graph_craft::application_io::EditorPreferences;
|
||||
use graph_craft::document::*;
|
||||
use graph_craft::graphene_compiler::Compiler;
|
||||
use graph_craft::proto::ProtoNetwork;
|
||||
use graph_craft::util::load_network;
|
||||
use graph_craft::wasm_application_io::EditorPreferences;
|
||||
use graphene_std::application_io::{ApplicationIo, NodeGraphUpdateMessage, NodeGraphUpdateSender};
|
||||
use graphene_std::application_io::{PlatformEditorApi, WasmApplicationIo};
|
||||
use graphene_std::text::FontCache;
|
||||
use graphene_std::wasm_application_io::{WasmApplicationIo, WasmEditorApi};
|
||||
use interpreted_executor::dynamic_executor::DynamicExecutor;
|
||||
use interpreted_executor::util::wrap_network_in_scope;
|
||||
use std::error::Error;
|
||||
@@ -121,7 +121,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let document_string = std::fs::read_to_string(document_path).expect("Failed to read document");
|
||||
|
||||
log::info!("Creating GPU context");
|
||||
let mut application_io = block_on(WasmApplicationIo::new_offscreen());
|
||||
let mut application_io = block_on(WasmApplicationIo::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")));
|
||||
@@ -140,7 +140,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||
let preferences = EditorPreferences {
|
||||
max_render_region_size: EditorPreferences::default().max_render_region_size,
|
||||
};
|
||||
let editor_api = Arc::new(WasmEditorApi {
|
||||
let editor_api = Arc::new(PlatformEditorApi {
|
||||
font_cache: FontCache::default(),
|
||||
application_io: Some(application_io_for_api),
|
||||
node_graph_message_sender: Box::new(UpdateLogger {}),
|
||||
@@ -247,7 +247,7 @@ fn fix_nodes(network: &mut NodeNetwork) {
|
||||
}
|
||||
}
|
||||
}
|
||||
fn compile_graph(document_string: String, editor_api: Arc<WasmEditorApi>) -> Result<ProtoNetwork, Box<dyn Error>> {
|
||||
fn compile_graph(document_string: String, editor_api: Arc<PlatformEditorApi>) -> Result<ProtoNetwork, Box<dyn Error>> {
|
||||
let mut network = load_network(&document_string);
|
||||
fix_nodes(&mut network);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ authors.workspace = true
|
||||
[features]
|
||||
default = []
|
||||
gpu = ["graphene-std/gpu", "graphene-std/wgpu"]
|
||||
wasm = ["graphene-std/wasm"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
use dyn_any::StaticType;
|
||||
use glam::{DAffine2, DVec2, IVec2};
|
||||
use graph_craft::application_io::PlatformEditorApi;
|
||||
use graph_craft::document::DocumentNode;
|
||||
use graph_craft::document::value::RenderOutput;
|
||||
use graph_craft::proto::{NodeConstructor, TypeErasedBox};
|
||||
use graphene_std::any::DynAnyNode;
|
||||
use graphene_std::application_io::{ImageTexture, SurfaceFrame};
|
||||
use graphene_std::application_io::ImageTexture;
|
||||
use graphene_std::brush::brush_cache::BrushCache;
|
||||
use graphene_std::brush::brush_stroke::BrushStroke;
|
||||
use graphene_std::gradient::GradientStops;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use graphene_std::platform_application_io::canvas_utils::CanvasHandle;
|
||||
#[cfg(feature = "gpu")]
|
||||
use graphene_std::raster::GPU;
|
||||
use graphene_std::raster::color::Color;
|
||||
@@ -18,17 +21,11 @@ use graphene_std::table::Table;
|
||||
use graphene_std::transform::Footprint;
|
||||
use graphene_std::uuid::NodeId;
|
||||
use graphene_std::vector::Vector;
|
||||
use graphene_std::wasm_application_io::WasmEditorApi;
|
||||
#[cfg(feature = "gpu")]
|
||||
use graphene_std::wasm_application_io::WasmSurfaceHandle;
|
||||
use graphene_std::{Artboard, Context, Graphic, NodeIO, NodeIOTypes, ProtoNodeIdentifier, concrete, fn_type_fut, future};
|
||||
use node_registry_macros::{async_node, convert_node, into_node};
|
||||
use std::collections::HashMap;
|
||||
#[cfg(feature = "gpu")]
|
||||
use std::sync::Arc;
|
||||
#[cfg(feature = "gpu")]
|
||||
use wgpu_executor::WgpuExecutor;
|
||||
use wgpu_executor::{WgpuSurface, WindowHandle};
|
||||
|
||||
// TODO: turn into hashmap
|
||||
fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeConstructor>> {
|
||||
@@ -47,7 +44,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
||||
convert_node!(from: Table<Raster<GPU>>, to: Table<Graphic>),
|
||||
// into_node!(from: Table<Raster<CPU>>, to: Table<Raster<SRGBA8>>),
|
||||
#[cfg(feature = "gpu")]
|
||||
into_node!(from: &WasmEditorApi, to: &WgpuExecutor),
|
||||
into_node!(from: &PlatformEditorApi, to: &WgpuExecutor),
|
||||
convert_node!(from: DVec2, to: DVec2),
|
||||
convert_node!(from: String, to: String),
|
||||
convert_node!(from: bool, to: String),
|
||||
@@ -138,14 +135,11 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
||||
async_node!(graphene_core::memo::MonitorNode<_, _, _>, input: Context, fn_params: [Context => graphene_std::vector::misc::InterpolationDistribution]),
|
||||
// Context nullification
|
||||
#[cfg(feature = "gpu")]
|
||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &WasmEditorApi, Context => graphene_std::ContextFeatures]),
|
||||
#[cfg(feature = "gpu")]
|
||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Arc<WasmSurfaceHandle>, Context => graphene_std::ContextFeatures]),
|
||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => &PlatformEditorApi, Context => graphene_std::ContextFeatures]),
|
||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderIntermediate, Context => graphene_std::ContextFeatures]),
|
||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => RenderOutput, Context => graphene_std::ContextFeatures]),
|
||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => WgpuSurface, Context => graphene_std::ContextFeatures]),
|
||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => Option<WgpuSurface>, Context => graphene_std::ContextFeatures]),
|
||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => WindowHandle, Context => graphene_std::ContextFeatures]),
|
||||
#[cfg(target_family = "wasm")]
|
||||
async_node!(graphene_core::context_modification::ContextModificationNode<_, _>, input: Context, fn_params: [Context => CanvasHandle, Context => graphene_std::ContextFeatures]),
|
||||
// ==========
|
||||
// MEMO NODES
|
||||
// ==========
|
||||
@@ -163,11 +157,8 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<f64>]),
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<f32>]),
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Vec<String>]),
|
||||
#[cfg(feature = "gpu")]
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Arc<WasmSurfaceHandle>]),
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => WindowHandle]),
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option<WgpuSurface>]),
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => SurfaceFrame]),
|
||||
#[cfg(target_family = "wasm")]
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => CanvasHandle]),
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => f64]),
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => f32]),
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => u32]),
|
||||
@@ -177,9 +168,7 @@ fn node_registry() -> HashMap<ProtoNodeIdentifier, HashMap<NodeIOTypes, NodeCons
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => DAffine2]),
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Footprint]),
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => RenderOutput]),
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => &WasmEditorApi]),
|
||||
#[cfg(feature = "gpu")]
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => WgpuSurface]),
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => &PlatformEditorApi]),
|
||||
#[cfg(feature = "gpu")]
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Table<Raster<GPU>>]),
|
||||
async_node!(graphene_core::memo::MemoNode<_, _>, input: Context, fn_params: [Context => Option<f64>]),
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use graph_craft::ProtoNodeIdentifier;
|
||||
use graph_craft::application_io::PlatformEditorApi;
|
||||
use graph_craft::concrete;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeInput, NodeNetwork};
|
||||
use graph_craft::generic;
|
||||
use graph_craft::wasm_application_io::WasmEditorApi;
|
||||
use graphene_std::Context;
|
||||
use graphene_std::ContextFeatures;
|
||||
use graphene_std::uuid::NodeId;
|
||||
use std::sync::Arc;
|
||||
use wgpu_executor::WgpuExecutor;
|
||||
|
||||
pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<WasmEditorApi>) -> NodeNetwork {
|
||||
pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<PlatformEditorApi>) -> NodeNetwork {
|
||||
network.generate_node_paths(&[]);
|
||||
|
||||
let inner_network = DocumentNode {
|
||||
@@ -102,7 +102,7 @@ pub fn wrap_network_in_scope(mut network: NodeNetwork, editor_api: Arc<WasmEdito
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
let mut scope_injections = vec![("editor-api".to_string(), (NodeId(2), concrete!(&WasmEditorApi)))];
|
||||
let mut scope_injections = vec![("editor-api".to_string(), (NodeId(2), concrete!(&PlatformEditorApi)))];
|
||||
|
||||
if cfg!(feature = "gpu") {
|
||||
nodes.push(DocumentNode {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use core_types::transform::Footprint;
|
||||
use dyn_any::{DynAny, StaticType, StaticTypeSized};
|
||||
use glam::{DAffine2, DVec2, UVec2};
|
||||
use glam::DVec2;
|
||||
use std::fmt::Debug;
|
||||
use std::future::Future;
|
||||
use std::hash::{Hash, Hasher};
|
||||
@@ -11,145 +11,36 @@ use std::time::Duration;
|
||||
use text_nodes::FontCache;
|
||||
use vector_types::vector::style::RenderMode;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SurfaceId(pub u64);
|
||||
|
||||
impl std::fmt::Display for SurfaceId {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_fmt(format_args!("{}", self.0))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize)]
|
||||
pub struct SurfaceFrame {
|
||||
pub surface_id: SurfaceId,
|
||||
/// Logical resolution in CSS pixels (used for foreignObject dimensions)
|
||||
pub resolution: DVec2,
|
||||
pub transform: DAffine2,
|
||||
}
|
||||
|
||||
impl Hash for SurfaceFrame {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.surface_id.hash(state);
|
||||
self.transform.to_cols_array().iter().for_each(|x| x.to_bits().hash(state));
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl StaticType for SurfaceFrame {
|
||||
type Static = SurfaceFrame;
|
||||
}
|
||||
|
||||
pub trait Size {
|
||||
fn size(&self) -> UVec2;
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
impl Size for web_sys::HtmlCanvasElement {
|
||||
fn size(&self) -> UVec2 {
|
||||
UVec2::new(self.width(), self.height())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, DynAny)]
|
||||
pub struct ImageTexture {
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub texture: Arc<wgpu::Texture>,
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
pub texture: (),
|
||||
}
|
||||
|
||||
impl<'a> serde::Deserialize<'a> for ImageTexture {
|
||||
fn deserialize<D>(_: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'a>,
|
||||
{
|
||||
unimplemented!("attempted to serialize a texture")
|
||||
}
|
||||
}
|
||||
|
||||
impl Hash for ImageTexture {
|
||||
#[cfg(feature = "wgpu")]
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.texture.hash(state);
|
||||
}
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
fn hash<H: Hasher>(&self, _state: &mut H) {}
|
||||
}
|
||||
|
||||
impl PartialEq for ImageTexture {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
#[cfg(feature = "wgpu")]
|
||||
{
|
||||
self.texture == other.texture
|
||||
}
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
{
|
||||
self.texture == other.texture
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
impl Size for ImageTexture {
|
||||
fn size(&self) -> UVec2 {
|
||||
UVec2::new(self.texture.width(), self.texture.height())
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, DynAny)]
|
||||
pub struct ImageTexture(Arc<wgpu::Texture>);
|
||||
#[cfg(feature = "wgpu")]
|
||||
impl AsRef<wgpu::Texture> for ImageTexture {
|
||||
fn as_ref(&self) -> &wgpu::Texture {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: Size> From<SurfaceHandleFrame<S>> for SurfaceFrame {
|
||||
fn from(x: SurfaceHandleFrame<S>) -> Self {
|
||||
let size = x.surface_handle.surface.size();
|
||||
Self {
|
||||
surface_id: x.surface_handle.window_id,
|
||||
transform: x.transform,
|
||||
resolution: size.into(),
|
||||
}
|
||||
#[cfg(feature = "wgpu")]
|
||||
impl From<wgpu::Texture> for ImageTexture {
|
||||
fn from(texture: wgpu::Texture) -> Self {
|
||||
Self(Arc::new(texture))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SurfaceHandle<Surface> {
|
||||
pub window_id: SurfaceId,
|
||||
pub surface: Surface,
|
||||
}
|
||||
|
||||
// #[cfg(target_family = "wasm")]
|
||||
// unsafe impl<T: dyn_any::WasmNotSend> Send for SurfaceHandle<T> {}
|
||||
// #[cfg(target_family = "wasm")]
|
||||
// unsafe impl<T: dyn_any::WasmNotSync> Sync for SurfaceHandle<T> {}
|
||||
|
||||
impl<S: Size> Size for SurfaceHandle<S> {
|
||||
fn size(&self) -> UVec2 {
|
||||
self.surface.size()
|
||||
#[cfg(feature = "wgpu")]
|
||||
impl From<Arc<wgpu::Texture>> for ImageTexture {
|
||||
fn from(texture: Arc<wgpu::Texture>) -> Self {
|
||||
Self(texture)
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl<T: 'static> StaticType for SurfaceHandle<T> {
|
||||
type Static = SurfaceHandle<T>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct SurfaceHandleFrame<Surface> {
|
||||
pub surface_handle: Arc<SurfaceHandle<Surface>>,
|
||||
pub transform: DAffine2,
|
||||
}
|
||||
|
||||
unsafe impl<T: 'static> StaticType for SurfaceHandleFrame<T> {
|
||||
type Static = SurfaceHandleFrame<T>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
pub type WasmSurfaceHandle = SurfaceHandle<web_sys::HtmlCanvasElement>;
|
||||
#[cfg(feature = "wasm")]
|
||||
pub type WasmSurfaceHandleFrame = SurfaceHandleFrame<web_sys::HtmlCanvasElement>;
|
||||
|
||||
// TODO: think about how to automatically clean up memory
|
||||
/*
|
||||
impl<'a, Surface> Drop for SurfaceHandle<'a, Surface> {
|
||||
fn drop(&mut self) {
|
||||
self.application_io.destroy_surface(self.surface_id)
|
||||
#[cfg(feature = "wgpu")]
|
||||
impl From<ImageTexture> for Arc<wgpu::Texture> {
|
||||
fn from(image_texture: ImageTexture) -> Self {
|
||||
image_texture.0
|
||||
}
|
||||
}*/
|
||||
}
|
||||
#[cfg(not(feature = "wgpu"))]
|
||||
#[derive(Debug, Clone, Hash, PartialEq, Eq, DynAny)]
|
||||
pub struct ImageTexture;
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub type ResourceFuture = Pin<Box<dyn Future<Output = Result<Arc<[u8]>, ApplicationError>>>>;
|
||||
@@ -157,11 +48,7 @@ pub type ResourceFuture = Pin<Box<dyn Future<Output = Result<Arc<[u8]>, Applicat
|
||||
pub type ResourceFuture = Pin<Box<dyn Future<Output = Result<Arc<[u8]>, ApplicationError>> + Send>>;
|
||||
|
||||
pub trait ApplicationIo {
|
||||
type Surface;
|
||||
type Executor;
|
||||
fn window(&self) -> Option<SurfaceHandle<Self::Surface>>;
|
||||
fn create_window(&self) -> SurfaceHandle<Self::Surface>;
|
||||
fn destroy_window(&self, surface_id: SurfaceId);
|
||||
fn gpu_executor(&self) -> Option<&Self::Executor> {
|
||||
None
|
||||
}
|
||||
@@ -169,21 +56,8 @@ pub trait ApplicationIo {
|
||||
}
|
||||
|
||||
impl<T: ApplicationIo> ApplicationIo for &T {
|
||||
type Surface = T::Surface;
|
||||
type Executor = T::Executor;
|
||||
|
||||
fn window(&self) -> Option<SurfaceHandle<Self::Surface>> {
|
||||
(**self).window()
|
||||
}
|
||||
|
||||
fn create_window(&self) -> SurfaceHandle<T::Surface> {
|
||||
(**self).create_window()
|
||||
}
|
||||
|
||||
fn destroy_window(&self, surface_id: SurfaceId) {
|
||||
(**self).destroy_window(surface_id)
|
||||
}
|
||||
|
||||
fn gpu_executor(&self) -> Option<&T::Executor> {
|
||||
(**self).gpu_executor()
|
||||
}
|
||||
@@ -260,12 +134,12 @@ impl GetEditorPreferences for DummyPreferences {
|
||||
}
|
||||
|
||||
pub struct EditorApi<Io> {
|
||||
/// Font data (for rendering text) made available to the graph through the [`WasmEditorApi`].
|
||||
/// Font data (for rendering text) made available to the graph through the `PlatformEditorApi`.
|
||||
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).
|
||||
pub application_io: Option<Arc<Io>>,
|
||||
pub node_graph_message_sender: Box<dyn NodeGraphUpdateSender + Send + Sync>,
|
||||
/// Editor preferences made available to the graph through the [`WasmEditorApi`].
|
||||
/// Editor preferences made available to the graph through the `PlatformEditorApi`.
|
||||
pub editor_preferences: Box<dyn GetEditorPreferences + Send + Sync>,
|
||||
}
|
||||
|
||||
|
||||
28
node-graph/libraries/canvas-utils/Cargo.toml
Normal file
28
node-graph/libraries/canvas-utils/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "graphene-canvas-utils"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
description = "graphene canvas utilities"
|
||||
authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
wgpu = ["dep:wgpu", "dep:wgpu-executor"]
|
||||
|
||||
[dependencies]
|
||||
# Local dependencies
|
||||
dyn-any = { workspace = true }
|
||||
core-types = { workspace = true }
|
||||
vector-types = { workspace = true }
|
||||
text-nodes = { workspace = true }
|
||||
graphene-application-io = { workspace = true }
|
||||
|
||||
# Workspace dependencies
|
||||
web-sys = { workspace = true }
|
||||
glam = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
wgpu = { workspace = true, optional = true }
|
||||
wgpu-executor = { workspace = true, optional = true }
|
||||
8
node-graph/libraries/canvas-utils/src/lib.rs
Normal file
8
node-graph/libraries/canvas-utils/src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
//! A collection of utilities for working with HTML canvases.
|
||||
//! This library is designed to be used in a WebAssembly context.
|
||||
//! It doesn't expose any functionality when compiled for non-WebAssembly targets
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
mod wasm;
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub use wasm::*;
|
||||
208
node-graph/libraries/canvas-utils/src/wasm.rs
Normal file
208
node-graph/libraries/canvas-utils/src/wasm.rs
Normal file
@@ -0,0 +1,208 @@
|
||||
use dyn_any::DynAny;
|
||||
#[cfg(feature = "wgpu")]
|
||||
use graphene_application_io::ImageTexture;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use web_sys::js_sys::{Object, Reflect};
|
||||
use web_sys::wasm_bindgen::{JsCast, JsValue};
|
||||
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, window};
|
||||
#[cfg(feature = "wgpu")]
|
||||
use wgpu_executor::WgpuExecutor;
|
||||
|
||||
const CANVASES_OBJECT_KEY: &str = "imageCanvases";
|
||||
|
||||
pub type CanvasId = u64;
|
||||
|
||||
static CANVAS_IDS: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
pub trait Canvas {
|
||||
fn id(&mut self) -> CanvasId;
|
||||
fn context(&mut self) -> CanvasRenderingContext2d;
|
||||
fn set_resolution(&mut self, resolution: glam::UVec2);
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub trait CanvasSurface: Canvas {
|
||||
fn present(&mut self, image_texture: &ImageTexture, executor: &WgpuExecutor);
|
||||
}
|
||||
|
||||
#[derive(Clone, DynAny)]
|
||||
pub struct CanvasHandle(Option<Arc<CanvasImpl>>);
|
||||
impl CanvasHandle {
|
||||
pub fn new() -> Self {
|
||||
Self(None)
|
||||
}
|
||||
fn get(&mut self) -> &CanvasImpl {
|
||||
if self.0.is_none() {
|
||||
self.0 = Some(Arc::new(CanvasImpl::new()));
|
||||
}
|
||||
self.0.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
impl Canvas for CanvasHandle {
|
||||
fn id(&mut self) -> CanvasId {
|
||||
self.get().canvas_id
|
||||
}
|
||||
fn context(&mut self) -> CanvasRenderingContext2d {
|
||||
self.get().context()
|
||||
}
|
||||
fn set_resolution(&mut self, resolution: glam::UVec2) {
|
||||
self.get().set_resolution(resolution);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "wgpu")]
|
||||
pub struct CanvasSurfaceHandle(CanvasHandle, Option<Arc<wgpu::Surface<'static>>>);
|
||||
#[cfg(feature = "wgpu")]
|
||||
impl CanvasSurfaceHandle {
|
||||
pub fn new() -> Self {
|
||||
Self(CanvasHandle::new(), None)
|
||||
}
|
||||
fn surface(&mut self, executor: &WgpuExecutor) -> &wgpu::Surface<'_> {
|
||||
if self.1.is_none() {
|
||||
let canvas = self.0.get().canvas.clone();
|
||||
let surface = executor
|
||||
.context
|
||||
.instance
|
||||
.create_surface(wgpu::SurfaceTarget::Canvas(canvas))
|
||||
.expect("Failed to create surface from canvas");
|
||||
self.1 = Some(Arc::new(surface));
|
||||
}
|
||||
self.1.as_ref().unwrap()
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "wgpu")]
|
||||
impl Canvas for CanvasSurfaceHandle {
|
||||
fn id(&mut self) -> CanvasId {
|
||||
self.0.id()
|
||||
}
|
||||
fn context(&mut self) -> CanvasRenderingContext2d {
|
||||
self.0.context()
|
||||
}
|
||||
fn set_resolution(&mut self, resolution: glam::UVec2) {
|
||||
self.0.set_resolution(resolution);
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "wgpu")]
|
||||
impl CanvasSurface for CanvasSurfaceHandle {
|
||||
fn present(&mut self, image_texture: &ImageTexture, executor: &WgpuExecutor) {
|
||||
let source_texture: &wgpu::Texture = image_texture.as_ref();
|
||||
|
||||
let surface = self.surface(executor);
|
||||
|
||||
// Blit the texture to the surface
|
||||
let mut encoder = executor.context.device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
|
||||
label: Some("Texture to Surface Blit"),
|
||||
});
|
||||
|
||||
let size = source_texture.size();
|
||||
|
||||
// Configure the surface at physical resolution (for HiDPI displays)
|
||||
let surface_caps = surface.get_capabilities(&executor.context.adapter);
|
||||
surface.configure(
|
||||
&executor.context.device,
|
||||
&wgpu::SurfaceConfiguration {
|
||||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_DST,
|
||||
format: wgpu::TextureFormat::Rgba8Unorm,
|
||||
width: size.width,
|
||||
height: size.height,
|
||||
present_mode: surface_caps.present_modes[0],
|
||||
alpha_mode: wgpu::CompositeAlphaMode::PreMultiplied,
|
||||
view_formats: vec![],
|
||||
desired_maximum_frame_latency: 2,
|
||||
},
|
||||
);
|
||||
|
||||
let surface_texture = surface.get_current_texture().expect("Failed to get surface texture");
|
||||
|
||||
encoder.copy_texture_to_texture(
|
||||
wgpu::TexelCopyTextureInfoBase {
|
||||
texture: source_texture,
|
||||
mip_level: 0,
|
||||
origin: Default::default(),
|
||||
aspect: Default::default(),
|
||||
},
|
||||
wgpu::TexelCopyTextureInfoBase {
|
||||
texture: &surface_texture.texture,
|
||||
mip_level: 0,
|
||||
origin: Default::default(),
|
||||
aspect: Default::default(),
|
||||
},
|
||||
source_texture.size(),
|
||||
);
|
||||
|
||||
executor.context.queue.submit([encoder.finish()]);
|
||||
surface_texture.present();
|
||||
}
|
||||
}
|
||||
|
||||
/// A wgpu surface backed by an HTML canvas element.
|
||||
/// Holds a reference to the canvas to prevent garbage collection.
|
||||
pub struct CanvasImpl {
|
||||
canvas_id: u64,
|
||||
canvas: HtmlCanvasElement,
|
||||
}
|
||||
|
||||
impl CanvasImpl {
|
||||
fn new() -> Self {
|
||||
let document = window().expect("should have a window in this context").document().expect("window should have a document");
|
||||
|
||||
let canvas: HtmlCanvasElement = document.create_element("canvas").unwrap().dyn_into::<HtmlCanvasElement>().unwrap();
|
||||
let canvas_id = CANVAS_IDS.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
// Store the canvas in the global scope so it doesn't get garbage collected
|
||||
let window = window().expect("should have a window in this context");
|
||||
let window_obj = Object::from(window);
|
||||
|
||||
let image_canvases_key = JsValue::from_str(CANVASES_OBJECT_KEY);
|
||||
|
||||
let mut canvases = Reflect::get(&window_obj, &image_canvases_key);
|
||||
if canvases.is_err() || canvases.as_ref().map_or(false, |v| v.is_undefined() || v.is_null()) {
|
||||
Reflect::set(&window_obj.clone(), &image_canvases_key, &Object::new()).unwrap();
|
||||
canvases = Reflect::get(&window_obj, &image_canvases_key);
|
||||
}
|
||||
|
||||
// Convert key and value to JsValue
|
||||
let js_key = JsValue::from_str(canvas_id.to_string().as_str());
|
||||
let js_value = JsValue::from(canvas.clone());
|
||||
|
||||
let canvases = Object::from(canvases.unwrap());
|
||||
|
||||
// Use Reflect API to set property
|
||||
Reflect::set(&canvases, &js_key, &js_value).unwrap();
|
||||
|
||||
Self { canvas_id, canvas }
|
||||
}
|
||||
fn context(&self) -> CanvasRenderingContext2d {
|
||||
self.canvas
|
||||
.get_context("2d")
|
||||
.expect("Failed to get 2D context from canvas")
|
||||
.unwrap()
|
||||
.dyn_into::<CanvasRenderingContext2d>()
|
||||
.expect("Failed to cast context to CanvasRenderingContext2d")
|
||||
}
|
||||
fn set_resolution(&self, resolution: glam::UVec2) {
|
||||
self.canvas.set_width(resolution.x);
|
||||
self.canvas.set_height(resolution.y);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CanvasImpl {
|
||||
fn drop(&mut self) {
|
||||
let canvas_id = self.canvas_id;
|
||||
let window = window().expect("should have a window in this context");
|
||||
let window_obj = Object::from(window);
|
||||
|
||||
let image_canvases_key = JsValue::from_str(CANVASES_OBJECT_KEY);
|
||||
|
||||
if let Ok(canvases) = Reflect::get(&window_obj, &image_canvases_key) {
|
||||
let canvases = Object::from(canvases);
|
||||
let js_key = JsValue::from_str(canvas_id.to_string().as_str());
|
||||
Reflect::delete_property(&canvases, &js_key).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SAFETY: WASM is single-threaded, so Send/Sync are safe
|
||||
unsafe impl Send for CanvasImpl {}
|
||||
unsafe impl Sync for CanvasImpl {}
|
||||
@@ -20,6 +20,5 @@ anyhow = { workspace = true }
|
||||
wgpu = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
web-sys = { workspace = true }
|
||||
winit = { workspace = true }
|
||||
vello = { workspace = true }
|
||||
bytemuck = { workspace = true }
|
||||
|
||||
@@ -7,13 +7,10 @@ use crate::resample::Resampler;
|
||||
use crate::shader_runtime::ShaderRuntime;
|
||||
use anyhow::Result;
|
||||
use core_types::Color;
|
||||
use dyn_any::StaticType;
|
||||
use futures::lock::Mutex;
|
||||
use glam::UVec2;
|
||||
use graphene_application_io::{ApplicationIo, EditorApi, SurfaceHandle, SurfaceId};
|
||||
use std::sync::Arc;
|
||||
use graphene_application_io::{ApplicationIo, EditorApi};
|
||||
use vello::{AaConfig, AaSupport, RenderParams, Renderer, RendererOptions, Scene};
|
||||
use wgpu::util::TextureBlitter;
|
||||
use wgpu::{Origin3d, TextureAspect};
|
||||
|
||||
pub use context::Context as WgpuContext;
|
||||
@@ -42,15 +39,6 @@ impl<'a, T: ApplicationIo<Executor = WgpuExecutor>> From<&'a EditorApi<T>> for &
|
||||
}
|
||||
}
|
||||
|
||||
pub type WgpuSurface = Arc<SurfaceHandle<Surface>>;
|
||||
pub type WgpuWindow = Arc<SurfaceHandle<WindowHandle>>;
|
||||
|
||||
pub struct Surface {
|
||||
pub inner: wgpu::Surface<'static>,
|
||||
pub target_texture: Mutex<Option<TargetTexture>>,
|
||||
pub blitter: TextureBlitter,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TargetTexture {
|
||||
texture: wgpu::Texture,
|
||||
@@ -103,15 +91,6 @@ impl TargetTexture {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub type Window = web_sys::HtmlCanvasElement;
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub type Window = Arc<dyn winit::window::Window>;
|
||||
|
||||
unsafe impl StaticType for Surface {
|
||||
type Static = Surface;
|
||||
}
|
||||
|
||||
const VELLO_SURFACE_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||
|
||||
impl WgpuExecutor {
|
||||
@@ -160,29 +139,6 @@ impl WgpuExecutor {
|
||||
pub fn resample_texture(&self, source: &wgpu::Texture, target_size: UVec2, transform: &glam::DAffine2) -> wgpu::Texture {
|
||||
self.resampler.resample(&self.context, source, target_size, transform)
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub fn create_surface(&self, canvas: graphene_application_io::WasmSurfaceHandle) -> Result<SurfaceHandle<Surface>> {
|
||||
let surface = self.context.instance.create_surface(wgpu::SurfaceTarget::Canvas(canvas.surface))?;
|
||||
self.create_surface_inner(surface, canvas.window_id)
|
||||
}
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
pub fn create_surface(&self, window: SurfaceHandle<Window>) -> Result<SurfaceHandle<Surface>> {
|
||||
let surface = self.context.instance.create_surface(wgpu::SurfaceTarget::Window(Box::new(window.surface)))?;
|
||||
self.create_surface_inner(surface, window.window_id)
|
||||
}
|
||||
|
||||
pub fn create_surface_inner(&self, surface: wgpu::Surface<'static>, window_id: SurfaceId) -> Result<SurfaceHandle<Surface>> {
|
||||
let blitter = TextureBlitter::new(&self.context.device, VELLO_SURFACE_FORMAT);
|
||||
Ok(SurfaceHandle {
|
||||
window_id,
|
||||
surface: Surface {
|
||||
inner: surface,
|
||||
target_texture: Mutex::new(None),
|
||||
blitter,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WgpuExecutor {
|
||||
@@ -213,5 +169,3 @@ impl WgpuExecutor {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub type WindowHandle = Arc<SurfaceHandle<Window>>;
|
||||
|
||||
@@ -1271,7 +1271,7 @@ mod tests {
|
||||
fn test_async_node() {
|
||||
let attr = quote!(category("IO"));
|
||||
let input = quote!(
|
||||
async fn load_image(api: &WasmEditorApi, #[expose] path: String) -> Table<Raster<CPU>> {
|
||||
async fn load_image(api: &PlatformEditorApi, #[expose] path: String) -> Table<Raster<CPU>> {
|
||||
// Implementation details...
|
||||
}
|
||||
);
|
||||
@@ -1296,7 +1296,7 @@ mod tests {
|
||||
where_clause: None,
|
||||
input: Input {
|
||||
pat_ident: pat_ident("api"),
|
||||
ty: parse_quote!(&WasmEditorApi),
|
||||
ty: parse_quote!(&PlatformEditorApi),
|
||||
implementations: Punctuated::new(),
|
||||
context_features: vec![],
|
||||
},
|
||||
|
||||
@@ -7,9 +7,9 @@ authors = ["Graphite Authors <contact@graphite.art>"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[features]
|
||||
default = ["wasm", "wgpu"]
|
||||
default = ["wgpu"]
|
||||
gpu = []
|
||||
wgpu = ["gpu", "graph-craft/wgpu", "graphene-application-io/wgpu"]
|
||||
wgpu = ["gpu", "graph-craft/wgpu", "graphene-application-io/wgpu", "graphene-canvas-utils?/wgpu"]
|
||||
wasm = [
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
@@ -24,6 +24,7 @@ wasm = [
|
||||
"vector-nodes/wasm",
|
||||
"graphene-core/wasm",
|
||||
"graph-craft/wasm",
|
||||
"dep:graphene-canvas-utils"
|
||||
]
|
||||
image-compare = []
|
||||
vello = ["gpu"]
|
||||
@@ -61,6 +62,9 @@ image = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
wgpu = { workspace = true }
|
||||
|
||||
# Optional local dependencies
|
||||
graphene-canvas-utils = { workspace = true, optional = true }
|
||||
|
||||
# Optional workspace dependencies
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
wasm-bindgen-futures = { workspace = true, optional = true }
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
pub mod any;
|
||||
pub mod pixel_preview;
|
||||
pub mod platform_application_io;
|
||||
pub mod render_cache;
|
||||
pub mod render_node;
|
||||
pub mod text;
|
||||
#[cfg(feature = "wasm")]
|
||||
pub mod wasm_application_io;
|
||||
pub use blending_nodes;
|
||||
pub use brush_nodes as brush;
|
||||
pub use core_types::*;
|
||||
|
||||
@@ -2,16 +2,16 @@ use crate::render_node::RenderOutputType;
|
||||
use core_types::transform::{Footprint, Transform};
|
||||
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, OwnedContextImpl};
|
||||
use glam::{DAffine2, DVec2, UVec2};
|
||||
use graph_craft::application_io::PlatformEditorApi;
|
||||
use graph_craft::document::value::RenderOutput;
|
||||
use graph_craft::wasm_application_io::WasmEditorApi;
|
||||
use graphene_application_io::{ApplicationIo, ImageTexture};
|
||||
use graphene_application_io::ApplicationIo;
|
||||
use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams};
|
||||
use vector_types::vector::style::RenderMode;
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
pub async fn pixel_preview<'a: 'n>(
|
||||
ctx: impl Ctx + ExtractAll + CloneVarArgs + Sync,
|
||||
editor_api: &'a WasmEditorApi,
|
||||
editor_api: &'a PlatformEditorApi,
|
||||
data: impl Node<Context<'static>, Output = RenderOutput> + Send + Sync,
|
||||
) -> RenderOutput {
|
||||
let Some(render_params) = ctx.vararg(0).ok().and_then(|v| v.downcast_ref::<RenderParams>()).cloned() else {
|
||||
@@ -59,9 +59,9 @@ pub async fn pixel_preview<'a: 'n>(
|
||||
let transform = DAffine2::from_translation(-upstream_min) * footprint.transform.inverse() * DAffine2::from_scale(logical_resolution);
|
||||
|
||||
let exec = editor_api.application_io.as_ref().unwrap().gpu_executor().unwrap();
|
||||
let resampled = exec.resample_texture(&source_texture.texture, physical_resolution, &transform);
|
||||
let resampled = exec.resample_texture(source_texture.as_ref(), physical_resolution, &transform);
|
||||
|
||||
result.data = RenderOutputType::Texture(ImageTexture { texture: resampled.into() });
|
||||
result.data = RenderOutputType::Texture(resampled.into());
|
||||
|
||||
result
|
||||
.metadata
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#[cfg(target_family = "wasm")]
|
||||
use base64::Engine;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use canvas_utils::{Canvas, CanvasHandle};
|
||||
#[cfg(target_family = "wasm")]
|
||||
use core_types::WasmNotSend;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use core_types::math::bbox::Bbox;
|
||||
@@ -8,10 +10,12 @@ use core_types::table::Table;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use core_types::transform::Footprint;
|
||||
use core_types::{Color, Ctx};
|
||||
pub use graph_craft::application_io::*;
|
||||
pub use graph_craft::document::value::RenderOutputType;
|
||||
pub use graph_craft::wasm_application_io::*;
|
||||
use graphene_application_io::ApplicationIo;
|
||||
#[cfg(target_family = "wasm")]
|
||||
pub use graphene_canvas_utils as canvas_utils;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use graphic_types::Graphic;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use graphic_types::Vector;
|
||||
@@ -22,17 +26,6 @@ use graphic_types::vector_types::gradient::GradientStops;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use rendering::{Render, RenderParams, RenderSvgSegmentList, SvgRender};
|
||||
use std::sync::Arc;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use wasm_bindgen::JsCast;
|
||||
#[cfg(target_family = "wasm")]
|
||||
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement};
|
||||
|
||||
/// Allocates GPU memory and a rendering context for vector-to-raster conversion.
|
||||
#[cfg(feature = "wgpu")]
|
||||
#[node_macro::node(category(""))]
|
||||
async fn create_surface<'a: 'n>(_: impl Ctx, editor: &'a WasmEditorApi) -> Arc<WasmSurfaceHandle> {
|
||||
Arc::new(editor.application_io.as_ref().unwrap().create_window())
|
||||
}
|
||||
|
||||
fn parse_headers(headers: &str) -> reqwest::header::HeaderMap {
|
||||
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
|
||||
@@ -132,7 +125,7 @@ fn image_to_bytes(_: impl Ctx, image: Table<Raster<CPU>>) -> Vec<u8> {
|
||||
|
||||
/// 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"))]
|
||||
async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[scope("editor-api")] editor_resources: &'a WasmEditorApi, #[name("URL")] url: String) -> Arc<[u8]> {
|
||||
async fn load_resource<'a: 'n>(_: impl Ctx, _primary: (), #[scope("editor-api")] editor_resources: &'a PlatformEditorApi, #[name("URL")] url: String) -> Arc<[u8]> {
|
||||
let Some(api) = editor_resources.application_io.as_ref() else {
|
||||
return Arc::from(include_bytes!("../../../graph-craft/src/null.png").to_vec());
|
||||
};
|
||||
@@ -168,6 +161,12 @@ fn decode_image(_: impl Ctx, data: Arc<[u8]>) -> Table<Raster<CPU>> {
|
||||
Table::new_from_element(Raster::new_cpu(image))
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[node_macro::node(category(""))]
|
||||
async fn create_canvas(_: impl Ctx) -> CanvasHandle {
|
||||
CanvasHandle::new()
|
||||
}
|
||||
|
||||
/// Renders a view of the input graphic within an area defined by the *Footprint*.
|
||||
#[cfg(target_family = "wasm")]
|
||||
#[node_macro::node(category(""))]
|
||||
@@ -182,7 +181,7 @@ async fn rasterize<T: WasmNotSend + 'n>(
|
||||
)]
|
||||
mut data: Table<T>,
|
||||
footprint: Footprint,
|
||||
surface_handle: Arc<graphene_application_io::SurfaceHandle<HtmlCanvasElement>>,
|
||||
mut canvas: CanvasHandle,
|
||||
) -> Table<Raster<CPU>>
|
||||
where
|
||||
Table<T>: Render,
|
||||
@@ -211,11 +210,8 @@ where
|
||||
render.format_svg(glam::DVec2::ZERO, size);
|
||||
let svg_string = render.svg.to_svg_string();
|
||||
|
||||
let canvas = &surface_handle.surface;
|
||||
canvas.set_width(resolution.x);
|
||||
canvas.set_height(resolution.y);
|
||||
|
||||
let context = canvas.get_context("2d").unwrap().unwrap().dyn_into::<CanvasRenderingContext2d>().unwrap();
|
||||
canvas.set_resolution(resolution);
|
||||
let context = canvas.context();
|
||||
|
||||
let preamble = "data:image/svg+xml;base64,";
|
||||
let mut base64_string = String::with_capacity(preamble.len() + svg_string.len() * 4);
|
||||
@@ -4,9 +4,9 @@ use core_types::math::bbox::AxisAlignedBbox;
|
||||
use core_types::transform::{Footprint, RenderQuality, Transform};
|
||||
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractAnimationTime, ExtractPointerPosition, ExtractRealTime, OwnedContextImpl};
|
||||
use glam::{DAffine2, DVec2, IVec2, UVec2};
|
||||
use graph_craft::application_io::PlatformEditorApi;
|
||||
use graph_craft::document::value::RenderOutput;
|
||||
use graph_craft::wasm_application_io::WasmEditorApi;
|
||||
use graphene_application_io::{ApplicationIo, ImageTexture};
|
||||
use graphene_application_io::ApplicationIo;
|
||||
use rendering::{RenderOutputType as RenderOutputTypeRequest, RenderParams};
|
||||
use std::collections::HashSet;
|
||||
use std::hash::Hash;
|
||||
@@ -374,7 +374,7 @@ fn flood_fill(start: &TileCoord, tile_set: &HashSet<TileCoord>, visited: &mut Ha
|
||||
#[node_macro::node(category(""))]
|
||||
pub async fn render_output_cache<'a: 'n>(
|
||||
ctx: impl Ctx + ExtractAll + CloneVarArgs + ExtractRealTime + ExtractAnimationTime + ExtractPointerPosition + Sync,
|
||||
editor_api: &'a WasmEditorApi,
|
||||
editor_api: &'a PlatformEditorApi,
|
||||
data: impl Node<Context<'static>, Output = RenderOutput> + Send + Sync,
|
||||
#[data] tile_cache: TileCache,
|
||||
) -> RenderOutput {
|
||||
@@ -460,7 +460,7 @@ pub async fn render_output_cache<'a: 'n>(
|
||||
let combined_metadata = composite_cached_regions(&all_regions, output_texture.as_ref(), &device_origin_offset, &footprint.transform, exec);
|
||||
|
||||
RenderOutput {
|
||||
data: RenderOutputType::Texture(ImageTexture { texture: output_texture }),
|
||||
data: RenderOutputType::Texture(output_texture.into()),
|
||||
metadata: combined_metadata,
|
||||
}
|
||||
}
|
||||
@@ -506,7 +506,7 @@ where
|
||||
let memory_size = (region_pixel_size.x * region_pixel_size.y) as usize * BYTES_PER_PIXEL;
|
||||
|
||||
CachedRegion {
|
||||
texture: rendered_texture.texture.as_ref().clone(),
|
||||
texture: rendered_texture.as_ref().clone(),
|
||||
texture_size: region_pixel_size,
|
||||
tiles: region.tiles.clone(),
|
||||
metadata: result.metadata,
|
||||
@@ -558,7 +558,7 @@ fn composite_cached_regions(
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
},
|
||||
wgpu::TexelCopyTextureInfo {
|
||||
texture: &output_texture,
|
||||
texture: output_texture,
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d { x: dst_x, y: dst_y, z: 0 },
|
||||
aspect: wgpu::TextureAspect::All,
|
||||
|
||||
@@ -2,10 +2,10 @@ use core_types::table::Table;
|
||||
use core_types::transform::{Footprint, Transform};
|
||||
use core_types::{CloneVarArgs, ExtractAll, ExtractVarArgs};
|
||||
use core_types::{Color, Context, Ctx, ExtractFootprint, OwnedContextImpl, WasmNotSend};
|
||||
pub use graph_craft::application_io::*;
|
||||
use graph_craft::document::value::RenderOutput;
|
||||
pub use graph_craft::document::value::RenderOutputType;
|
||||
pub use graph_craft::wasm_application_io::*;
|
||||
use graphene_application_io::{ApplicationIo, ExportFormat, ImageTexture, RenderConfig};
|
||||
use graphene_application_io::{ApplicationIo, ExportFormat, RenderConfig};
|
||||
use graphic_types::raster_types::Image;
|
||||
use graphic_types::raster_types::{CPU, Raster};
|
||||
use graphic_types::{Artboard, Graphic, Vector};
|
||||
@@ -124,7 +124,7 @@ async fn create_context<'a: 'n>(
|
||||
}
|
||||
|
||||
#[node_macro::node(category(""))]
|
||||
async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, editor_api: &'a WasmEditorApi, data: RenderIntermediate) -> RenderOutput {
|
||||
async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, editor_api: &'a PlatformEditorApi, data: RenderIntermediate) -> RenderOutput {
|
||||
let footprint = ctx.footprint();
|
||||
let render_params = ctx
|
||||
.vararg(0)
|
||||
@@ -202,7 +202,7 @@ async fn render<'a: 'n>(ctx: impl Ctx + ExtractFootprint + ExtractVarArgs, edito
|
||||
.expect("Failed to render Vello scene"),
|
||||
);
|
||||
|
||||
RenderOutputType::Texture(ImageTexture { texture })
|
||||
RenderOutputType::Texture(texture.into())
|
||||
}
|
||||
_ => unreachable!("Render node did not receive its requested data type"),
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use core_types::{Ctx, table::Table};
|
||||
use graph_craft::wasm_application_io::WasmEditorApi;
|
||||
use graph_craft::application_io::PlatformEditorApi;
|
||||
use graphic_types::Vector;
|
||||
pub use text_nodes::*;
|
||||
|
||||
@@ -9,7 +9,7 @@ fn text<'i: 'n>(
|
||||
_: impl Ctx,
|
||||
/// The Graphite editor's source for global font resources.
|
||||
#[scope("editor-api")]
|
||||
editor_resources: &'i WasmEditorApi,
|
||||
editor_resources: &'i PlatformEditorApi,
|
||||
/// The text content to be drawn.
|
||||
#[widget(ParsedWidgetOverride::Custom = "text_area")]
|
||||
#[default("Lorem ipsum")]
|
||||
|
||||
Reference in New Issue
Block a user