gridmode improvements

This commit is contained in:
Leon van Kammen 2026-09-16 13:43:33 +02:00
parent 81b9d238a9
commit a10118ad32
25 changed files with 150 additions and 63 deletions

View file

@ -54,11 +54,6 @@ local apiSync = {
-- runs sync in foreground -- runs sync in foreground
app.on('init', function() app.on('init', function()
foreach( app.plugin.openbrush.opts, function(k,opt)
if opt.key == 'app.opts.plugin_openbrush_sync_folder' then
opt.values = app.cache.dirs
end
end)
if util.contains(argv, 'generate') then api.sync(api) end if util.contains(argv, 'generate') then api.sync(api) end
end) end)

View file

@ -418,7 +418,7 @@ end
util.hyphenate = function(str) util.hyphenate = function(str)
local str,match = str:lower() local str,match = str:lower()
:gsub("[^%w]", "-") :gsub("[^%w_]", "-")
:gsub("%-+", "-") :gsub("%-+", "-")
:gsub("^%-", "") :gsub("^%-", "")
:gsub("%-$", "") :gsub("%-$", "")
@ -426,7 +426,7 @@ util.hyphenate = function(str)
end end
util.dehyphenate = function(str) util.dehyphenate = function(str)
local str,match = str:gsub("_[a-zA-Z0-9]+","") -- remove ids starting with _ local str,match = str:gsub("^_[a-zA-Z0-9]+","") -- remove ids starting with _
:gsub("%-%-","-") -- remove double dash :gsub("%-%-","-") -- remove double dash
:gsub("-"," ") -- convert to spaces :gsub("-"," ") -- convert to spaces
:gsub("^%s*(.-)%s*$", "%1") -- trim :gsub("^%s*(.-)%s*$", "%1") -- trim

View file

@ -0,0 +1,60 @@
<h1>Hi there! 👋</h1>
Welcome to this XR experience.<br/>
<br/>
<div class="btn script">
<button class="btn secondary" onclick="document.querySelector('#tips').style.display = ''">Tips for navigating in XR</button>
</div>
<div id="tips" style="display:none">
<br/>
<table>
<tr>
<td>
<b>Keyboard+Mouse</b>
</td>
<td>
use the WASD-keys and mousedrag to navigate
</td>
</tr>
<tr>
<td width="200">
<b>AR/VR headset</b>
</td>
<td>
walk around / use right-longpinch (or thumbstick) for local teleports<br/>
</td>
</tr>
<tr>
<td>
<b>PhoneVR</b>
</td>
<td>
use gaze or bluetooth/mouse to navigate
</td>
</tr>
<tr>
<td>
<b>PhoneAR</b>
</td>
<td>
walk around and tap screen to click
</td>
</tr>
<tr>
<td>
<b>Mobile</b>
</td>
<td>
use the WASD-keys and mousedrag to navigate
</td>
</tr>
</table>
<br/>
You've got this!
</div>
<br/>
This virtual world is generated in realtime via <a href='https://icosa.gallery' target='_blank'>icosa.gallery</a>.
You can add/create parcels in icosa.gallery, by publishing via <a href='https://openbrush.app'>OpenBrush</a>, <a href='https://openblocks.app'>OpenBlocks</a>, or <a href='https://blender.org' target='_blank'>Blender</a>.<br>
> NOTE: Make sure to add hashtag **#tronverse-icosagallery** to the title (and re-run xrforge for fast re-sync).

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -0,0 +1 @@
{"assetId":"0dycp4HDxf4","authorId":"9Vkr0htmSzc","authorName":"coderofsalvation","createTime":"2026-09-15T13:03:05.314Z","description":"","displayName":"tronverse_floor #overlay #tronverse-icosagallery","formats":[{"formatComplexity":{},"formatType":"GLTF2","isCorsAllowed":true,"isPreferredForDownload":true,"isPreferredForGalleryViewer":true,"resources":[],"role":"USER_SUPPLIED_GLTF","root":{"contentType":"model\/gltf-binary","relativePath":"tronverse_floor.glb","url":"https:\/\/s3.us-east-005.backblazeb2.com\/icosa-gallery\/icosa\/28534\/755050994710086613\/GLTF2\/tronverse_floor.glb"}}],"isCurated":false,"isIcosaGalleryCompatible":true,"license":"CREATIVE_COMMONS_BY","licenseVersion":"4.0","name":"assets\/0dycp4HDxf4","thumbnail":{"relativePath":"preview_image.jpg","url":"https:\/\/s3.us-east-005.backblazeb2.com\/icosa-gallery\/icosa\/28534\/755050994710086613\/preview_image.jpg"},"triangleCount":0,"updateTime":"2026-09-16T11:21:33.683Z","url":"http:\/\/api.icosa.gallery\/v1\/assets\/0dycp4HDxf4","visibility":"PUBLIC"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View file

@ -0,0 +1 @@
{"assetId":"C9J0_dyCzd8","authorId":"9Vkr0htmSzc","authorName":"coderofsalvation","createTime":"2026-09-15T18:22:06.894Z","description":"copy this asset as a starting point to paint into","displayName":"tronverse_parcel_empty #tronverse-icosagallery","formats":[{"formatComplexity":{},"formatType":"GLTF2","isCorsAllowed":true,"isPreferredForDownload":true,"isPreferredForGalleryViewer":true,"resources":[],"role":"USER_SUPPLIED_GLTF","root":{"contentType":"model\/gltf-binary","relativePath":"tronverse_parcel_empty.glb","url":"https:\/\/s3.us-east-005.backblazeb2.com\/icosa-gallery\/icosa\/28534\/755131280311452630\/GLTF2\/tronverse_parcel_empty.glb"}}],"isCurated":false,"isIcosaGalleryCompatible":true,"license":"CREATIVE_COMMONS_BY","licenseVersion":"4.0","name":"assets\/C9J0_dyCzd8","thumbnail":{"relativePath":"preview_image.jpg","url":"https:\/\/s3.us-east-005.backblazeb2.com\/icosa-gallery\/icosa\/28534\/755131280311452630\/preview_image.jpg"},"triangleCount":0,"updateTime":"2026-09-16T11:21:20.762Z","url":"http:\/\/api.icosa.gallery\/v1\/assets\/C9J0_dyCzd8","visibility":"PUBLIC"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View file

@ -0,0 +1 @@
{"assetId":"igTGJIhJcSw","authorId":"9Vkr0htmSzc","authorName":"coderofsalvation","createTime":"2026-09-16T10:16:05.148Z","description":"","displayName":"tronverse_parcel3 #tronverse-icosagallery","formats":[{"formatComplexity":{},"formatType":"GLTF2","isCorsAllowed":true,"isPreferredForDownload":true,"isPreferredForGalleryViewer":true,"resources":[],"role":"USER_SUPPLIED_GLTF","root":{"contentType":"model\/gltf-binary","relativePath":"tronverse_parcel3.glb","url":"https:\/\/s3.us-east-005.backblazeb2.com\/icosa-gallery\/icosa\/28534\/755371354949157841\/GLTF2\/tronverse_parcel3.glb"}}],"isCurated":false,"isIcosaGalleryCompatible":true,"license":"CREATIVE_COMMONS_BY","licenseVersion":"4.0","name":"assets\/igTGJIhJcSw","thumbnail":{"relativePath":"preview_image.jpg","url":"https:\/\/s3.us-east-005.backblazeb2.com\/icosa-gallery\/icosa\/28534\/755371354949157841\/preview_image.jpg"},"triangleCount":0,"updateTime":"2026-09-16T11:20:41.832Z","url":"http:\/\/api.icosa.gallery\/v1\/assets\/igTGJIhJcSw","visibility":"PUBLIC"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View file

@ -0,0 +1 @@
{"assetId":"mRU7wJh9BLQ","authorId":"9Vkr0htmSzc","authorName":"coderofsalvation","createTime":"2026-09-15T11:44:11.217Z","description":"","displayName":"tronverse_parcel1 #tronverse-icosagallery","formats":[{"formatComplexity":{},"formatType":"GLTF2","isCorsAllowed":true,"isPreferredForDownload":true,"isPreferredForGalleryViewer":true,"resources":[],"role":"USER_SUPPLIED_GLTF","root":{"contentType":"model\/gltf-binary","relativePath":"tronverse_parcel1.glb","url":"https:\/\/s3.us-east-005.backblazeb2.com\/icosa-gallery\/icosa\/28534\/755031138472297450\/GLTF2\/tronverse_parcel1.glb"}}],"isCurated":false,"isIcosaGalleryCompatible":true,"license":"CREATIVE_COMMONS_BY","licenseVersion":"4.0","name":"assets\/mRU7wJh9BLQ","thumbnail":{"relativePath":"preview_image.jpg","url":"https:\/\/s3.us-east-005.backblazeb2.com\/icosa-gallery\/icosa\/28534\/755031138472297450\/preview_image.jpg"},"triangleCount":0,"updateTime":"2026-09-16T11:21:48.917Z","url":"http:\/\/api.icosa.gallery\/v1\/assets\/mRU7wJh9BLQ","visibility":"PUBLIC"}

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -0,0 +1 @@
{"assetId":"oiIu1aNn6Ek","authorId":"9Vkr0htmSzc","authorName":"coderofsalvation","createTime":"2026-09-16T10:06:44.019Z","description":"","displayName":"tronverse_parcel2 #overlay #tronverse-icosagallery","formats":[{"formatComplexity":{},"formatType":"GLTF2","isCorsAllowed":true,"isPreferredForDownload":true,"isPreferredForGalleryViewer":true,"resources":[],"role":"USER_SUPPLIED_GLTF","root":{"contentType":"model\/gltf-binary","relativePath":"tronverse_parcel2.glb","url":"https:\/\/s3.us-east-005.backblazeb2.com\/icosa-gallery\/icosa\/28534\/755369001416131548\/GLTF2\/tronverse_parcel2.glb"}}],"isCurated":false,"isIcosaGalleryCompatible":true,"license":"CREATIVE_COMMONS_BY","licenseVersion":"4.0","name":"assets\/oiIu1aNn6Ek","thumbnail":{"relativePath":"preview_image.jpg","url":"https:\/\/s3.us-east-005.backblazeb2.com\/icosa-gallery\/icosa\/28534\/755369001416131548\/preview_image.jpg"},"triangleCount":0,"updateTime":"2026-09-16T11:20:54.904Z","url":"http:\/\/api.icosa.gallery\/v1\/assets\/oiIu1aNn6Ek","visibility":"PUBLIC"}

View file

@ -62,7 +62,7 @@ div#xrecosystem{
<h2>Turn files into AR/VR 🥽 experiences</h2> <h2>Turn files into AR/VR 🥽 experiences</h2>
<center> <center>
A static website generator for interconnected XR experiences.<br> A static website generator for interconnected XR experiences.<br>
Optionally runs as server/client too. Generate and manage open virtual worlds, powered by <a href="https://coderofsalvation.github.io/janus-guide">JanusXR</a> &amp; <a href="https://xrfragment.org">XR Fragments</a>.
<br/> <br/>
<br/> <br/>
<div class="btn"> <div class="btn">
@ -225,7 +225,8 @@ div#xrecosystem{
<h3 id="autocompose">Auto-compose virtual worlds</h3> <h3 id="autocompose">Auto-compose virtual worlds</h3>
Instead of manually composing virtual worlds from multiple assets, XRForge's philosophy is <b>auto-layout</b> and <b>interoperability</b>.<br> Instead of manually composing virtual worlds from multiple assets, XRForge's philosophy is <b>auto-layout</b> and <b>interoperability</b>.<br>
This means that assets produced in <a href="https://openblocks.app" target="_blank">OpenBlocks</a> or <a href="https://openbrush.app" target="_blank">OpenBrush</a> can <b>(in)directly</b> get imported</b> (via online 3D stores like <a href="https://icosa.gallery" target="_blank">icosa.gallery-</a> and <a href="https://manyfold.app" target="_blank">manyfold.app</a>).<br> This means that existing RSS/Atom-feeds (from blogs) and partial content from websites can be projected into virtual worlds, <b>via URLs</b>.<br/>
But also assets produced elsewhere, in <a href="https://openblocks.app" target="_blank">OpenBlocks</a> or <a href="https://openbrush.app" target="_blank">OpenBrush</a> are synchronized (via online 3D stores like <a href="https://icosa.gallery" target="_blank">icosa.gallery-</a> and <a href="https://manyfold.app" target="_blank">manyfold.app</a>).<br>
This allows for infinitely scalable virtual worlds, yet completely downloadable and portable.<br> This allows for infinitely scalable virtual worlds, yet completely downloadable and portable.<br>
XRForge features <b>layout-engines</b> which allows <b>crowdsourcing virtual worlds</b> via assets using various apps/platforms:<br> XRForge features <b>layout-engines</b> which allows <b>crowdsourcing virtual worlds</b> via assets using various apps/platforms:<br>
<br> <br>

View file

@ -7,6 +7,8 @@ ${header}
{if defined(item) and item.parent and item.parent.info then {if defined(item) and item.parent and item.parent.info then
description = item.parent.info description = item.parent.info
elseif defined(itemdir) and itemdir.info then
description = itemdir.info
else else
description = opts.plugin_global_popup_instructions description = opts.plugin_global_popup_instructions
end} end}

View file

@ -65,7 +65,7 @@ end}
print(' <object id="${item.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" /> -->
<text js_id="location" pos="0 6 -5" billboard="y" col="#BBFFFF" scale="10 10 1" text="${url}" emissive="0 0 0" roughness="0.3" metalness="0" /> <text js_id="location" pos="0 6 -5" billboard="y" col="#BBFFFF" scale="10 10 1" text="${url}" emissive="0 0 0" roughness="0.3" metalness="0" />
{if not defined(item) and defined(items) and #items > 0 then {if not defined(item) and defined(items) and #items > 0 then
local layout = itemdir.layout or '' local layout = itemdir.layout or ''

View file

@ -79,7 +79,16 @@ return {
v.file.state = nil v.file.state = nil
end end
end) end)
-- sort & apply dirs to plugin options
table.sort( app.cache.dirs) table.sort( app.cache.dirs)
foreach( app.plugin, function(k,plugin)
foreach( plugin.opts, function(k,opt)
if opt.type == 'dir' then
opt.values = app.cache.dirs
end
end)
end)
end, end,
scanDir = function(scandir,parent,seen) scanDir = function(scandir,parent,seen)

View file

@ -108,46 +108,46 @@ Welcome to this XR experience.<br/>
<div id="tips" style="display:none"> <div id="tips" style="display:none">
<br/> <br/>
<table> <table>
<tr> <tr>
<td width="200"> <td>
<b>AR/VR headset</b> <b>Keyboard+Mouse</b>
</td> </td>
<td> <td>
walk around / use right-longpinch (or thumbstick) for local teleports<br/> use the WASD-keys and mousedrag to navigate
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td width="200">
<b>PhoneVR</b> <b>AR/VR headset</b>
</td> </td>
<td> <td>
use gaze or bluetooth/mouse to navigate walk around / use right-longpinch (or thumbstick) for local teleports<br/>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<b>PhoneAR</b> <b>PhoneVR</b>
</td> </td>
<td> <td>
walk around and tap screen to click use gaze or bluetooth/mouse to navigate
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<b>Mobile</b> <b>PhoneAR</b>
</td> </td>
<td> <td>
use the WASD-keys and mousedrag to navigate walk around and tap screen to click
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<b>Keyboard+Mouse</b> <b>Mobile</b>
</td> </td>
<td> <td>
use the WASD-keys and mousedrag to navigate use the WASD-keys and mousedrag to navigate
</td> </td>
</tr> </tr>
</table> </table>
<br/> <br/>
You've got this! You've got this!

View file

@ -2,13 +2,14 @@ local portals = {
render = function(items) render = function(items)
local jml = '' local jml = ''
for i,v in pairs(items) do for i,v in pairs(items) do
local width = v.portal_width or tonumber(app.opts.plugin_global_portal_width )
local center = -((#items*width)/1.3) + (i*width)
local url = v.url local url = v.url
local thumb = ' shad..er_id="defaultportal"' local width = tonumber(app.opts.plugin_global_portal_width )
local thumb = ' shader_id="defaultportal"'
if #v.thumbnail > 0 then if #v.thumbnail > 0 then
thumb = ' thumb_id="img' .. i .. '" ' thumb = ' thumb_id="img' .. i .. '" '
if v.portal_width then width = tonumber(v.portal_width) end
end end
local center = -((#items*width)/1.3) + (i*width)
if v.dir then url = url .. "/" end if v.dir then url = url .. "/" end
if v.redirect then url = v.redirect end -- dont wrap standalone glb's in janusxr if v.redirect then url = v.redirect end -- dont wrap standalone glb's in janusxr
jml = jml jml = jml

View file

@ -20,7 +20,7 @@ local function scanForOpenBrush(exp)
end end
foreach( tags, function(k,tag) foreach( tags, function(k,tag)
tag = tag:gsub("#","") tag = tag:gsub("#","")
if exp.path:match('/' .. tag .. '/') then -- might not be precise enough if exp.path:match('/' .. tag:gsub("-","%%-") .. '/') then -- might not be precise enough
if not exp.parent.janusxr then if not exp.parent.janusxr then
exp.parent.janusxr = {} exp.parent.janusxr = {}
end end
@ -76,8 +76,8 @@ local plugin = {
key='app.opts.plugin_openbrush_layout_grid_size', key='app.opts.plugin_openbrush_layout_grid_size',
title='Grid size', title='Grid size',
info='in square meters (only used for grid layout)', info='in square meters (only used for grid layout)',
default="75", default="74",
placeholder="75", placeholder="74",
values='' values=''
}, },
{ {
@ -98,9 +98,9 @@ local plugin = {
key='app.opts.plugin_openbrush_sync_folder', key='app.opts.plugin_openbrush_sync_folder',
title='Sync to folder', title='Sync to folder',
info='openbrush (icosa.gallery) assets will be saved here', info='openbrush (icosa.gallery) assets will be saved here',
placeholder = "openbrush", default = "./myverse/tronverse",
default = app.opts.plugin_diskscan_dir, type = "dir",
values = {} values = {} -- populated by plugin/diskscan.lua
}, },
{ {
key='app.opts.plugin_openbrush_sync_folder_thumbnail', key='app.opts.plugin_openbrush_sync_folder_thumbnail',
@ -130,8 +130,8 @@ local plugin = {
key='app.opts.plugin_openbrush_sync_hashtag', key='app.opts.plugin_openbrush_sync_hashtag',
title='tags', title='tags',
info=( sync_URL and 'space-separated tags will generate their own folders' or ''), info=( sync_URL and 'space-separated tags will generate their own folders' or ''),
default = "#xrforge", default = "#tronverse-icosagallery",
placeholder = "#xrforge", placeholder = "#tronverse-icosagallery",
values='' values=''
}, },
{ {
@ -185,8 +185,8 @@ local plugin = {
key='app.opts.plugin_openbrush_sync_maxitems', key='app.opts.plugin_openbrush_sync_maxitems',
title='Max latest items', title='Max latest items',
info='limit the total amount of result', info='limit the total amount of result',
placeholder='3', placeholder='8',
default = "3", default = "8",
values="" values=""
}, },
{ {
@ -236,6 +236,17 @@ local plugin = {
return DecodeJson(opts.body) return DecodeJson(opts.body)
end, end,
ensureREADME = function(tag,file)
if not util.fileExist( file ) then
util.writefile( file, app.opts.plugin_global_popup_instructions .. '\n'
.. [[This virtual world is generated in realtime via <a href='https://icosa.gallery' target='_blank'>icosa.gallery</a>.
You can add/create parcels in icosa.gallery, by publishing via <a href='https://openbrush.app'>OpenBrush</a>, <a href='https://openblocks.app'>OpenBlocks</a>, or <a href='https://blender.org' target='_blank'>Blender</a>.<br>
> NOTE: Make sure to add hashtag **]] .. tag .. [[** to the title (and re-run xrforge for fast re-sync). ]]
)
end
end,
-- expected by src/.lua/apiSync.lua -- expected by src/.lua/apiSync.lua
download = function(t,tag) download = function(t,tag)
local me = app.plugin.openbrush local me = app.plugin.openbrush
@ -245,6 +256,7 @@ local plugin = {
local dirname = '_' .. asset.assetId .. '-' .. util.hyphenate( asset.displayName) local dirname = '_' .. asset.assetId .. '-' .. util.hyphenate( asset.displayName)
local dirnameFull = me.getFolder(tag) .. '/' .. dirname local dirnameFull = me.getFolder(tag) .. '/' .. dirname
local jsonfile = dirnameFull .. '/asset.json' local jsonfile = dirnameFull .. '/asset.json'
local README = me.getFolder(tag) .. '/README.md'
local sync = true local sync = true
local assetPrevious = false local assetPrevious = false
local f = false local f = false
@ -268,6 +280,8 @@ local plugin = {
os.execute("mkdir -p " .. dirnameFull) os.execute("mkdir -p " .. dirnameFull)
end end
me.ensureREADME(tag,README)
if util.fileExist(jsonfile) then if util.fileExist(jsonfile) then
assetPrevious = DecodeJson( util.readfile(jsonfile) ) assetPrevious = DecodeJson( util.readfile(jsonfile) )
if util.parseIsoDate(asset.updateTime) == util.parseIsoDate(assetPrevious.updateTime) then if util.parseIsoDate(asset.updateTime) == util.parseIsoDate(assetPrevious.updateTime) then