From 520b8cd2c50277d3c39d901c13029294aafc43a1 Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Thu, 23 Oct 2025 19:20:50 +0200 Subject: [PATCH] separated code into extensions --- lib/AsyncEmitter.js | 46 +++++++++++++++++++++++++++++ lib/widget.js | 43 +++++++-------------------- lib/widget/play.js | 34 ++++++++++++++++++++++ lib/widget/thumb.js | 26 +++++++++++++++++ lib/xrforge/index.js | 69 ++++++++++++++++++++++++++++++++++---------- package.json | 8 ++++- shell.nix | 1 - 7 files changed, 177 insertions(+), 50 deletions(-) create mode 100644 lib/AsyncEmitter.js create mode 100644 lib/widget/play.js create mode 100644 lib/widget/thumb.js diff --git a/lib/AsyncEmitter.js b/lib/AsyncEmitter.js new file mode 100644 index 0000000..8890e47 --- /dev/null +++ b/lib/AsyncEmitter.js @@ -0,0 +1,46 @@ +/* + * async Event Emitter + * + * // Example usage + * const emitter = new AsyncEmitter(); + * + * // Register some async listeners + * emitter.on("data", async (msg) => { + * console.log("Listener 1 starting..."); + * await new Promise((r) => setTimeout(r, 1000)); + * console.log("Listener 1 done:", msg); + * }); + * + * emitter.on("data", async (msg) => { + * console.log("Listener 2 starting..."); + * await new Promise((r) => setTimeout(r, 500)); + * console.log("Listener 2 done:", msg); + * }); + * + * (async () => { + * console.log("Emitting..."); + * await emitter.emit("data", "Hello async world!"); + * console.log("All listeners finished."); + * })(); + */ + +class AsyncEmitter { + constructor() { + this.listeners = new Map(); + } + + on(event, listener) { + if (!this.listeners.has(event)) this.listeners.set(event, []); + this.listeners.get(event).push( listener ); + } + + async emit(event, ...args) { + const listeners = this.listeners.get(event) || []; + if( document.location.hostname == 'localhost' ) console.info(`emit(${event})`) + for (const fn of listeners) { + await fn(...args); // <-- Waits for each listener to finish + } + } +} + +export {AsyncEmitter} diff --git a/lib/widget.js b/lib/widget.js index 040f9bc..1da7e47 100644 --- a/lib/widget.js +++ b/lib/widget.js @@ -1,42 +1,21 @@ +import {AsyncEmitter} from "./AsyncEmitter.js" function widget(){ - return new Proxy({ + const me = new AsyncEmitter() + me.player = document.querySelector("#player"), + me.src = document.location.search.substr(1), + me.ext = {} - backend: null, - player: document.querySelector("#player"), - src: document.location.search.substr(1), - ext: {}, + return new Proxy( me, { + get(me,k){ return me[k] }, - init(opts){ - for( var i in opts) this[i] = opts[i] - }, - - play(){ - // set URL - player.setAttribute("gltf-model", `url(${widget.src})` ) - - document.querySelector("#btn_play").style.display = 'none' - - let script = document.createElement("script") - script.src = "https://aframe.io/releases/1.7.0/aframe.min.js" - document.head.appendChild(script) - - script = document.createElement("script") - script.src = "backend.xrforge.js" - document.head.appendChild(script) + set(me,k,v){ + me[k] = v + return true } - }, - { - get(me,k){ return me[k] }, - - set(me,k,v){ - me[k] = v - return true - } - - }) + }) } export {widget} diff --git a/lib/widget/play.js b/lib/widget/play.js new file mode 100644 index 0000000..311807d --- /dev/null +++ b/lib/widget/play.js @@ -0,0 +1,34 @@ + +export default function extension(widget){ + + const ext = { + + async init(){ + + // let other extensions set this.src if needed + await widget.emit("init.play", this) + + if( widget.src ){ + document.querySelector('#scene').setAttribute("gltf-model",`url(${widget.src})`) + } + + this.initButton() + }, + + initButton(){ + const btn_play = document.querySelector("#btn_play") + btn_play.addEventListener("click", async () => { + btn_play.style.display = 'none' // hide button + widget.emit("play") + }) + } + + } + + widget.ext.play = ext + + // register async listeners + widget.on("init", ext.init.bind(ext) ) + +} + diff --git a/lib/widget/thumb.js b/lib/widget/thumb.js new file mode 100644 index 0000000..ab04ac1 --- /dev/null +++ b/lib/widget/thumb.js @@ -0,0 +1,26 @@ + +export default function extension(widget){ + + const ext = { + + src: "", + + init: async () => { + + // let other extensions set this.src if needed + await widget.emit("init.thumb", widget.ext.thumb ) + + if( widget.ext.thumb.src ){ + document.body.style.background = `url(${widget.ext.thumb.src}) no-repeat center / cover` + } + } + + } + + widget.ext.thumb = ext + + // register async listeners + widget.on("init", ext.init.bind(ext) ) + +} + diff --git a/lib/xrforge/index.js b/lib/xrforge/index.js index a3b1e0a..e200952 100644 --- a/lib/xrforge/index.js +++ b/lib/xrforge/index.js @@ -1,24 +1,61 @@ -import {widget} from './../widget.js' +import {widget as Widget} from './../widget.js' import {inferSource} from './util.js' -const btn_play = document.querySelector("#btn_play") -btn_play.addEventListener("click", () => { - btn_play.style.display = 'none' // hide button - import('aframe') -}) +// extensions +import {default as thumb} from './../widget/thumb.js' +import {default as play} from './../widget/play.js' -let src = document.location.search.substr(1) -let cover = src -const img = /\.(png|jpg|webp)/ +const widget = Widget() + +// init extensions +thumb(widget) +play(widget) + +// init xrforge extension +widget.ext.xrforge = { -// manyfold URLs need to be de-obfuscated -if( src.match(img) ){ - cover = await inferSource(src) - src = cover.replace(img,".glb") } -console.dir({cover,src}) +// in case of Manyfold backend, a thumbnail is passed (not gltf src e.g.) +// so we will infer the source-url based on the thumbnail +widget.on('init.thumb', async (thumb) => { + let src = widget.src + const img = /\.(png|jpg|webp)/ -document.querySelector('#scene').setAttribute("gltf-model",`url(${src})`) -document.body.style.background = `url(${cover}) no-repeat center / cover` + if( src.match(img) ){ + thumb.src = src + let inferredSrc = await inferSource(src) + widget.src = inferredSrc.replace(img,".glb") + } +}) + +widget.on("play", async () => { + // initialize a specialized build of THREE/AFRAME + const AFRAME = await import('aframe') + window.THREE.DRACOLoader = await import('three/examples/jsm/loaders/DRACOLoader.js') + window.THREE.FBXLoader = await import('three/examples/jsm/loaders/FBXLoader.js') + window.THREE.USDZLoader = await import('three/examples/jsm/loaders/USDZLoader.js') + window.THREE.ColladaLoader = await import('three/examples/jsm/loaders/ColladaLoader.js') + window.THREE.MTLLoader = await import('three/examples/jsm/loaders/MTLLoader.js') + window.THREE.GLTFExporter = await import('three/examples/jsm/exporters/GLTFExporter.js') + // optional utils + window.zipjs = await import("@zip.js/zip.js") + window.webdav = await import("webdav") + window.rs = await import("remotestoragejs") + window.trystero = await import("trystero") + + // include xrsh (remote for now, until the final integration is more clear) + const script = document.createElement("script") + script.src = "https://xrsh.isvery.ninja/xrsh.js" + script.addEventListener("load", function(){ + const ent = document.createElement("a-entity") + ent.setAttribute("isoterminal","minimized:true") + ent.setAttribute("position","0 1.6 -0.3") + document.querySelector("a-scene").appendChild(ent) + }) + document.body.appendChild(script) +}) + +await widget.emit("init") +console.dir(widget) diff --git a/package.json b/package.json index ebb274c..fdd0e7f 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,13 @@ "license": "", "description": "", "dependencies": { + "@needle-tools/three-animation-pointer": "^1.0.7", + "@zip.js/zip.js": "^2.8.8", "aframe": "^1.7.1", - "xrf": "^0.0.1" + "remotestoragejs": "^2.0.0-beta.8", + "trystero": "^0.22.0", + "webdav": "^5.8.0", + "xrf": "^0.0.1", + "xrsh": "^0.0.5" } } diff --git a/shell.nix b/shell.nix index 94a7d7a..6a800f6 100644 --- a/shell.nix +++ b/shell.nix @@ -11,7 +11,6 @@ # nativeBuildInputs is usually what you want -- tools you need to run nativeBuildInputs = with pkgs.buildPackages; [ - nodejs_20 monolith bun