From 81b9d238a9c4d1aaa30fa93f29b949fbaeccb4e0 Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Tue, 15 Sep 2026 20:41:30 +0200 Subject: [PATCH] improved default popup for mobile + openbrush/manyfold centralized refactor --- src/.init.lua | 32 ++- src/.lua/apiSync.lua | 86 ++++++-- src/css/xrforge.css | 2 + src/page/admin/index.html | 7 +- src/page/admin/index.lua | 6 +- src/page/admin/plugin.html | 16 +- src/page/dir.lua | 1 - src/plugin/diskscan.lua | 1 - src/plugin/global.lua | 95 +++++---- src/plugin/janusxr/layout/grid.lua | 43 ++-- src/plugin/manyfold.lua | 332 +++++++++++++++++++++++++---- src/plugin/openbrush.lua | 86 +++----- src/plugin/scheduler.lua | 2 + 13 files changed, 521 insertions(+), 188 deletions(-) diff --git a/src/.init.lua b/src/.init.lua index 1bf3c8a..4f3dd31 100644 --- a/src/.init.lua +++ b/src/.init.lua @@ -31,6 +31,7 @@ app = require("soakbean") { app.reset = function() app.cache = { items = {}, url = {}, shared = require('sharedcache'), dirs={} } end +app.reset() app.addPlugin = function(file) local p = require(file) @@ -50,7 +51,8 @@ end app.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 + local postfix = '_' .. ( experience.parent and experience.parent.id or tostring(os.time()) ) + app.cache.items[ino .. postfix ] = experience else app.cache.items[ino] = experience end @@ -63,9 +65,7 @@ end -- api skeleton (implemented via soakbean's app.on(...)) app.set = function(k,v) end -app.init = function() - app.reset() -end +app.init = function() end -- load config app.opts = require('config')(app) @@ -97,19 +97,16 @@ if unix.getpid() == 0 then unix.exit(0) end -- child-threads shall not pass beyo app.runcmd(app) -- run clicmds -print = function(...) Log(kLogInfo, ... or "''") end -- nice redbean logging from now on +print = function(...) Log(kLogInfo, ... or "''") end -- nice redbean logging from now on -- init URL router app.url['^/data'] = '/data.lua' -- setup custom file endpoint app.use( app.plugin.global.middleware.homepage_2D_or_3D ) -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('^/about[/]?$', util.pagerender('page/about.html')) - app.post('^/save', function(req,res,next) -- setup inline POST endpoint -- also .get(), .put(), .delete(), .options() app.cache = req.body -- middleware auto-decodes json @@ -125,7 +122,17 @@ app -- return user == app.opts.admin_user and pass == app.opts.admin_pass end })) -.use( require("json").middleware() ) -- try plug'n'play json API middleware + +app.url['^/admin[/]?$'] = '/page/admin/index.lua' +app.use( require('middleware/onlyGET')({exclude={'^/admin'}}) ) -- we are staticwebgen-first server-last +app.get('/admin/server/reset', function(req,res,next) -- easy way to reload/rescan xrforge + app.reset() + res.status(303) + res.header('Location', req.header['Referer']:gsub("?.*","") .. '?notify=Server%20is%20being%20reloaded.' ) + res.send(req,res) +end) + +app.use( require("json").middleware() ) -- try plug'n'play json API middleware .use( require('page/dir').middleware ) -- directory browser .use( app.router( app.url ) ) -- try url router .use( app.response() ) -- try serve app response (if any) @@ -149,3 +156,10 @@ OnServerStart = function() app.init() -- this triggers init event end +OnServerStop = function() + -- run the 'reset' cli-cmd equivalent (as childprocesses might leave + -- the terminal in a confused state) + io.write("\27[H\27[2J\27[3J\27[0m") + io.flush() +end + diff --git a/src/.lua/apiSync.lua b/src/.lua/apiSync.lua index bcbce85..cbee13a 100644 --- a/src/.lua/apiSync.lua +++ b/src/.lua/apiSync.lua @@ -1,22 +1,76 @@ +-- this is a helper/orchestrator for apicalls/syncs +-- +-- implemented in openbrush.lua + local apiSync = { - syncHashtags = function(opts) - local synced = 0 - local tags = opts.tags - if #tags == 0 then table.insert(tags,'') end - foreach( tags, function(k,tag) - local url = opts.apiCall( #tag > 0 and tag or nil) - opts.body = nil -- erase previous url result - print('🤞 fetch ' .. url) - local t = opts.fetch(url,opts) - if t then - synced = synced + opts.download(t,tag) - end - end, true) - if synced > 0 then - app.plugin.diskscan.scan(app.opts.plugin_diskscan_dir) + init = function(pluginname) + local plugin = app.plugin[pluginname] + local api = {} + api.pluginname = pluginname + if not plugin.apiCall or not plugin.fetch or not plugin.download then + print("apiSync.lua: plugin " .. pluginname .. " does not implement the apiSync interface") + os.exit() end - return synced + api.apiCall = plugin.apiCall + api.fetch = plugin.fetch + api.download = plugin.download + + api.syncHashtags = function(opts) + local synced = 0 + local tags = opts.tags + if #tags == 0 then table.insert(tags,'') end + foreach( tags, function(k,tag) + local url = opts.apiCall( #tag > 0 and tag or nil) + opts.body = nil -- erase previous url result + print('🤞 fetch ' .. url) + local t = opts.fetch(url,opts) + if t then + synced = synced + opts.download(t,tag) + end + end, true) + if synced > 0 then + app.cache.url = {} -- delete page cache + app.plugin.diskscan.scan(app.opts.plugin_diskscan_dir) + end + return synced + end + + api.sync = function(api) + if #app.opts['plugin_' .. pluginname .. '_sync_URL'] > 0 and util.cmdExist('wget') then + local synced = api.syncHashtags({ + tags = util.split( app.opts['plugin_' .. api.pluginname .. '_sync_hashtag'] or '', ' '), + apiCall = api.apiCall, + fetch = api.fetch, + download = api.download + }) + end + end + + -- runs sync in foreground + app.on('reset', function() + api.sync(api) + end) + + -- runs sync in foreground + 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 + end) + + -- runs sync background + app.on('scheduler', function(opts) + if util.contains(argv, 'server') then + if opts.interval == app.opts['plugin_' .. pluginname .. '_sync_when'] then + api.sync(api) + end + end + end) + return api end } diff --git a/src/css/xrforge.css b/src/css/xrforge.css index be4757a..4880e82 100644 --- a/src/css/xrforge.css +++ b/src/css/xrforge.css @@ -57,6 +57,7 @@ td,div,p{ input, textarea, +button, select{ box-sizing: border-box; margin: 0px 0px 10px 0px; @@ -91,6 +92,7 @@ input[type=submit]{ .btn a:active, .btn a:visited, .btn a:link, +button, input[type=submit] { font-size:15px; display: inline-block; diff --git a/src/page/admin/index.html b/src/page/admin/index.html index 90ebc27..dd8fca6 100644 --- a/src/page/admin/index.html +++ b/src/page/admin/index.html @@ -3,7 +3,9 @@ ${header}

-${notification} +{if #notification > 0 then + print('
' .. notification .. '
') +end}
@@ -19,6 +21,9 @@ ${notification}
+
diff --git a/src/page/admin/index.lua b/src/page/admin/index.lua index f4214c8..9cece6d 100644 --- a/src/page/admin/index.lua +++ b/src/page/admin/index.lua @@ -1,6 +1,6 @@ local util = require("util") -local data = {notification=''} +local data = {notification= GetParam('notify') or ''} data.plugins = "" data.tabs = {} @@ -87,9 +87,9 @@ function saveData() end) if saveconfig then app.set('app.opts', app.opts) - data.notification = '
updated
' + data.notification = 'updated' else - data.notification = '
no changes detected
' + data.notification = 'no changes detected' end end end diff --git a/src/page/admin/plugin.html b/src/page/admin/plugin.html index b426fd3..4bc5030 100644 --- a/src/page/admin/plugin.html +++ b/src/page/admin/plugin.html @@ -31,7 +31,21 @@ ]]) end end} -

+
+ {if true then + local btns = plugin.buttons or {} + if #btns > 0 then + for i,btn in pairs(btns) do + print([[ +
+ ]] .. (btn.label or '') .. [[ +
+ ]]) + end + print('
') + end + end} +
${opts} diff --git a/src/page/dir.lua b/src/page/dir.lua index 87eb25a..6b21820 100644 --- a/src/page/dir.lua +++ b/src/page/dir.lua @@ -99,7 +99,6 @@ return { -- what to do here? no generated portals? data.janusxr = app.tpl( util.readfile(v.path), data ) end - print_r(data.item) end end end) diff --git a/src/plugin/diskscan.lua b/src/plugin/diskscan.lua index 113632b..6e68dba 100644 --- a/src/plugin/diskscan.lua +++ b/src/plugin/diskscan.lua @@ -43,7 +43,6 @@ return { end, scan = function(scandir) - app.reset() -- flush cache local me = app.plugin.diskscan local seen = {symlinks={}} -- ensure rootdir as experience diff --git a/src/plugin/global.lua b/src/plugin/global.lua index 8439721..4415588 100644 --- a/src/plugin/global.lua +++ b/src/plugin/global.lua @@ -98,51 +98,60 @@ local plugin = { title='Viewer instructions', info='Default popup for 3D viewer instructions', default = [[ -

Tips to navigate in XR

- - - - - - - - - - - - - - - - - - - - - -
- AR/VR headset - - walk around / use right-longpinch (or thumbstick) for local teleports
-
- PhoneVR - - use gaze or bluetooth/mouse to navigate -
- PhoneAR - - walk around and tap screen to click -
- Mobile - - use the WASD-keys and mousedrag to navigate -
- Keyboard+Mouse - - use the WASD-keys and mousedrag to navigate -
+

Hi there! 👋

+Welcome to this XR experience.
+
+
+ +
+ +
]], control='textarea', diff --git a/src/plugin/janusxr/layout/grid.lua b/src/plugin/janusxr/layout/grid.lua index 68cb450..617b124 100644 --- a/src/plugin/janusxr/layout/grid.lua +++ b/src/plugin/janusxr/layout/grid.lua @@ -27,11 +27,12 @@ local grid = { local index = 0 for i, v in pairs(items) do local url = v.redirect and v.redirect or v.url + local overlay = v.title:match('#overlay') and true or false -- Position current item local pos = { - x = v.title:match("#overlay") and 0 or x * gridsize, + x = overlay and 0 or x * gridsize, y = 0, - z = v.title:match("#overlay") and 0 or z * gridsize + z = overlay and 0 or z * gridsize } local posStr = pos.x .. ' ' .. pos.y .. ' ' .. pos.z local is_nested_room = false @@ -41,24 +42,26 @@ local grid = { else jml = jml .. ' \n' end - index = index + 1 - -- Move to next position in the spiral - if index < #items then - x = x + dx - z = z + dz - stepsTaken = stepsTaken + 1 - if stepsTaken == steps then - stepsTaken = 0 - -- Rotate clockwise: right -> up -> left -> down - local oldDx = dx - dx = -dz - dz = oldDx - turns = turns + 1 - -- Increase the length after every second turn - if turns % 2 == 0 then - steps = steps + 1 - end - end + if not overlay then + index = index + 1 + -- Move to next position in the spiral + if index < #items then + x = x + dx + z = z + dz + stepsTaken = stepsTaken + 1 + if stepsTaken == steps then + stepsTaken = 0 + -- Rotate clockwise: right -> up -> left -> down + local oldDx = dx + dx = -dz + dz = oldDx + turns = turns + 1 + -- Increase the length after every second turn + if turns % 2 == 0 then + steps = steps + 1 + end + end + end end end return jml diff --git a/src/plugin/manyfold.lua b/src/plugin/manyfold.lua index 625b2de..17faa8b 100644 --- a/src/plugin/manyfold.lua +++ b/src/plugin/manyfold.lua @@ -1,64 +1,312 @@ -local util = require('util') +local util = require('util') +local apiSync = require('apiSync') -app.on('scan_experience', function(exp) - --print_r(exp) -end) +--app.on('init', function() +-- app.plugin.manyfold.api = apiSync.init( 'manyfold' ) +--end) -app.on('scan_experience_file', function(exp) -end) +local function scanTiltFile(exp) + if exp.path:lower():match(".tilt$") then + app.markViewable(exp) + end +end + +local function scanForOpenBrush(exp) + local tags = app.plugin.manyfold.tags + if not tags then + tags = util.split( app.opts.plugin_manyfold_sync_hashtag, ' ') + table.insert( tags,"manyfold") + app.plugin.manyfold.tags = tags + end + foreach( tags, function(k,tag) + tag = tag:gsub("#","") + if exp.path:match('/' .. tag .. '/') then -- might not be precise enough + if not exp.parent.janusxr then + exp.parent.janusxr = {} + end + exp.parent.janusxr.room = app.opts.plugin_manyfold_wrap_room + exp.parent.portal_width = tonumber( app.opts.plugin_manyfold_portal_width ) + exp.parent.layout = app.opts.plugin_manyfold_layout + end + end) + scanTiltFile(exp) +end + + +--app.on('scan_experience', scanForOpenBrush ) +--app.on('scan_experience_file', scanForOpenBrush ) +--app.on("scan_experience_janusxr", function(opts) scanForOpenBrush(opts.exp) end) app.on('set', function(k,v) end) +local sync_URL = app.opts_plugin_manyfold_sync_URL or '' + -- this registers the plugin UI local plugin = { - name = "Manyfold", - tab = " Manyfold", - info = '', + name = "ManyFold", + infowide = true, + info = [[ + + OpenBrush lets you paint in 3D space with virtual reality.
+ XRForge's webviewer (JanusWeb) supports interconnecting OpenBrush projects via 3D portals or a grid.
+ OpenBrush projects are stored in a (selfhosted) icosa.gallery instance, which can be configured below.

+ NOTE: for collective virtualworld building use hashtags in project-titles. Each hashtag will become its own folder, which allows for crowdsourcing/combining scenes designed in manyfold. + ]], + --buttons = { + -- { label = "resync now!", href="/reset" } + --}, opts = { { - key='app.opts.plugin_manyfold_instance_1', - title='Instance 1 URL', - info='Manyfold URL goes here (https://xrforge.isvery.ninja e.g.)', - default="https://xrforge.isvery.ninja", - values='' + key='app.opts.plugin_manyfold_wrap_room', + title='Wrap VR space', + info='render 3D files in janusxr.xml room', + default="room_plane", + values= app.plugin.janusxr.opts[4].values -- reuse rooms from janusxr plugin }, { - key='app.opts.plugin_manyfold_instance_2', - title='Instance 2 URL', - info='Manyfold URL goes here (https://xrforge.isvery.ninja e.g.)', - default="", - values='' + key='app.opts.plugin_manyfold_layout', + title='Layout assets', + info='NOTE: use #overlay in asset-title to skip layout (sky/floor/e.g.)', + default="grid", + values={portals="As portals", grid="As grid"} }, { - key='app.opts.plugin_manyfold_instance_3', - title='Instance 3 URL', - info='Manyfold URL goes here (https://xrforge.isvery.ninja e.g.)', - default="", + key='app.opts.plugin_manyfold_layout_grid_size', + title='Grid size', + info='in square meters (only used for grid layout)', + default="3", + placeholder="3", values='' }, { key='app.opts.plugin_manyfold_sync_strategy', - title='Sync strategy', - info="Specify how content from manyfold instances should be fetched", - default="recent", - values={recent='only recent',all_recent='everything + recent', disabled='disabled'} + title='How to sync', + info='download assets locally (prevent linkrot)', + default = "mirror", + values={mirror="mirror locally"} }, - --{ - -- key='app.opts.plugin_manyfold_sync_strategy', - -- title='Instance 3 URL', - -- info='Specify how content from manyfold instances should be fetched", - -- default="recent", - -- values={recent='only recent',all_recent='everything + recent', disabled='disabled'} - --}, - --{ - -- key='app.opts.plugin_manyfold_sync_interval', - -- title='Instance 3 URL', - -- info='Specify how often xrforge should check for recent items #serveronly' - -- default="recent", - -- values={recent='only recent',all_recent='everything + recent', disabled='disabled'} - --}, - } + { + key='app.opts.plugin_manyfold_sync_when', + title='Sync interval', + info='when to sync?', + default = "boot", + values={disabled="disabled", boot="on boot", hourly="hourly", daily="daily"} + }, + { + key='app.opts.plugin_manyfold_sync_folder', + title='Sync to folder', + info='manyfold (icosa.gallery) assets will be saved here', + placeholder = "manyfold", + default = app.opts.plugin_diskscan_dir, + values = {} + }, + { + key='app.opts.plugin_manyfold_sync_folder_thumbnail', + title='Thumbnail', + info='URL of Thumbnail for folder/portal', + placeholder = "img/manyfold.png", + default = "img/manyfold.png", + values='' + }, + { + key='app.opts.plugin_manyfold_portal_width', + title='Portal spacing', + info='Spacing between portals (prevent thumbnail-overlap)', + placeholder = "4.2", + default = "4.2", + values='' + }, + { + key='app.opts.plugin_manyfold_sync_URL', + title='Icosa gallery URL', + info='public/selfhosted icosa.gallery server-instance', + placeholder = "icosa.gallery", + default = "icosa.gallery", + values='' + }, + { + key='app.opts.plugin_manyfold_sync_hashtag', + title='tags', + info=( sync_URL and 'space-separated tags will generate their own folders' or ''), + default = "#1 #xrforge", + placeholder = "#1 #xrforge", + values='' + }, + { + key='app.opts.plugin_manyfold_sync_category', + title='category', + info=( sync_URL and 'space-separated, leave empty or see API docs' or ''), + placeholder = "ART", + default = "", + values='' + }, + { + key='app.opts.plugin_manyfold_sync_keywords', + title='keywords', + info=( sync_URL and 'space-separated, leave empty or see API docs' or ''), + placeholder = "xrforge", + default = "", + values='' + }, + { + key='app.opts.plugin_manyfold_sync_triangle_max', + title='Polygons maximum', + info=( sync_URL and 'performance-cap, leave empty or see API docs' or ''), + placeholder = "10000", + default = "", + values='' + }, + { + key='app.opts.plugin_manyfold_sync_license', + title='License', + info='filter assets on specific license', + default = "", + values={ [""]="", ALL_CC="CreateCommons (ALL)"} + }, + { + key='app.opts.plugin_manyfold_sync_author', + title='Author name', + info='space-separated: filter assets on specific Author name', + placeholder='sarah', + default = "", + values="" + }, + { + key='app.opts.plugin_manyfold_sync_authorid', + title='Author id', + info='space-separated: filter assets on specific Author id', + placeholder='9e8b98fbe', + default = "", + values="" + }, + { + key='app.opts.plugin_manyfold_sync_maxitems', + title='Max latest items', + info='limit the total amount of result', + placeholder='3', + default = "3", + values="" + }, + { + key='app.opts.plugin_manyfold_sync_collection', + title='In collection', + info='should assets be part of a collection?', + default = "disabled", + values={enabled="yes", disabled="no"} + } + }, + + -- expected by src/.lua/apiSync.lua + apiCall = function(tag) + return 'https://api.' .. app.opts.plugin_manyfold_sync_URL .. '/v1/assets?xrforge=' .. EscapeFragment(app.host) + .. '&xrforge_strategy=' .. EscapeFragment(app.opts.plugin_manyfold_sync_strategy) + .. '&time=' .. tostring( os.time() ) + .. (tag and #tag > 0 and '&keywords=' .. EscapeFragment(tag) or '') + .. (#app.opts.plugin_manyfold_sync_category > 0 and '&category=' .. EscapeFragment(app.opts.plugin_manyfold_sync_category) or '') + .. (#app.opts.plugin_manyfold_sync_keywords > 0 and '&keywords=' .. EscapeFragment(app.opts.plugin_manyfold_sync_keywords) or '') + .. (#app.opts.plugin_manyfold_sync_triangle_max > 0 and '&triangleCountMax=' .. EscapeFragment(app.opts.plugin_manyfold_sync_triangle_max) or '') + .. (#app.opts.plugin_manyfold_sync_license > 0 and '&license=' .. app.opts.plugin_manyfold_sync_license or '' ) + .. (app.opts.plugin_manyfold_sync_collection == 'enabled' and '&inCollection=true' or '' ) + .. (#app.opts.plugin_manyfold_sync_maxitems > 0 and '&pageSize=' .. EscapeFragment(app.opts.plugin_manyfold_sync_maxitems) or '' ) + -- TODO: filter AFTER the request + -- .. (#app.opts.plugin_manyfold_sync_author > 0 and '&authorName=' .. EscapeFragment(app.opts.plugin_manyfold_sync_author) or '') + -- .. (#app.opts.plugin_manyfold_sync_authorid > 0 and '&authorId=' .. EscapeFragment(app.opts.plugin_manyfold_sync_authorid) or '') + end, + + getFolder = function(tag) + local folder = app.opts.plugin_manyfold_sync_folder + if tag then + folder = folder .. '/' .. util.hyphenate(tag) + end + return folder + end, + + -- expected by src/.lua/apiSync.lua + fetch = function(url,opts) + opts = opts or {} + if not opts.body then + local ok, headers, body = Fetch( url ) + if not ok then + return print("[manyfold.lua] error: could not fetch: " .. app.plugin.manyfold.apiCall(tag)) + end + opts.body = body + end + return DecodeJson(opts.body) + end, + + -- expected by src/.lua/apiSync.lua + download = function(t,tag) + local me = app.plugin.manyfold + local synced = 0 + foreach( t.assets, function(k,asset) + if type(asset) == 'table' then + local dirname = '_' .. asset.assetId .. '-' .. util.hyphenate( asset.displayName) + local dirnameFull = me.getFolder(tag) .. '/' .. dirname + local jsonfile = dirnameFull .. '/asset.json' + local sync = true + local assetPrevious = false + local f = false + + function preferFormat(f) + local found = false + foreach( asset.formats, function(k,v) + if v.formatType == f then + found = v + end + end) + return found + end + + f = preferFormat('TILT') + if not f then f = preferFormat('GLTF2') end + if not f then f = preferFormat('GLTF1') end + if not f then f = preferFormat('OBJ') end + if f then + if not util.fileExist( dirnameFull ) then + os.execute("mkdir -p " .. dirnameFull) + end + + if util.fileExist(jsonfile) then + assetPrevious = DecodeJson( util.readfile(jsonfile) ) + if util.parseIsoDate(asset.updateTime) == util.parseIsoDate(assetPrevious.updateTime) then + sync = false -- already finished + end + end + if sync then + print('✅ ' .. asset.displayName .. ' (syncing) #manyfold') + util.writefile( jsonfile, EncodeJson(asset) ) + local rootfile = dirnameFull .. '/' .. f.root.url:gsub(".*/","") + local thumbnailfile = '' + if asset.thumbnail then + thumbnailfile = dirnameFull .. '.' .. asset.thumbnail.url:gsub(".*%.",'') + print('🤞 fetch ' .. asset.thumbnail.url) + os.execute('wget "' ..asset.thumbnail.url .. '" -O "' .. thumbnailfile .. '"') + end + print('🤞 fetch ' .. f.root.url) + os.execute('wget "' .. f.root.url .. '" -O "' .. rootfile .. '"') + if #f.resources > 0 then + foreach( f.resources, function(k,resource) + local file = dirnameFull .. '/' .. resource.url:gsub(".*/","") + print('🤞 fetch ' .. resource.url) + os.execute('wget "' .. resource.url .. '" -O "' .. file .. '"') + end) + end + synced = synced + 1 + else + print('â„šī¸ ' .. asset.displayName .. ' (up to date) #manyfold') + end + else + print('[manyfold.lua] error: no suitable format found for ' .. asset.displayName .. ' ' .. asset.assetId) + end + end + end, true) + return synced + end } +if not util.cmdExist('wget') then + plugin.info = plugin.info .. '

NOTE: wget command not installed..plugin is disabled' +end + return plugin diff --git a/src/plugin/openbrush.lua b/src/plugin/openbrush.lua index 1ce884c..0c31b8f 100644 --- a/src/plugin/openbrush.lua +++ b/src/plugin/openbrush.lua @@ -1,34 +1,8 @@ -local util = require('util') +local util = require('util') local apiSync = require('apiSync') -local syncIcosaGallery = function() - if #app.opts.plugin_openbrush_sync_URL > 0 then - local synced = apiSync.syncHashtags({ - tags = util.split( app.opts.plugin_openbrush_sync_hashtag or '', ' '), - apiCall = app.plugin.openbrush.apiCall, - fetch = app.plugin.openbrush.fetch, - download = app.plugin.openbrush.download - }) - end -end - 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 - syncIcosaGallery() - end -end) - -app.on('scheduler', function(opts) - if util.contains(argv, 'server') then - if util.cmdExist('wget') and opts.interval == app.opts.plugin_openbrush_sync_when then - syncIcosaGallery() - end - end + app.plugin.openbrush.api = apiSync.init( 'openbrush' ) end) local function scanTiltFile(exp) @@ -52,6 +26,7 @@ local function scanForOpenBrush(exp) end exp.parent.janusxr.room = app.opts.plugin_openbrush_wrap_room exp.parent.portal_width = tonumber( app.opts.plugin_openbrush_portal_width ) + exp.parent.grid_size = tonumber( app.opts.plugin_openbrush_grid_size ) exp.parent.layout = app.opts.plugin_openbrush_layout end end) @@ -75,39 +50,42 @@ local plugin = { info = [[ OpenBrush lets you paint in 3D space with virtual reality.
- XRForge's webviewer (JanusWeb) supports interconnecting OpenBrush projects via 3D portals.
+ XRForge's webviewer (JanusWeb) supports interconnecting OpenBrush projects via 3D portals or a grid.
OpenBrush projects are stored in a (selfhosted) icosa.gallery instance, which can be configured below.

NOTE: for collective virtualworld building use hashtags in project-titles. Each hashtag will become its own folder, which allows for crowdsourcing/combining scenes designed in openbrush. ]], + --buttons = { + -- { label = "resync now!", href="/reset" } + --}, opts = { { key='app.opts.plugin_openbrush_wrap_room', title='Wrap VR space', info='render 3D files in janusxr.xml room', - default="room_plane", + default="", values= app.plugin.janusxr.opts[4].values -- reuse rooms from janusxr plugin }, { key='app.opts.plugin_openbrush_layout', title='Layout assets', info='NOTE: use #overlay in asset-title to skip layout (sky/floor/e.g.)', - default="portals", + default="grid", values={portals="As portals", grid="As grid"} }, { key='app.opts.plugin_openbrush_layout_grid_size', title='Grid size', - info='in square meters (unused for non-grid layout)', - default="3", - placeholder="3", + info='in square meters (only used for grid layout)', + default="75", + placeholder="75", values='' }, { key='app.opts.plugin_openbrush_sync_strategy', title='How to sync', - info='mirroring = most resilient; references = instant updates', + info='download assets locally (prevent linkrot)', default = "mirror", - values={mirror="mirror locally",reference="reference remote URL"} + values={mirror="mirror locally"} }, { key='app.opts.plugin_openbrush_sync_when', @@ -116,13 +94,6 @@ local plugin = { default = "boot", values={disabled="disabled", boot="on boot", hourly="hourly", daily="daily"} }, - { - key='app.opts.plugin_openbrush_sync_bruteforce', - title='Sync anyways', - info='ignore timestamps', - default = "enabled", - values={disabled="disabled", enabled="enabled"} - }, { key='app.opts.plugin_openbrush_sync_folder', title='Sync to folder', @@ -142,7 +113,7 @@ local plugin = { { key='app.opts.plugin_openbrush_portal_width', title='Portal spacing', - info='Adjust spacing between portals (prevent thumbnail-overlap)', + info='Spacing between portals (prevent thumbnail-overlap)', placeholder = "4.2", default = "4.2", values='' @@ -159,8 +130,8 @@ local plugin = { key='app.opts.plugin_openbrush_sync_hashtag', title='tags', info=( sync_URL and 'space-separated tags will generate their own folders' or ''), - default = "#1 #xrforge", - placeholder = "#1 #xrforge", + default = "#xrforge", + placeholder = "#xrforge", values='' }, { @@ -187,6 +158,13 @@ local plugin = { default = "", values='' }, + { + key='app.opts.plugin_openbrush_sync_license', + title='License', + info='filter assets on specific license', + default = "", + values={ [""]="", ALL_CC="CreateCommons (ALL)"} + }, { key='app.opts.plugin_openbrush_sync_author', title='Author name', @@ -220,14 +198,16 @@ local plugin = { } }, + -- expected by src/.lua/apiSync.lua apiCall = function(tag) return 'https://api.' .. app.opts.plugin_openbrush_sync_URL .. '/v1/assets?xrforge=' .. EscapeFragment(app.host) .. '&xrforge_strategy=' .. EscapeFragment(app.opts.plugin_openbrush_sync_strategy) + .. '&time=' .. tostring( os.time() ) .. (tag and #tag > 0 and '&keywords=' .. EscapeFragment(tag) or '') .. (#app.opts.plugin_openbrush_sync_category > 0 and '&category=' .. EscapeFragment(app.opts.plugin_openbrush_sync_category) or '') .. (#app.opts.plugin_openbrush_sync_keywords > 0 and '&keywords=' .. EscapeFragment(app.opts.plugin_openbrush_sync_keywords) or '') .. (#app.opts.plugin_openbrush_sync_triangle_max > 0 and '&triangleCountMax=' .. EscapeFragment(app.opts.plugin_openbrush_sync_triangle_max) or '') - .. (app.opts.plugin_openbrush_sync_strategy == 'mirror' and '&license=ALL_CC' or '' ) + .. (#app.opts.plugin_openbrush_sync_license > 0 and '&license=' .. app.opts.plugin_openbrush_sync_license or '' ) .. (app.opts.plugin_openbrush_sync_collection == 'enabled' and '&inCollection=true' or '' ) .. (#app.opts.plugin_openbrush_sync_maxitems > 0 and '&pageSize=' .. EscapeFragment(app.opts.plugin_openbrush_sync_maxitems) or '' ) -- TODO: filter AFTER the request @@ -243,6 +223,7 @@ local plugin = { return folder end, + -- expected by src/.lua/apiSync.lua fetch = function(url,opts) opts = opts or {} if not opts.body then @@ -255,6 +236,7 @@ local plugin = { return DecodeJson(opts.body) end, + -- expected by src/.lua/apiSync.lua download = function(t,tag) local me = app.plugin.openbrush local synced = 0 @@ -292,11 +274,13 @@ local plugin = { sync = false -- already finished end end - if sync or app.opts.plugin_openbrush_sync_bruteforce == 'enabled' then + if sync then + print('✅ ' .. asset.displayName .. ' (syncing) #openbrush') util.writefile( jsonfile, EncodeJson(asset) ) local rootfile = dirnameFull .. '/' .. f.root.url:gsub(".*/","") - local thumbnailfile = dirnameFull .. '.' .. asset.thumbnail.url:gsub(".*%.",'') - if app.opts.plugin_openbrush_layout == 'portals' then + local thumbnailfile = '' + if asset.thumbnail and asset.thumbnail.url then + thumbnailfile = dirnameFull .. '.' .. asset.thumbnail.url:gsub(".*%.",'') print('🤞 fetch ' .. asset.thumbnail.url) os.execute('wget "' ..asset.thumbnail.url .. '" -O "' .. thumbnailfile .. '"') end @@ -311,7 +295,7 @@ local plugin = { end synced = synced + 1 else - print('[openbrush.lua] ' .. asset.displayName .. ' is up to date') + print('â„šī¸ ' .. asset.displayName .. ' (up to date) #openbrush') end else print('[openbrush.lua] error: no suitable format found for ' .. asset.displayName .. ' ' .. asset.assetId) diff --git a/src/plugin/scheduler.lua b/src/plugin/scheduler.lua index df79ed4..a31de3e 100644 --- a/src/plugin/scheduler.lua +++ b/src/plugin/scheduler.lua @@ -47,6 +47,8 @@ return { if pid ~= nil then unix.kill( pid, unix.SIGTERM) end + print("gracefully stopping child-processes") + unix.wait() unix.exit() end) end