local util = require('util') local apiSync = require('apiSync') app.on('init', function() app.plugin.openbrush.api = apiSync.init( 'openbrush' ) 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.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.grid_size = tonumber( app.opts.plugin_openbrush_grid_size ) exp.parent.layout = app.opts.plugin_openbrush_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_openbrush_sync_URL or '' -- this registers the plugin UI local plugin = { name = "OpenBrush", 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 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="", 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="grid", values={portals="As portals", grid="As grid"} }, { key='app.opts.plugin_openbrush_layout_grid_size', title='Grid size', 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='download assets locally (prevent linkrot)', default = "mirror", values={mirror="mirror locally"} }, { key='app.opts.plugin_openbrush_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_openbrush_sync_folder', title='Sync to folder', info='openbrush (icosa.gallery) assets will be saved here', placeholder = "openbrush", default = app.opts.plugin_diskscan_dir, values = {} }, { key='app.opts.plugin_openbrush_sync_folder_thumbnail', title='Thumbnail', info='URL of Thumbnail for folder/portal', placeholder = "img/openbrush.png", default = "img/openbrush.png", values='' }, { key='app.opts.plugin_openbrush_portal_width', title='Portal spacing', info='Spacing between portals (prevent thumbnail-overlap)', placeholder = "4.2", default = "4.2", values='' }, { key='app.opts.plugin_openbrush_sync_URL', title='Icosa gallery URL', info='public/selfhosted icosa.gallery server-instance', 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 = "#xrforge", placeholder = "#xrforge", values='' }, { key='app.opts.plugin_openbrush_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_openbrush_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_openbrush_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_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', info='space-separated: filter assets on specific Author name', placeholder='sarah', default = "", values="" }, { key='app.opts.plugin_openbrush_sync_authorid', title='Author id', info='space-separated: filter assets on specific Author id', placeholder='9e8b98fbe', default = "", values="" }, { key='app.opts.plugin_openbrush_sync_maxitems', title='Max latest items', info='limit the total amount of result', placeholder='3', default = "3", values="" }, { key='app.opts.plugin_openbrush_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_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_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 -- .. (#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, -- 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("[openbrush.lua] error: could not fetch: " .. app.plugin.openbrush.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.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 then print('✅ ' .. asset.displayName .. ' (syncing) #openbrush') util.writefile( jsonfile, EncodeJson(asset) ) local rootfile = dirnameFull .. '/' .. f.root.url:gsub(".*/","") 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 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) #openbrush') end else print('[openbrush.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