Single-object viewer POC (#14)

This commit implements a Sketchfab-style single-object viewer as a minimal
proof-of-concept.

### `@storyteller/studio-web`
This is a new package to house web components for interfacing with the
`@storyteller/studio` WASM library. It currently includes a single component,
`<sts-viewer />`, which is exported from `@storyteller/studio-web/viewer`.

### `react-frontend`
This is a new application intended as a proof-of-concept and testbed for React
interop with `@storyteller/studio-web`.

Web components are currently a little bit fiddly to use in a React application.
`react-frontend/src/app/studio-viewer.tsx` demonstrates how a React-idiomatic
component can be written as a thin wrapper around a web component, and
`react-frontend/src/app/app.tsx` demonstrates that wrapper component in use.
This commit is contained in:
Danny McGee
2023-12-07 15:46:32 -05:00
committed by GitHub
parent 155272a5ba
commit cba70a62ae
70 changed files with 71793 additions and 1157 deletions
+6
View File
@@ -132,3 +132,9 @@ where
}
}
}
impl AsJsValue for u32 {
fn as_js(&self) -> Result<JsValue, String> {
Ok(JsValue::from_f64(*self as f64))
}
}
+18 -77
View File
@@ -1,90 +1,31 @@
use std::{cell::RefCell, sync::OnceLock};
use bevy::window::Window;
use parking_lot::ReentrantMutex;
use web_sys::{CustomEvent, CustomEventInit, Element};
use wasm_bindgen::prelude::wasm_bindgen;
mod conv;
mod util;
pub use conv::*;
pub(crate) use util::*;
pub(crate) type WasmMessageBuffer<T> = OnceLock<ReentrantMutex<RefCell<Option<T>>>>;
static INITIALIZED: OnceLock<ReentrantMutex<RefCell<bool>>> = OnceLock::new();
pub(crate) fn dispatch_to_js(
window: &Window,
event_type: &str,
// NOTE: We use dynamic dispatch here to keep the wasm binary small(-ish)
value: &dyn AsJsValue,
) -> Result<(), String> {
let canvas = try_get_canvas(window)?;
/// The Studio application must only be initialized once (via `init` or
/// `startViewer`). Multiple calls to either of those functions will cause the
/// application to panic. This function can be used to query whether the app has
/// already been initialized.
#[wasm_bindgen(js_name = isInitialized)]
pub fn is_initialized() -> bool {
let wrapper = INITIALIZED.get_or_init(|| ReentrantMutex::new(RefCell::new(false)));
let guard = wrapper.lock();
let result = *guard.borrow();
let detail = value.as_js()?;
let event = CustomEvent::new_with_event_init_dict(
event_type,
CustomEventInit::new()
.bubbles(true)
.cancelable(true)
.detail(&detail),
)
.map_err(|err| format!("{err:?}"))?;
canvas
.dispatch_event(&event)
.map_err(|err| format!("{err:?}"))?;
Ok(())
result
}
fn try_get_canvas(window: &Window) -> Result<Element, String> {
let canvas_selector = window
.canvas
.as_ref()
.ok_or("Canvas selector not set on Bevy Window".to_string())?;
let window = web_sys::window().ok_or("web_sys::window() returned None".to_string())?;
let document = window
.document()
.ok_or("window.document() returned None".to_string())?;
match document.query_selector(canvas_selector) {
Ok(Some(canvas)) => Ok(canvas),
Ok(None) => Err(format!(
"Failed to find element with selector '{canvas_selector}'"
)),
Err(err) => Err(format!("JavaScript threw an Error: {err:?}")),
}
pub(crate) fn initialize() {
let wrapper = INITIALIZED.get_or_init(|| ReentrantMutex::new(RefCell::new(false)));
let guard = wrapper.lock();
*guard.borrow_mut() = true;
}
macro_rules! get_buffer {
($buffer:ident) => {
$buffer.get_or_init(|| {
::parking_lot::ReentrantMutex::new(::std::cell::RefCell::new(
::core::option::Option::None,
))
})
};
}
macro_rules! write_message {
($buffer:ident, $expr:expr) => {{
let buffer = crate::wasm::get_buffer!($buffer);
if let Some(lock) = buffer.try_lock() {
*lock.borrow_mut() = Some($expr);
}
}};
}
macro_rules! read_buffer {
($buffer:ident, |$msg:ident| $stmt:stmt) => {{
let buffer = crate::wasm::get_buffer!($buffer);
if let Some(lock) = buffer.try_lock() {
if let Some($msg) = lock.borrow_mut().take() {
$stmt
}
}
}};
}
pub(crate) use get_buffer;
pub(crate) use read_buffer;
pub(crate) use write_message;
+85
View File
@@ -0,0 +1,85 @@
use std::{cell::RefCell, sync::OnceLock};
use bevy::window::Window;
use parking_lot::ReentrantMutex;
use web_sys::{CustomEvent, CustomEventInit, Element};
use super::AsJsValue;
pub(crate) type WasmMessageBuffer<T> = OnceLock<ReentrantMutex<RefCell<Option<T>>>>;
pub(crate) trait StaticBuffer<T> {
type Inner;
fn inner(&self) -> &Self::Inner;
fn write_message(&self, value: T);
fn take_message(&self) -> Option<T>;
}
impl<T> StaticBuffer<T> for WasmMessageBuffer<T> {
type Inner = ReentrantMutex<RefCell<Option<T>>>;
#[inline]
fn inner(&self) -> &Self::Inner {
self.get_or_init(|| ReentrantMutex::new(RefCell::new(None)))
}
#[inline]
fn write_message(&self, value: T) {
let mutex = self.inner();
if let Some(guard) = mutex.try_lock() {
*guard.borrow_mut() = Some(value);
}
}
#[inline]
fn take_message(&self) -> Option<T> {
let mutex = self.inner();
mutex.try_lock().and_then(|guard| guard.borrow_mut().take())
}
}
pub(crate) fn dispatch_to_js(
window: &Window,
event_type: &str,
// NOTE: We use dynamic dispatch here to keep the wasm binary small(-ish)
value: &dyn AsJsValue,
) -> Result<(), String> {
let canvas = try_get_canvas(window)?;
let detail = value.as_js()?;
let event = CustomEvent::new_with_event_init_dict(
event_type,
CustomEventInit::new()
.bubbles(true)
.cancelable(true)
.detail(&detail),
)
.map_err(|err| format!("{err:?}"))?;
canvas
.dispatch_event(&event)
.map_err(|err| format!("{err:?}"))?;
Ok(())
}
fn try_get_canvas(window: &Window) -> Result<Element, String> {
let canvas_selector = window
.canvas
.as_ref()
.ok_or("Canvas selector not set on Bevy Window".to_string())?;
let window = web_sys::window().ok_or("web_sys::window() returned None".to_string())?;
let document = window
.document()
.ok_or("window.document() returned None".to_string())?;
match document.query_selector(canvas_selector) {
Ok(Some(canvas)) => Ok(canvas),
Ok(None) => Err(format!(
"Failed to find element with selector '{canvas_selector}'"
)),
Err(err) => Err(format!("JavaScript threw an Error: {err:?}")),
}
}