mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-17 00:13:17 +00:00
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:
@@ -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
@@ -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;
|
||||
|
||||
@@ -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:?}")),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user