fixed symlink & referrer stuff

This commit is contained in:
Leon van Kammen 2026-08-24 13:29:35 +02:00
parent 26d1bfbd1e
commit 3981800e42
12 changed files with 128 additions and 69 deletions

View file

@ -1,8 +1,8 @@
-H -H
Access-Control-Allow-Origin: * Access-Control-Allow-Origin: *
-H -H
Access-Control-Allow-Method: GET, OPTIONS Access-Control-Allow-Method: GET, HEAD, OPTIONS
-H -H
Allow: GET, POST, OPTIONS Allow: GET, HEAD, POST, OPTIONS
-D -D
. .

View file

@ -26,6 +26,7 @@ app = require("soakbean") {
generate = {file="cmd/generate.lua", info="generate www-folder with XR experiences"}, generate = {file="cmd/generate.lua", info="generate www-folder with XR experiences"},
test = {file="cmd/test.lua", info="run testsuite (selftest)"} test = {file="cmd/test.lua", info="run testsuite (selftest)"}
}, },
addPlugin = function(file) addPlugin = function(file)
local p = require(file) local p = require(file)
table.insert(app.plugin,p) table.insert(app.plugin,p)
@ -40,6 +41,14 @@ app = require("soakbean") {
end) end)
app.set('app.opts', app.opts) app.set('app.opts', app.opts)
return p return p
end,
addExperience = function(experience, ino)
if app.cache.items[ino] then -- handle duplicate symlinks, symlinks to symlinks e.g.
app.cache.items[ino .. ' ' .. experience.parent.id ] = experience
else
app.cache.items[ino] = experience
end
end end
} }
@ -78,9 +87,9 @@ print = function(...) Log(kLogInfo, ... or "''") end -- nice redbean logging fro
-- init URL router -- init URL router
app.url['^/data'] = '/data.lua' -- setup custom file endpoint app.url['^/data'] = '/data.lua' -- setup custom file endpoint
app.use( require('middleware/onlyGET')({exclude={'^/admin'}}) ) -- we are staticwebgen-first server-last
app.use( app.plugin.global.middleware.homepage_2D_or_3D ) app.use( app.plugin.global.middleware.homepage_2D_or_3D )
app.url['^/admin[/]?$'] = '/page/admin/index.lua' app.url['^/admin[/]?$'] = '/page/admin/index.lua'
app.use( require('middleware/onlyGET')({exclude={'^/admin'}}) ) -- we are staticwebgen-first server-last
app.get('^/design[/]?$', util.pagerender('page/design.html')) app.get('^/design[/]?$', util.pagerender('page/design.html'))
app.get('^/about[/]?$', util.pagerender('page/about.html')) app.get('^/about[/]?$', util.pagerender('page/about.html'))

View file

@ -106,6 +106,7 @@ util.template = function(content,data)
-- set fallback vars -- set fallback vars
mdata.urlpath = util.baseURLify( GetPath() ) mdata.urlpath = util.baseURLify( GetPath() )
mdata.url = mdata.url or mdata.urlpath mdata.url = mdata.url or mdata.urlpath
mdata.janusurl = mdata.janusurl or mdata.urlpath
mdata.title = mdata.title or app.opts.subtitle mdata.title = mdata.title or app.opts.subtitle
mdata.header_include = mdata.header_include or '' mdata.header_include = mdata.header_include or ''
mdata.footerlinks = {} mdata.footerlinks = {}

View file

