From 257b9f56e1903017addd2d2baed7c8dddfe902d0 Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Tue, 11 Aug 2026 17:47:40 +0200 Subject: [PATCH] show fediportals in dir.html + convert plugin now works with X3D --- shell.nix | 2 +- src/.init.lua | 1 + src/css/xrforge.css | 5 +- src/img/button-88x31.gif | Bin 0 -> 7033 bytes src/myverse/x3d/test_world.x3d | 52 +++++++++++++++++ src/myverse/x3d/test_world.x3d.glb | Bin 0 -> 1724 bytes src/page/about.html | 45 +++++---------- src/page/dir.html | 23 ++++++++ src/page/dir.lua | 27 +++++---- src/page/footer.html | 4 +- src/page/janusxr.html | 30 ++++++---- src/plugin/convert.lua | 88 +++++++++++++++++++++++++++++ src/plugin/global.lua | 3 +- src/plugin/janusxr.lua | 67 ++++++++++++++++------ src/plugin/xrfragments.lua | 3 +- 15 files changed, 272 insertions(+), 78 deletions(-) create mode 100644 src/img/button-88x31.gif create mode 100644 src/myverse/x3d/test_world.x3d create mode 100644 src/myverse/x3d/test_world.x3d.glb create mode 100644 src/plugin/convert.lua diff --git a/shell.nix b/shell.nix index 55b57ca..5a6aec9 100644 --- a/shell.nix +++ b/shell.nix @@ -10,7 +10,7 @@ foo = pkgs.mkShell { # nativeBuildInputs is usually what you want -- tools you need to run nativeBuildInputs = with pkgs.buildPackages; [ - + assimp ]; shellHooks = '' diff --git a/src/.init.lua b/src/.init.lua index 7616493..f0b687c 100644 --- a/src/.init.lua +++ b/src/.init.lua @@ -57,6 +57,7 @@ app.opts = require('config')(app) -- load system plugins (in specific order for admin panel) app.plugin.global = app.addPlugin('plugin/global') app.plugin.diskscan = app.addPlugin('plugin/diskscan') +app.plugin.convert = app.addPlugin('plugin/convert') app.plugin.moderation = app.addPlugin('plugin/moderation') app.plugin.xrfragments = app.addPlugin('plugin/xrfragments') app.plugin.janusxr = app.addPlugin('plugin/janusxr') diff --git a/src/css/xrforge.css b/src/css/xrforge.css index f30721e..e6e0e93 100644 --- a/src/css/xrforge.css +++ b/src/css/xrforge.css @@ -278,11 +278,10 @@ img.icon{ display:none; } padding:10px 20px; overflow:clip; } -.info .tag{ - margin-bottom:7px; -} b.tag{ + margin-bottom:7px; + line-height:22px; display:inline-block; background: #77f; color: white; diff --git a/src/img/button-88x31.gif b/src/img/button-88x31.gif new file mode 100644 index 0000000000000000000000000000000000000000..e060d3505e22a840e8bf3b5e3386efb389f6d582 GIT binary patch literal 7033 zcmeHLUrbxq89%$Qzwg{@ z7sAqZZHOiaKcf<785GYn=}%;3xrG}ls@WiZQP7H5_qQ?4>@Fm5r<85i)xgvx}$gvA7B0uS&F z*YE{n%oc=0KSNb5RpqKG2o_IMhHtorFG0%DXFTJ_ElZG) zh!3!M;@si_x*}Y#z(-4POF%~`4FSLl(;+mXg_O_(G+_-nzzG2(La>H2kcK5_fd_!$ z8?FVVh-Tw6p79gN7=%Q8fW;FRf)XgxPb_3cDW5V)L`v#T>6KCvN2v~FTLw&&Wek*j zNsA(oNN|mEK$ajLVvHEE2S}SeK=8JPgc+SzxSh|_cARX z)ZHjgHK;iUo=7WAeq#ULJ4Yzg{|EtHI!zXP#L0?;Xe{`{C*k?`ExmJ3kF*c=?D&p^ zI5=+HvLW7?rcYM;Rp}Xn<#f*z^37oR$O}#s$0LX$rjb&@8}P&`nU857yAVboe6oG8 z>^N!}zYF8`%auUR6~+xiA>UiPo=4b8ZCZwq0z3gHTQ%! zZ~zdWe|mG}0w|_?=5{$@?D*R%ng8R?^i7BKbj!+x!wE1XKg|U3sf5UFf&k}hxzWFz zYTW?Qoah(@MBV)1bMXyPR3QlcusBDPcGk~37~aWd`DP0Ss&w+$wcORqr~cVzx_!Z{ z3?m%t7*#&ox%x!nT-@rJd%A%Vez13rukJFjZ(aZ7;-NPV!=duoP1M`D**x(Jgu6S^ z09`$cQ2Tev)t8C?*%yR}fLJu&UjO^zF9vf6SVn1ke3-LPV?)^W z4`!wk8hjTReP3Yo)kY-y$MqBEJE!e1fDtB)w(-a2qgX%J-DSj+`E9LxWbn!viBTN$ z(rN<6E0}{YfB}lJ+rbZ3*2`xbS~rA25DMz{xj$gXlG(uIN1MYj_7)&cxA#J5-PKfv zWbqbt@r50eGD1=cH1adcW3RO*QU-S*wEnh{)1@hZUYcq9zFY{Vve+1BnB4!@Q@^W1 zc=yucUwV6;1;WDK2H;V>{qg1DL11(nd})|;rB7|+5~KfUQ9<1zcru%$Y%C2o5k|0r zq3l_b7_f>5*Vx_!Hu)i$u~=WkhvgFpr*i$5hi~{Nn^E?chtF3a+|vjn2MBQW5XA1t zMu@0l(&h?uC|g+f7aCGQK%mGwGP8Mod90Hmt0-w&S*S8VltbZI-=(GD8(o1g3R7?U zV+m!O19|13$uN1Q?L(n72v;hJYv`7ocMUU%Oc7=V$CHwes1yKSYcF~3hkL%?#@|uA212aiQH!xcJO&V2L7liD6sm>FO&OAD5UK-Fcv2Qu{`KNSD4J9OQJ8q93IXYZ z$Wuh-5S3_K?TMRJ2vm^!u1f*bWY5|V&V7Ns$pi!*D2|238dPn;2UM1XnO+>&A=9f% zrR_YI&qyGJQM%zaaFxM6aO0(W*IICfI zqzC~qPWT*v{v@^$KS!EAFM3Y&NFOgC-C{B2#XT8*jxMJD@ zwv3RIW?3eRN03J+45n96X#g#8d?mzpt0lB);Ct9BJnR)hwDgbvZ}$q(Z`3PfwNYB}w5&by9)I6^ zg+MZ^T|G)W@wey|@^t6AXRkmI=oM(!r)9nky#h`TZ2EA{Ft}g6LUdEFK)bEeD=5I& zmR>=!cX|at;JDw0UV+%lUZLI&V_SL!uzwxrfXoICgsU$21w)@)a=V1^#{3g5g} zh${E&6{1PHG2X{sA>>@FoJ+P2*SHGb_g+CmtG&4$aqfN(qE|>h>=i04&;Mhu@RNT7 D5 + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/myverse/x3d/test_world.x3d.glb b/src/myverse/x3d/test_world.x3d.glb new file mode 100644 index 0000000000000000000000000000000000000000..80977012960f120c091a367f648b608c67986439 GIT binary patch literal 1724 zcma)7>28}q5TH zeS>yBj2$~^tE@E4F!#;@qx5wDsbLu3UmC`@7l!fSq;ouFQCX^rg;RE=beR-63z@Ju zGucSxN=H?piPIUYytNI@)8ula7aF=OF%BacXWOw^; zySvP!y^PNuq_?oS?3Bf}+=E;sJ$BUMFdn#A2=GP=yEn;5%(RI1$~E%bgA*TZ(s zE!PAzErIXGaG^qzW-`6-a>oqY*yDvm6vMXsxcD+j2U=kv?0V=Mv)xHw^-jCn$0z%p?omCy6d1d-;bL#7NjSFc3NEB4d?ygn zc73YWzXksl!b}7{c@skVZI3q6mL>(O1=I;HMBoJQVQvUZa)&!@A$->pfe>n=jghL4 zOM~Kgj??zZd_lKe8E{WR4;}y*g9cOORw^MLR3aIcYt>j$maEDmn%i;jXs?I*p=vY_ zXb$-=WBTMat!bJx{{++mD7Ajk_j~8yn4VwXHSl8vgT4>Yt95?<{sMXn`1PE(wW7bE zYaI$;JR`vW0tfgB!E*xobr|#uscGO>1UQgGh64;;@L&r)(7=#^2mi1I4GbB0IIsl` G4A~#h#@i(T literal 0 HcmV?d00001 diff --git a/src/page/about.html b/src/page/about.html index 2d03b52..01be761 100644 --- a/src/page/about.html +++ b/src/page/about.html @@ -95,41 +95,22 @@ div#xrecosystem{

What is it

An open, seamless XR web built from simple URLs and directories, without gatekeepers.
- XRForge transforms standard local or remote file directories into interconnected 3D spaces. Users can seamlessly "surf" spatial webs (plural!), turning everyday directories into 3D portals. + XRForge transforms standard local or remote file directories into interconnected 3D spaces. Users can seamlessly "surf" spatial webs (plural!), turning everyday directories into 3D portals.
+ +

-
-
-
-    ┌───────────────────────────────────┐   ┌──────────────────────────────────────────┐
-    │           local disk              │   │          Surf via 3D portals             │
-    ├───────────────────────────────────┤   └──────────────────────────────────────────┘
-    │                                   │
-    │  /myverse                         │    https://my.org/myverse
-    │    /elearnings                    │    https://my.org/myverse/elearnings
-    │       /elearning1/1.glb           │    https://my.org/myverse/elearning/elearning1
-    │       /elearning2/2.glb           │    https://my.org/myverse/elearning/elearning2
-    │                                   │
-    │  ┌──────────────────────────────┐ │
-    │  │ github / webdav / dropbox etc│ │
-    │  │                              │ │
-    │  │ /mygames                     │ │    https://my.org/myverse/mygames
-    │  │   /biggusgateWebXR           │ │    https://my.org/myverse/mygames/biggusgate
-    │  │   /spasca                    │ │    https://my.org/myverse/mygames/spasca
-    │  │ /otherxrforge                │ │    https://my.org/myverse/otherxrforge
-    │  │                              │ │
-    │  │  ┌───────────┐ ┌───────────┐ │ │
-    │  │  │/manyfold  │ │archive.org│ │ │    https://my.org/myverse/manyfold
-    │  │  └───────────┘ └───────────┘ │ │    https://my.org/myverse/myarchive
-    │  │                              │ │
-    │  │ +40 integrations!            │ │
-    │  └──────────────────────────────┘ │
-    └───────────────────────────────────┘ 
-
-    
-
-

Why people want XRForge

diff --git a/src/page/dir.html b/src/page/dir.html index 612994c..40238a3 100644 --- a/src/page/dir.html +++ b/src/page/dir.html @@ -37,6 +37,29 @@ end} ]]) end end} + {if parent.parent == nil then + for i=1,5 do + local urls = app.opts[ 'plugin_federation_url_' .. i] + if #urls > 0 then + local title = urls:gsub(' .*','') + local fediurl = '/?url=' .. EscapeFragment(title) + local thumburl = urls:gsub('.* ','') + local icon = 'list-tree' + local thumbnail = '' + if #thumburl > 0 then thumbnail = 'style="background-image: url(' .. EscapeFragment(thumburl) .. ');"' end + print([[ + + ]]) + end + end + end}
${footer} diff --git a/src/page/dir.lua b/src/page/dir.lua index fe7483d..132d2bd 100644 --- a/src/page/dir.lua +++ b/src/page/dir.lua @@ -16,7 +16,8 @@ return { if urlpath == "/" or urlpath:match("^"..rootpath) - and not urlpath:gsub(".*/",""):match("%.") then + and not urlpath:gsub(".*/",""):match("%.") + and not req.param.url then local redirect = false if not app.cache.url[ urlhash ] or util.count(req.params) > 0 then @@ -25,13 +26,14 @@ return { foreach( app.cache.items, function(k,v) if req.authenticated or app.plugin.moderation.isAllowed(v) then if v.parent then - --trace( "path = " .. util.stripPath(v.path) .. " " .. - -- "parent = " .. util.stripPath(v.parent.path) .. " " .. - -- "matchpath = " .. util.stripPath(matchpath) - --) + trace( "path = " .. util.stripPath(v.path) .. " " .. + "parent = " .. util.stripPath(v.parent.path) .. " " .. + "matchpath = " .. util.stripPath(matchpath) + ) if match(v.path, matchpath) then -- show parent folder if any local parent = app.cache.items[ v.parent.id ] if v.portal then + trace("-- portal redirect") redirect = v.portal.url end if match(parent.path, app.opts.plugin_diskscan_dir) then -- add rootfolder if needed @@ -41,15 +43,20 @@ return { end -- add items from dir - if match(v.parent.path, matchpath) and (v.dir or v.view3D) then - local parent = app.cache.items[ v.parent.id ] - res.items[ v.id ] = v - res.parent = parent + if match(v.parent.path, matchpath) then + if v.dir or v.view3D then + trace("-- dir item: " .. v.path ) + local parent = app.cache.items[ v.parent.id ] + res.items[ v.id ] = v + res.parent = parent + end end end end end) + local data = util.merge({ + url = req.param.url, -- query hint to render janus.url in iframe (index.html) html_items='', items = {}, parent=res.parent @@ -66,7 +73,7 @@ return { local port = ':' .. GetPort() if port == 80 then port = '' end data.url = GetScheme() .. '://' .. GetHost() .. port .. v.url - if v.janusxr then + if v.janusxr and v.janusxr.jml then -- what to do here? no generated portals? data.janusxr = app.tpl( util.readfile(v.path), data ) end diff --git a/src/page/footer.html b/src/page/footer.html index 6699b77..ad6f19b 100644 --- a/src/page/footer.html +++ b/src/page/footer.html @@ -8,7 +8,6 @@

- diff --git a/src/page/janusxr.html b/src/page/janusxr.html index 99f5dfc..49fb8eb 100644 --- a/src/page/janusxr.html +++ b/src/page/janusxr.html @@ -9,7 +9,9 @@ end} {if items and #items > 0 then for i,v in pairs(items) do - print('\t') + if #v.thumbnail > 0 then + print('\t') + end end end} {if showfediportals then @@ -30,17 +32,20 @@ end} ]]) else local room = app.opts.plugin_janusxr_portalroom + if item and item.janusxr and item.janusxr.room then + room = item.janusxr.room + end if room == 'random' then room = app.plugin.janusxr.getRandomRoom() end - print('') + print(' ') end} {if true then - function addPortal(id,x,v,url,i,z,thumbprefix,rotation,scale) - print('\t\t') - print('\t\t ') - print('\t\t ') - print('\t\t ') - print('\t\t') + function addPortal(id,x,v,url,i,z,thumb,rotation,scale) + print('\t') + print('\t ') + print('\t ') + print('\t ') + print('\t') end end} @@ -52,9 +57,13 @@ end} local width = 3.2 local center = -((#items*width)/1.3) + (i*width) local url = v.url + local thumb = ' shader_id="defaultportal"' + if v.thumbnail then + thumb = ' thumb_id="img' .. i .. '" ' + end if v.dir then url = url .. "/" end if v.redirect then url = v.redirect end -- dont wrap standalone glb's in janusxr - addPortal('dir',center, v, url,i, -4, 'img', '0 0 0', '0.2 0.2 0.2') + addPortal('dir',center, v, url,i, -4, thumb, '0 0 0', '0.2 0.2 0.2') end end} {if showfediportals and not item then @@ -74,7 +83,8 @@ end} for i,fediurl in pairs(portals) do local width = 3.2 local center = -((#portals*width)/1.3) + (i*width) - addPortal('federated',center, {title=fediurl}, fediurl,i, 4, 'federated', '0 180 0', '0.1 0.1 0.1') + local thumb = ' image_id="federated' .. i .. '"' + addPortal('federated',center, {title=fediurl}, fediurl,i, 4, thumb, '0 180 0', '0.1 0.1 0.1') end end} diff --git a/src/plugin/convert.lua b/src/plugin/convert.lua new file mode 100644 index 0000000..32f7f08 --- /dev/null +++ b/src/plugin/convert.lua @@ -0,0 +1,88 @@ +local util = require('util') + +app.on('scan_experience', function(exp) + --print_r(exp) +end) + +app.on('scan_experience_file', function(exp) + local me = app.plugin.convert + if me.assimp then + local filetypes_in = app.opts.plugin_convert_assimp_filetypes_in:gsub(" "," ") + local filetypes_out = app.opts.plugin_convert_assimp_filetypes_out:gsub(" "," ") + local convert = false + foreach( util.split( filetypes_in , ' '), function(k,ext) + ext = ext:gsub("%W", "") + if exp.path:lower():match( "." .. ext:lower() .. "$") then + convert = true + end + end) + if convert then + foreach( util.split( filetypes_out , ' '), function(k,ext) + ext = ext:gsub("%W", "") + local outfile = exp.path .. '.' .. ext + local fullpath = exp.file.fullpath .. '.' .. ext + local flags = '' + if ext:lower() == 'glb' then flags = flags .. ' --embed-textures' end + if not util.fileExist(outfile) then + print('> assimp export ' .. exp.file.fullpath .. ' ' .. fullpath ) + local ok = os.execute('assimp export ' .. exp.file.fullpath .. ' ' .. fullpath ) + if ok then + -- notify extra file to plugins + local stat,err = unix.stat(outfile) + if not err then + app.pub("scan_experience_file", nil, util.merge( exp, { + parent = exp.parent, + path = outfile, + url = "/" .. util.stripPath(outfile), + id = outfile, + file = {stat=stat, ino = stat:ino(), attr=stat, fullpath=fullpath} + })) + end + end + end + end) + end + end +end) + +app.on('set', function(k,v) +end) + +-- this registers the plugin UI +local plugin = { + name = "Conversion", + info = 'Auto-convert (older) 3D fileformats to newer ones, by generating sidecarfiles (foo.x3d.glb e.g.) via assimp', + opts = { + { + key='app.opts.plugin_convert_assimp_filetypes_in', + title='from filetype', + info='Space-separated 3D File extensions for assimp to process, lua patterns are allowed', + default = "x3d", + placeholder = "", + values='' + }, + { + key='app.opts.plugin_convert_assimp_filetypes_out', + title='to filetype', + info='Space-separated 3D File extensions for assimp to process, lua patterns are allowed', + default = 'glb', + values='' + }, + }, +} + +if util.cmdExist('assimp') then + plugin.assimp = true + local stdout, retstr, retcode = util.execute('assimp listext') + plugin.filetypes = stdout:gsub(";%*."," ") + plugin.info = "
assimp detected 
" .. plugin.info .. '
Supported filetypes: ' + foreach( util.split( plugin.filetypes, ' '), function(k,ext) + plugin.info = plugin.info .. '' .. ext .. ' ' + end) +else + plugin.info = plugin.info .. "

NOTE: assimp is not installed on this system: please install." + +end + + +return plugin diff --git a/src/plugin/global.lua b/src/plugin/global.lua index 6808cbe..0bb6a58 100644 --- a/src/plugin/global.lua +++ b/src/plugin/global.lua @@ -14,6 +14,7 @@ end) local plugin = { name = "Global settings", info = '', + ext = {"mp3","wav","ogg","mp4","vtt","md","html","jml","jpg","png","svg"}, opts = { { key='app.opts.plugin_global_title', @@ -66,7 +67,7 @@ local plugin = { }, middleware = { homepage_2D_or_3D = function(req,res,next) - if (app.opts.plugin_global_homepage == '3D') or (req.param.view == '3D') then + if (app.opts.plugin_global_homepage == '3D') or (req.param.view == '3D') or (req.param.url) then res.template = 'page/index.html' else res.template = 'page/dir.html' diff --git a/src/plugin/janusxr.lua b/src/plugin/janusxr.lua index c9250c2..48ae3e8 100644 --- a/src/plugin/janusxr.lua +++ b/src/plugin/janusxr.lua @@ -8,7 +8,27 @@ app.on('scan_experience', function(exp) end) app.on('scan_experience_file', function(exp) - local size = exp.file.attr:size() + local me = app.plugin.janusxr + local size = exp.file.attr:size() + local wrap = false + + if app.opts.plugin_janusxr_wrap_3dfile ~= "disabled" + and not (exp.tag.xrfragments and exp.tag.xrfragments.qualify) then -- dont wrap xrfragments experience + foreach( me.ext.object, function(k,ext) + if exp.path:lower():match("."..ext.."$") then + wrap = ext + end + end) + if wrap then + exp.view3D = true + exp.janusxr = { + room = app.opts.plugin_janusxr_wrap_3dfile + } + app.cache.items[ exp.file.ino ] = exp + print("✅ └─ plugin/janusxr: wrapping " .. exp.path) + end + end + if size < 100000 then -- skip large binary files > 100kb local data = util.readfile( exp.file.fullpath) if data then @@ -21,7 +41,7 @@ app.on('scan_experience_file', function(exp) app.pub("scan_experience_janusxr", nil, {exp=exp, item=item}) -- be nice to other plugins app.cache.items[ exp.file.ino ] = exp exp.view3D = true - exp.janusxr = true + exp.janusxr = {} print("✅ └─ plugin/janusxr: " .. exp.file.name) end else @@ -34,9 +54,14 @@ app.on('set', function(k,v) end) -- this registers the plugin UI +local rooms = {random="random room",room_plane="wirefloor",room1="room1",room2="room2",room3="room3",room4="room4",room5="room5",room6="room6",room7="room7"} + local plugin = { name = "JanusXR", info = 'JanusXR connects XR experiences via XML portals (+AFRAME (geometry)markup also supported).', + ext = { + object = {'glb', 'gltf', 'obj', 'fbx', 'ply', 'stl', 'wrl', 'gltf', 'glb', 'bvh' } + }, opts = { { key='app.opts.plugin_janusxr_janusweb_endpoint', @@ -52,33 +77,33 @@ local plugin = { default="https://p.janusxr.org https://cors.xrforge.org", values='' }, + { + key='app.opts.plugin_janusxr_wrap_3dfile', + title='wrap 3D file', + info='render 3D files in janusxr.xml room', + default="room_plane", + values=util.merge(rooms,{disabled="-- disabled --"}) + }, { key='app.opts.plugin_janusxr_wrap_image', title='wrap images', - info='Generate janusxr.xml to embed images into an XR experience', - default="yes", - values={yes="yes",no="no"} + info='render images in janusxr.xml room', + default="room_plane", + values=util.merge(rooms,{disabled="-- disabled --"}) }, { key='app.opts.plugin_janusxr_wrap_video', title='wrap videos', - info='Generate janusxr.xml to embed videos into an XR experience', - default="yes", - values={yes="yes",no="no"} + info='render videos in janusxr.xml room', + default="room_plane", + values=util.merge(rooms,{disabled="-- disabled --"}) }, { key='app.opts.plugin_janusxr_wrap_aframe', title='wrap AFRAME', - info='Generate janusxr.xml to embed AFRAME markup into an XR experience', - default="yes", - values={yes="yes",no="no"} - }, - { - key='app.opts.plugin_janusxr_wrap_template', - title='Media room', - info='What kind of space should images/videos be rendered into?', - default="random", - values={random="random room",plane="wirefloor",room1="room1",room2="room2",room3="room3",room4="room4",room5="room5",room6="room6",room7="room7"} + info='render AFRAME markup in janusxr.xml room', + default="room_plane", + values=util.merge(rooms,{disabled="-- disabled --"}) }, { key='app.opts.plugin_janusxr_portalroom', @@ -124,5 +149,11 @@ local plugin = { return room end } + +plugin.info = plugin.info .. "

supported 3D files: " +foreach( plugin.ext.object, function(k,ext) + plugin.info = plugin.info .. '' .. ext .. ' ' +end) +plugin.info = plugin.info .. '+ JML XML-markup in any text/binary file' return plugin diff --git a/src/plugin/xrfragments.lua b/src/plugin/xrfragments.lua index bebc82c..5dcf2a3 100644 --- a/src/plugin/xrfragments.lua +++ b/src/plugin/xrfragments.lua @@ -10,7 +10,6 @@ app.on('scan_experience_file', function(exp) local me = app.plugin.xrfragments foreach( util.split( app.opts.plugin_xrfragments_filetypes, ' '), function(k,ext) if exp.path:lower():match("."..ext.."$") then - exp.view3D = true local hasHref = false if ext == 'glb' then -- detect XR Fragment exp https://xrfragment.org/#%F0%9F%93%9C%20level0%3A%20File @@ -37,6 +36,7 @@ app.on('scan_experience_file', function(exp) end exp.tag.xrfragments = { qualify = hasHref or hasPNG or hasWebVTT or hasOGG or hasJSONref, href = hasHref } if exp.tag.xrfragments.qualify then + exp.view3D = true print("✅ └─ plugin/xrfragments: detected xrfragments experience") end if exp.tag.xrfragments.href then @@ -47,6 +47,7 @@ app.on('scan_experience_file', function(exp) end) -- finally insert if viewable if exp.view3D then + print("✅ └─ plugin/xrfragments: marking as 3D viewable") app.cache.items[ exp.file.ino ] = exp end end