2026-09-04 16:34:57 +02:00
local util = require ( ' util ' )
2026-09-11 18:42:12 +02:00
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
2026-09-04 16:34:57 +02:00
app.on ( ' init ' , function ( )
2026-09-11 18:42:12 +02:00
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
2026-09-04 16:34:57 +02:00
end )
2026-09-11 18:42:12 +02:00
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
2026-09-04 16:34:57 +02:00
end )
2026-09-11 18:42:12 +02:00
local function scanTiltFile ( exp )
if exp.path : lower ( ) : match ( " .tilt$ " ) then
app.markViewable ( exp )
end
end
2026-09-04 16:34:57 +02:00
2026-09-11 18:42:12 +02:00
local function scanForOpenBrush ( exp )
local tags = app.plugin . openbrush.tags
if not tags then
tags = util.split ( app.opts . plugin_openbrush_sync_hashtag , ' ' )
table.insert ( tags , " openbrush " )
app.plugin . openbrush.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_openbrush_wrap_room
exp.parent . portal_width = tonumber ( app.opts . plugin_openbrush_portal_width )
exp.parent . layout = app.opts . plugin_openbrush_layout
end
end )
scanTiltFile ( exp )
end
2026-09-04 16:34:57 +02:00
2026-09-11 18:42:12 +02:00
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 )
2026-09-04 16:34:57 +02:00
end )
local sync_URL = app.opts_plugin_openbrush_sync_URL or ' '
-- this registers the plugin UI
local plugin = {
name = " OpenBrush " ,
infowide = true ,
info = [ [
< img src = " img/openbrush.png " style = " width:50%; max-width:233px; display:inline-block; float:left; margin-right:20px; " />
< a href = " https://openbrush.app/ " target = " _blank " > OpenBrush </ a > lets you paint in 3 D space with virtual reality . < br >
XRForge ' s webviewer (<a href="https://github.com/jbaicoianu/janusweb">JanusWeb</a>) supports interconnecting OpenBrush projects via 3D portals.<br/>
2026-09-11 18:42:12 +02:00
OpenBrush projects are stored in a ( selfhosted ) < a href = " https://icosa.gallery " target = " _blank " > icosa.gallery </ a > instance , which can be configured below . < br >< br >
< b > NOTE : </ b > for collective virtualworld building use < b > hashtags in project - titles </ b > . Each hashtag will become its own folder , which allows for < a href = " /about#autocompose " > crowdsourcing / combining scenes </ a > designed in openbrush .
2026-09-04 16:34:57 +02:00
] ] ,
opts = {
2026-09-11 18:42:12 +02:00
{
key = ' app.opts.plugin_openbrush_wrap_room ' ,
title = ' Wrap VR space ' ,
info = ' render 3D files in <a href="https://coderofsalvation.github.io/janus-guide/#/build/room" target="_blank">janusxr.xml</a> room ' ,
default = " room_plane " ,
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 " ,
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 " ,
values = ' '
} ,
{
key = ' app.opts.plugin_openbrush_sync_strategy ' ,
title = ' How to sync ' ,
info = ' mirroring = most resilient; references = instant updates ' ,
default = " mirror " ,
values = { mirror = " mirror locally " , reference = " reference remote URL " }
} ,
2026-09-04 16:34:57 +02:00
{
key = ' app.opts.plugin_openbrush_sync_when ' ,
2026-09-11 18:42:12 +02:00
title = ' Sync interval ' ,
info = ' when to sync? ' ,
default = " boot " ,
values = { disabled = " disabled " , boot = " on boot " , hourly = " hourly " , daily = " daily " }
2026-09-04 16:34:57 +02:00
} ,
{
2026-09-11 18:42:12 +02:00
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 ' ,
2026-09-04 16:34:57 +02:00
title = ' Sync to folder ' ,
info = ' openbrush (icosa.gallery) assets will be saved here ' ,
placeholder = " openbrush " ,
2026-09-11 18:42:12 +02:00
default = app.opts . plugin_diskscan_dir ,
values = { }
2026-09-04 16:34:57 +02:00
} ,
{
2026-09-11 18:42:12 +02:00
key = ' app.opts.plugin_openbrush_sync_folder_thumbnail ' ,
2026-09-04 16:34:57 +02:00
title = ' Thumbnail ' ,
info = ' URL of Thumbnail for folder/portal ' ,
placeholder = " img/openbrush.png " ,
default = " img/openbrush.png " ,
values = ' '
} ,
2026-09-11 18:42:12 +02:00
{
key = ' app.opts.plugin_openbrush_portal_width ' ,
title = ' Portal spacing ' ,
info = ' Adjust spacing between portals (prevent thumbnail-overlap) ' ,
placeholder = " 4.2 " ,
default = " 4.2 " ,
values = ' '
} ,
2026-09-04 16:34:57 +02:00
{
key = ' app.opts.plugin_openbrush_sync_URL ' ,
title = ' Icosa gallery URL ' ,
info = ' public/selfhosted <a href="https://github.com/icosa-foundation/icosa-gallery target="_blank">icosa.gallery</a> server-instance ' ,
2026-09-11 18:42:12 +02:00
placeholder = " icosa.gallery " ,
default = " icosa.gallery " ,
values = ' '
} ,
{
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 " ,
2026-09-04 16:34:57 +02:00
values = ' '
} ,
{
key = ' app.opts.plugin_openbrush_sync_category ' ,
title = ' category ' ,
info = ( sync_URL and ' space-separated, leave empty or see <a href="https://api. ' .. sync_URL .. ' /v1/docs#/Assets/icosa_api_assets_get_assets">API docs</a> ' or ' ' ) ,
placeholder = " ART " ,
default = " " ,
values = ' '
} ,
{
key = ' app.opts.plugin_openbrush_sync_keywords ' ,
title = ' keywords ' ,
info = ( sync_URL and ' space-separated, leave empty or see <a href="https://api. ' .. sync_URL .. ' /v1/docs#/Assets/icosa_api_assets_get_assets">API docs</a> ' or ' ' ) ,
placeholder = " xrforge " ,
default = " " ,
values = ' '
} ,
{
key = ' app.opts.plugin_openbrush_sync_triangle_max ' ,
title = ' Polygons maximum ' ,
info = ( sync_URL and ' performance-cap, leave empty or see <a href="https://api. ' .. sync_URL .. ' /v1/docs#/Assets/icosa_api_assets_get_assets">API docs</a> ' or ' ' ) ,
placeholder = " 10000 " ,
default = " " ,
values = ' '
} ,
{
key = ' app.opts.plugin_openbrush_sync_author ' ,
title = ' Author name ' ,
2026-09-11 18:42:12 +02:00
info = ' space-separated: filter assets on specific Author name ' ,
2026-09-04 16:34:57 +02:00
placeholder = ' sarah ' ,
default = " " ,
values = " "
} ,
{
key = ' app.opts.plugin_openbrush_sync_authorid ' ,
title = ' Author id ' ,
2026-09-11 18:42:12 +02:00
info = ' space-separated: filter assets on specific Author id ' ,
2026-09-04 16:34:57 +02:00
placeholder = ' 9e8b98fbe ' ,
default = " " ,
values = " "
} ,
2026-09-11 18:42:12 +02:00
{
key = ' app.opts.plugin_openbrush_sync_maxitems ' ,
title = ' Max latest items ' ,
info = ' limit the total amount of result ' ,
placeholder = ' 3 ' ,
default = " 3 " ,
values = " "
} ,
2026-09-04 16:34:57 +02:00
{
key = ' app.opts.plugin_openbrush_sync_collection ' ,
2026-09-11 18:42:12 +02:00
title = ' In collection ' ,
2026-09-04 16:34:57 +02:00
info = ' should assets be part of a collection? ' ,
2026-09-11 18:42:12 +02:00
default = " disabled " ,
values = { enabled = " yes " , disabled = " no " }
2026-09-04 16:34:57 +02:00
}
2026-09-11 18:42:12 +02:00
} ,
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 )
.. ( 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_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
-- .. (#app.opts.plugin_openbrush_sync_author > 0 and '&authorName=' .. EscapeFragment(app.opts.plugin_openbrush_sync_author) or '')
-- .. (#app.opts.plugin_openbrush_sync_authorid > 0 and '&authorId=' .. EscapeFragment(app.opts.plugin_openbrush_sync_authorid) or '')
end ,
getFolder = function ( tag )
local folder = app.opts . plugin_openbrush_sync_folder
if tag then
folder = folder .. ' / ' .. util.hyphenate ( tag )
end
return folder
end ,
fetch = function ( url , opts )
opts = opts or { }
if not opts.body then
local ok , headers , body = Fetch ( url )
if not ok then
return print ( " [openbrush.lua] error: could not fetch: " .. app.plugin . openbrush.apiCall ( tag ) )
end
opts.body = body
end
return DecodeJson ( opts.body )
end ,
download = function ( t , tag )
local me = app.plugin . openbrush
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 or app.opts . plugin_openbrush_sync_bruteforce == ' enabled ' then
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
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 ( ' [openbrush.lua] ' .. asset.displayName .. ' is up to date ' )
end
else
print ( ' [openbrush.lua] error: no suitable format found for ' .. asset.displayName .. ' ' .. asset.assetId )
end
end
end , true )
return synced
end
2026-09-04 16:34:57 +02:00
}
2026-09-11 18:42:12 +02:00
if not util.cmdExist ( ' wget ' ) then
plugin.info = plugin.info .. ' <br><br><b>NOTE:</b> wget command not installed..plugin is disabled '
end
2026-09-04 16:34:57 +02:00
return plugin