@ -3,7 +3,7 @@ return function(opts)
opts = opts or {exclude={"^/admin"}} opts = opts or {exclude={"^/admin"}}
return function(req,res,next) return function(req,res,next)
local pass = false local pass = false
if req.method == "GET" then if req.method == "GET" or req.method == "HEAD" then
pass = true pass = true
else else
foreach(opts.exclude, function(k,path) foreach(opts.exclude, function(k,path)

View file

@ -57,9 +57,7 @@ end
app.on('set', function(k,v) -- clear url cache app.on('set', function(k,v) -- clear url cache
if k:match("app.opts") then -- after (admin) updates to app.opts.* if k:match("app.opts") then -- after (admin) updates to app.opts.*
foreach( app.cache.url, function(k,v) app.cache.url = {}
app.cache.url = {}
end)
end end
end) end)

View file

@ -22,8 +22,7 @@ end}
function getURL(url) function getURL(url)
if app.arg[1] == 'generate' or url:match("://") then if app.arg[1] == 'generate' or url:match("://") then
url = 'janusweb/index.html#janus.url=' .. url = '?view=3D#janus.url=' .. EscapeFragment( url )
EscapeFragment( url ) .. '&janus.referrer=' .. util.getURL(urlpath )
url = util.baseURLify(url) url = util.baseURLify(url)
else else
url = util.stripPath( url ) url = util.stripPath( url )

View file

@ -25,6 +25,7 @@ return {
local matchpath = urlpath local matchpath = urlpath
if urlpath == '/' then matchpath = rootpath end -- generate janusxr footer for root url if urlpath == '/' then matchpath = rootpath end -- generate janusxr footer for root url
foreach( app.cache.items, function(k,v) foreach( app.cache.items, function(k,v)
print("URL=" ..v.url)
if req.authenticated or app.plugin.moderation.isAllowed(v) then if req.authenticated or app.plugin.moderation.isAllowed(v) then
if v.parent then if v.parent then
-- trace( "path = " .. util.stripPath(v.path) .. " " .. -- trace( "path = " .. util.stripPath(v.path) .. " " ..
@ -57,7 +58,7 @@ return {
end) end)
local data = util.merge({ local data = util.merge({
url = req.param.url, -- query hint to render janus.url in iframe (index.html) janusurl = req.param.url, -- query hint to render janus.url in iframe (index.html)
items = {}, items = {},
isroot = isroot, isroot = isroot,
referrer = referrer, referrer = referrer,
@ -71,7 +72,7 @@ return {
i=i+1 i=i+1
if v.view3D then if v.view3D then
local isMarkupExperience = function(v) local isMarkupExperience = function(v)
return v.janusxr and (v.janusxr.jml or v.janusxr.aframe) return v.janusxr and (v.janusxr.jml or v.janusxr.wrap)
end end
-- prioritize markup experience above binary 3D files as experience containers -- prioritize markup experience above binary 3D files as experience containers
if not data.item or (isMarkupExperience(v) and not isMarkupExperience(data.item)) then if not data.item or (isMarkupExperience(v) and not isMarkupExperience(data.item)) then
@ -80,9 +81,11 @@ return {
data.item = v data.item = v
data.title = data.item.title data.title = data.item.title
data.parent = v.parent data.parent = v.parent
data.url = app.opts.baseurl .. util.stripPath(v.url) -- because we are running in iframe data.janusurl = (v.janusxr and v.janusxr.wrap) and
-- .. EscapeFragment( (v.url:match("#") and "&" or "#" ) .. 'janus.referrer=' .. EscapeFragment(v.referrer) ) app.opts.baseurl .. util.stripPath(v.parent.url)
if v.janusxr and (v.janusxr.jml or v.janusxr.aframe) then or
app.opts.baseurl .. util.stripPath(v.url)
if v.janusxr and (v.janusxr.jml or v.janusxr.wrap and v.janusxr.wrap.aframe) then
-- what to do here? no generated portals? -- what to do here? no generated portals?
data.janusxr = app.tpl( util.readfile(v.path), data ) data.janusxr = app.tpl( util.readfile(v.path), data )
end end

View file

@ -3,7 +3,7 @@ ${header}
<div></div> <div></div>
</div> </div>
<iframe border="0" frameborder="0" src="janusweb/index.html#janus.url=${url}&janus.referrer=${referrer}" id="viewer"></iframe> <iframe border="0" frameborder="0" src="janusweb/index.html#janus.url=${janusurl}&janus.referrer=${referrer}" id="viewer"></iframe>
{if defined(item) and item.parent and item.parent.info then {if defined(item) and item.parent and item.parent.info then
print([[ print([[
@ -45,6 +45,10 @@ end}
}, 1000 ) }, 1000 )
}); });
}); });
// reroute janus.url to iframe if needed
let params = new URLSearchParams( document.location.href.replace(/.*#/,'') )
if( params?.get('janus.url') ) iframe.src = 'janusweb/index.html#janus.url=' + encodeURIComponent( params.get('janus.url') )
</script> </script>

View file

@ -40,17 +40,21 @@ end}
end} end}
</assets> </assets>
{if true then {if true then
local room = app.opts.plugin_janusxr_portalroom local room = ''
if defined(item) and item.janusxr and item.janusxr.room then pos = '0 0 0'
room = item.janusxr.room if not defined(item) and defined(items) then
room = app.opts.plugin_janusxr_portalroom
end end
if room == 'random' then room = app.plugin.janusxr.getRandomRoom() end if defined(item) and item.janusxr and item.janusxr.wrap then
if defined(item) and item.tag.xrfragments and item.tag.xrfragments.qualify then pos = '0 0 2'
room = app.plugin.janusxr.wrapRoomAroundFile(item)
end
if defined(item) and item.tag.xrfragments and item.tag.xrfragments.href then
room = '' -- xrfragments 3D files dont need to be wrapped with room room = '' -- xrfragments 3D files dont need to be wrapped with room
end end
print(' <room skybox="true" showavatar="false" use_local_asset="' .. room .. '">') print(' <room skybox="true" pos="' .. pos .. '" showavatar="false" use_local_asset="' .. room .. '">')
if defined(item) then if defined(item) then
print(' <object id="${url}"/>') print(' <object id="${item.url}"/>')
end end
end} end}
<light js_id="dan9-janus:light/light_cone_angle=0-6" pos="0 3 2" collision_trigger="false" /> <light js_id="dan9-janus:light/light_cone_angle=0-6" pos="0 3 2" collision_trigger="false" />
@ -87,6 +91,10 @@ end}
local width = 3.2 local width = 3.2
local center = -((#itemsremote*width)/1.3) + (i*width) local center = -((#itemsremote*width)/1.3) + (i*width)
local url = v.url local url = v.url
--if url:match("://") then
-- url = url .. ( url:match("#") and '&' or '#' ) .. 'janus.referrer=' ..
-- EscapeFragment( util.getURL() )
--end
local thumb = ' shader_id="defaultportal"' local thumb = ' shader_id="defaultportal"'
if #v.thumbnail > 0 then if #v.thumbnail > 0 then
thumb = ' thumb_id="imgremote' .. i .. '" ' thumb = ' thumb_id="imgremote' .. i .. '" '
@ -106,10 +114,10 @@ end}
url = referrer, url = referrer,
title = app.opts.plugin_janusxr_portal_up, title = app.opts.plugin_janusxr_portal_up,
} }
addPortal('parent',4, v, v.url, 0, 0, ' shader_id="defaultportal"', '0 -90 0', '0.2 0.2 0.2') addPortal('parent',5, v, v.url, 0, 0, ' shader_id="defaultportal"', '0 -90 0', '0.2 0.2 0.2')
end} end}
<object id="cube" rotation="0 0 0" position="0 0 0" scale="2 0.1 2"/> <!-- something to stand on --> <object id="cube" rotation="0 45 0" pos="${pos}" scale="2 0.1 2"/> <!-- something to stand on -->
{if not defined(item) then {if not defined(item) then
print('${janusxr_jml_deco}') print('${janusxr_jml_deco}')
end} end}

View file

@ -37,7 +37,7 @@ return {
shouldHideFile = function(name) shouldHideFile = function(name)
return name == ".." -- parent return name == ".." -- parent
or name == "util" -- repository or name == "util" -- repository
or name:match("^%.") -- hidden or name:match("^%.[a-zA-Z0-9_-]") -- hidden
or name == "src" -- repository or name == "src" -- repository
or name == "result" -- repository or name == "result" -- repository
end, end,
@ -46,7 +46,7 @@ return {
local me = app.plugin.diskscan local me = app.plugin.diskscan
local seen = {symlinks={}} local seen = {symlinks={}}
-- ensure rootdir as experience -- ensure rootdir as experience
app.cache.items[ scandir ] = { app.addExperience({
path = scandir, path = scandir,
url = "/" .. util.stripPath(scandir), url = "/" .. util.stripPath(scandir),
name = scandir:gsub(".*/",""), name = scandir:gsub(".*/",""),
@ -54,7 +54,7 @@ return {
title = scandir:gsub(".*/",""):gsub("-"," "), title = scandir:gsub(".*/",""):gsub("-"," "),
file = { ino = scandir }, file = { ino = scandir },
id = scandir id = scandir
} },scandir)
me.scanDir(scandir, nil, seen ) me.scanDir(scandir, nil, seen )
-- deal with (circular) symlinks -- deal with (circular) symlinks
foreach( seen.symlinks, function(ino,experience) foreach( seen.symlinks, function(ino,experience)
@ -65,8 +65,8 @@ return {
print("✅ └─ plugin/diskscan: detected symlink..creating portal") print("✅ └─ plugin/diskscan: detected symlink..creating portal")
experience.file = refItem.file experience.file = refItem.file
experience.portal = refItem experience.portal = refItem
app.pub("scan_experience", nil, experience) app.pub("scan_experience" .. (experience.dir and '' or '_file') , nil, experience)
app.cache.items[experience.id] = experience app.addExperience(experience,ino)
end end
end end
end) end)
@ -100,26 +100,34 @@ return {
} }
experience.parent = parent experience.parent = parent
if seen[ino] then return end -- dont process dirs/files twice
if (kind == unix.DT_LNK) then if (kind == unix.DT_LNK) then
experience.symlink = true
local stat, err = unix.stat(fullpath) local stat, err = unix.stat(fullpath)
if stat then if stat then
experience.id_target = stat:ino() experience.id_target = stat:ino() -- stat follows symlink by default
local realpath = unix.realpath(fullpath)
state,err = unix.opendir(realpath)
if not err then
experience.symlink = true
experience.dir = true
state:close()
end
seen.symlinks[ ino ] = experience seen.symlinks[ ino ] = experience
end end
return
end end
--local already_seen = seen[ino] and seen[ino].parent.file.ino == experience.parent.file.ino and true or false -- process once
--if already_seen and not experience.symlink then return end
-- found directory -- found directory
if (kind == unix.DT_DIR and kind ~= unix.DT_LNK) if not me.shouldHideFile(name) and
and not me.shouldHideFile(name) then ( (kind == unix.DT_DIR and kind ~= unix.DT_LNK) or experience.dir )
seen[ino] = experience -- prevent recursion with symlinks then
print("".. fullpath .. " " ..ino) print("".. fullpath )
experience.dir = true experience.dir = true
experience.portal3D = true -- can be viewed as 3d portal experience.portal3D = true -- can be viewed as 3d portal
app.pub("scan_experience", nil, experience) app.pub("scan_experience", nil, experience)
app.cache.items[ino] = experience app.addExperience(experience,ino)
-- check for directory thumbnail sidecarfile -- check for directory thumbnail sidecarfile
local thumbExt = {"png","jpg","webp"} local thumbExt = {"png","jpg","webp"}
foreach( thumbExt, function(k,ext) foreach( thumbExt, function(k,ext)
@ -136,14 +144,18 @@ return {
end end
---- found file ---- found file
if (kind ~= unix.DT_DIR and kind ~= unix.DT_LNK) if not me.shouldHideFile(name) and
and not me.shouldHideFile(name) then ( (kind ~= unix.DT_DIR and kind ~= unix.DT_LNK) or not experience.dir )
seen[ino] = experience.file -- prevent recursion with symlinks then
trace(" ".. fullpath) trace(" ".. fullpath)
experience.tag.file = true experience.tag.file = true
app.pub("scan_experience_file", nil, experience) app.pub("scan_experience_file", nil, experience)
local size = attr:size() local size = attr:size()
end end
-- remember file/parent relationship
seen[ino] = experience
end end
local dref, derr; local dref, derr;

View file

@ -13,7 +13,7 @@ app.on('dir', function(opts)
if #app.opts.plugin_janusxr_random_objects > 0 and if #app.opts.plugin_janusxr_random_objects > 0 and
#app.opts.plugin_janusxr_random_colors > 0 then #app.opts.plugin_janusxr_random_colors > 0 then
opts.data.janusxr_jml_deco = app.plugin.janusxr.getRandomDeco(7,50,hash,0,30) .. opts.data.janusxr_jml_deco = app.plugin.janusxr.getRandomDeco(7,50,hash,0,30) ..
app.plugin.janusxr.getRandomDeco(7,50,hash .. hash,30,30) app.plugin.janusxr.getRandomDeco(7,50, "2" .. hash,30,30)
end end
end) end)
@ -31,10 +31,8 @@ app.on('scan_experience_file', function(exp)
end) end)
if wrap then if wrap then
exp.view3D = true exp.view3D = true
exp.janusxr = { exp.janusxr = { wrap = { object = true }}
room = app.opts.plugin_janusxr_wrap_3dfile app.addExperience( exp, exp.file.ino )
}
app.cache.items[ exp.file.ino ] = exp
print("✅ └─ plugin/janusxr: wrapping " .. exp.path) print("✅ └─ plugin/janusxr: wrapping " .. exp.path)
end end
end end
@ -49,13 +47,13 @@ app.on('scan_experience_file', function(exp)
exp.title = title:gsub("<.*","") exp.title = title:gsub("<.*","")
end end
app.pub("scan_experience_janusxr", nil, {exp=exp, item=item}) -- be nice to other plugins app.pub("scan_experience_janusxr", nil, {exp=exp, item=item}) -- be nice to other plugins
app.cache.items[ exp.file.ino ] = exp app.addExperience( exp, exp.file.ino )
exp.view3D = true exp.view3D = true
exp.parent.redirect = exp.url -- mark parent to redirect to this standalone url instead (not glb assets e.g.) exp.parent.redirect = exp.url -- mark parent to redirect to this standalone url instead (not glb assets e.g.)
exp.janusxr = {} exp.janusxr = {}
print("✅ └─ plugin/janusxr: " .. exp.file.name) print("✅ └─ plugin/janusxr: " .. exp.file.name)
if data:match(pattern_aframe) and app.opts.plugin_janusxr_wrap_aframe == 'enabled' then if data:match(pattern_aframe) and app.opts.plugin_janusxr_wrap_aframe == 'enabled' then
exp.janusxr.aframe = true exp.janusxr.wrap = {aframe = true }
print("✅ └─ plugin/janusxr: AFRAME markup detected") print("✅ └─ plugin/janusxr: AFRAME markup detected")
end end
end end
@ -68,7 +66,7 @@ end)
app.on('set', function(k,v) end) app.on('set', function(k,v) end)
-- this registers the plugin UI -- 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 rooms = {random="random room",randomurl="random room (URL-deterministic)",room_plane="wirefloor",room1="room1",room2="room2",room3="room3",room4="room4",room5="room5",room6="room6",room7="room7"}
local plugin = { local plugin = {
name = "JanusXR", name = "JanusXR",
@ -95,29 +93,29 @@ local plugin = {
key='app.opts.plugin_janusxr_wrap_3dfile', key='app.opts.plugin_janusxr_wrap_3dfile',
title='VR wrap 3D file', title='VR wrap 3D file',
info='render 3D files in <a href="https://coderofsalvation.github.io/janus-guide/#/build/room" target="_blank">janusxr.xml</a> room', info='render 3D files in <a href="https://coderofsalvation.github.io/janus-guide/#/build/room" target="_blank">janusxr.xml</a> room',
default="room_plane", default="randomurl",
values=util.merge(rooms,{disabled="-- disabled --"}) values=util.merge(rooms,{disabled="-- disabled --"})
}, },
{ {
key='app.opts.plugin_janusxr_wrap_image', key='app.opts.plugin_janusxr_wrap_image',
title='VR wrap images', title='VR wrap images',
info='render images in <a href="https://coderofsalvation.github.io/janus-guide/#/build/room" target="_blank">janusxr.xml</a> room', info='render images in <a href="https://coderofsalvation.github.io/janus-guide/#/build/room" target="_blank">janusxr.xml</a> room',
default="room_plane", default="randomurl",
values=util.merge(rooms,{disabled="-- disabled --"}) values=util.merge(rooms,{disabled="-- disabled --"})
}, },
{ {
key='app.opts.plugin_janusxr_wrap_video', key='app.opts.plugin_janusxr_wrap_video',
title='VR wrap videos', title='VR wrap videos',
info='render videos in <a href="https://coderofsalvation.github.io/janus-guide/#/build/room" target="_blank">janusxr.xml</a> room', info='render videos in <a href="https://coderofsalvation.github.io/janus-guide/#/build/room" target="_blank">janusxr.xml</a> room',
default="room_plane", default="randomurl",
values=util.merge(rooms,{disabled="-- disabled --"}) values=util.merge(rooms,{disabled="-- disabled --"})
}, },
{ {
key='app.opts.plugin_janusxr_wrap_aframe', key='app.opts.plugin_janusxr_wrap_aframe',
title='AFRAME support', title='AFRAME support',
info='render <a href="https://aframe.io">AFRAME</a> (geometry) markup when detected in URLs', info='render <a href="https://aframe.io">AFRAME</a> (geometry) markup when detected in URLs',
default="enabled", default="room_plane",
values={enabled="enabled",disabled="disabled"} values=util.merge(rooms,{disabled="-- disabled --"})
}, },
{ {
key='app.opts.plugin_janusxr_portalroom', key='app.opts.plugin_janusxr_portalroom',
@ -138,7 +136,7 @@ local plugin = {
key='app.opts.plugin_janusxr_random_objects', key='app.opts.plugin_janusxr_random_objects',
title='Random objects', title='Random objects',
info="Random object id's in portal rooms", info="Random object id's in portal rooms",
default="capsule cone cube cylinder pyramid sphere torus", default="capsule cone cylinder pyramid sphere torus",
placeholder="capsule cone cylinder pyramid sphere torus", placeholder="capsule cone cylinder pyramid sphere torus",
values="", values="",
}, },
@ -174,21 +172,54 @@ local plugin = {
} }
}, },
getRandomRoom = function() wrapRoomAroundFile = function(item)
local room = ''
if item.janusxr.wrap.object then
room = app.opts.plugin_janusxr_wrap_3dfile
end
if room == 'random' then
room = app.plugin.janusxr.getRandomRoom()
end
if room == 'randomurl' then
room = app.plugin.janusxr.getRandomRoom(item.url)
end
return room
end,
getRandomRoom = function(url)
local me = app.plugin.janusxr local me = app.plugin.janusxr
local room= '' local room= ''
math.randomseed(os.time()) math.randomseed(os.time())
foreach( me.opts, function(k,v) foreach( me.opts, function(k,v)
if v.key == 'app.opts.plugin_janusxr_portalroom' then if v.key == 'app.opts.plugin_janusxr_portalroom' then
local ordered = {} local ordered = {}
foreach( v.values, function(k,v) table.insert(ordered,v) end) foreach( v.values, function(k,v)
room = ordered[ math.random( #ordered ) ] if not v:match("random") then
table.insert(ordered,v)
end
end)
table.sort(ordered) -- ensure same order
if url then
room = ordered[ me.hash_string_to_number(url) % #ordered ]
else
room = ordered[ math.random( #ordered ) ]
end
end end
end) end)
return room return room
end, end,
hash_string_to_number = function(str)
local hash = 5381
for i = 1, #str do
local char = string.byte(str, i)
hash = (hash * 33 + char) % 2147483647
end
return hash
end,
getRandomDeco = function(num_objects, radius, seed_string, y, scale) getRandomDeco = function(num_objects, radius, seed_string, y, scale)
local me = app.plugin.janusxr
num_objects = num_objects or 6 num_objects = num_objects or 6
y = y or 0 y = y or 0
radius = radius or 2 -- Configurable distance from (0, 0, 0) radius = radius or 2 -- Configurable distance from (0, 0, 0)
@ -196,18 +227,12 @@ local plugin = {
local JML = '' local JML = ''
local colors = util.split( app.opts.plugin_janusxr_random_colors, ' ') local colors = util.split( app.opts.plugin_janusxr_random_colors, ' ')
local ids = util.split( app.opts.plugin_janusxr_random_objects, ' ') local ids = util.split( app.opts.plugin_janusxr_random_objects, ' ')
table.sort(colors) -- ensure same order
table.sort(ids) -- ensure same order
local function hash_string_to_number(str)
local hash = 5381
for i = 1, #str do
local char = string.byte(str, i)
hash = (hash * 33 + char) % 2147483647
end
return hash
end
local index = { local index = {
color= hash_string_to_number(seed_string) % #colors, color= me.hash_string_to_number(seed_string) % #colors,
id = hash_string_to_number(seed_string) % #ids id = me.hash_string_to_number(seed_string) % #ids
} }
for i = 1, num_objects do for i = 1, num_objects do

View file

@ -51,7 +51,7 @@ app.on('scan_experience_file', function(exp)
-- finally insert if viewable -- finally insert if viewable
if exp.view3D then if exp.view3D then
print("✅ └─ plugin/xrfragments: marking as 3D viewable") print("✅ └─ plugin/xrfragments: marking as 3D viewable")
app.cache.items[ exp.file.ino ] = exp app.addExperience(exp, exp.file.ino)
end end
end end
end) end)