This commit is contained in:
Leon van Kammen 2026-08-28 11:26:17 +02:00
parent 9360423a5c
commit f4b633d560
28 changed files with 180 additions and 58 deletions

View file

@ -53,6 +53,10 @@ $ ./lovr xurfer
...
```
## Longterm goal
* Android launcher for OpenXR & WebXR apps
## Security
Instead of the traditional sandbox-in-a-browser-model (Chromium/Firefox e.g.), we suggest running XURFER via [bubblewrap](https://github.com/meta-quest/bubblewrap)

19
nix/luax-release.sh Executable file
View file

@ -0,0 +1,19 @@
#!/bin/sh
export PATH=$PATH:.
which luax || {
wget "https://cdelord.fr/pub/luax/10.10.0/luax-linux-x86_64-musl"
ln -s luax-linux-x86_64-musl luax
chmod +x luax
}
cd xurfer
files="$(find * -type f | grep lua | grep -vE '(runtime/lov*)')"
echo $files
luax compile -t list | tail -n+3 | awk '$1 ~ /[a-z]-[a-z]/ {print $1}' | \
while read platform; do
#luax compile -t $platform -o ./../xurfer-$platform ${files}
# TODO: luaX needs unique modulenames : main.lua clashes in the binary
luax compile -t $platform -o ./../xurfer-$platform conf.lua ecs.lua events.lua ext/kitchensink/main.lua ext/xrfragments/main.lua #ext/xrfragments/level2.lua ext/xrfragments/level4.lua ext/xrfragments/lovr-xrf.lua ext/skeleton/main.lua ext/unixy/main.lua ext/gazecontrol/main.lua ext/janusxr/main.lua ext/http-lovr/main.lua ext/startupscene/main.lua ext/3DFile/main.lua ext/tween/main.lua ext/tween/tween.lua ext/AudioFile/main.lua ext/button/main.lua ext/URI/main.lua init.lua lib/lust.lua lib/tiny-ecs.lua lib/xmlSimple.lua lib/json.lua lib/url.lua main.lua runtime/lua/conf.lua runtime/lua/main.lua runtime/lua/api.lua runtime/detect.lua test/main.lua util.lua
exit
done

View file

@ -1,8 +1,13 @@
#!/bin/sh
export DEBUG=1
cd xurfer
runtime="/home/leon/apps/lovr-v0.18.0-x86_64.AppImage"
#./main.lua
#runtime="/home/leon/apps/lovr-v0.18.0-x86_64.AppImage"
runtime="/home/leon/apps/lovr-v0.18.1-x86_64.AppImage"
#appimage-run $runtime . test.glb
appimage-run $runtime . https://snips.sh/f/_U5-XctEVE?r=1
#appimage-run $runtime . http://localhost:8080/simple-a.glb
#appimage-run $runtime . http://localhost:8080/level3-4-playaudio.glb
#appimage-run $runtime . http://localhost:8080/simple-b.glb
#appimage-run $runtime . test.localhost.jml0
#appimage-run $runtime . http://localhost:8080/simple-b-wireframe.glb

View file

@ -1,4 +1,5 @@
-- default argument
--@LIB
if arg[1] == nil then
arg[1] = "https://snips.sh/f/_U5-XctEVE?r=1"
end

View file

@ -1,3 +1,4 @@
--@LIB
local ecs = require("lib/tiny-ecs")
ecs.init = function(api)

10
xurfer/events.lua Normal file
View file

@ -0,0 +1,10 @@
--@LIB
return {
input = {
pointer = { released = false, pressed = false, moved = false, hover = false, wheel = false},
key = { released = false, pressed = false },
input = false, -- string if true,
collider = false, -- lovr collider
hitpoint = false, -- {x=0, y=0, z=0}
}
}

View file

@ -1,3 +1,4 @@
--@LIB
local api = ...
local ecs = api.ecs

View file

@ -1,3 +1,4 @@
--@LIB
local api = ...
local ecs = api.ecs

View file

@ -1,3 +1,4 @@
--@LIB
local api = ...
local ecs = api.ecs

View file

@ -0,0 +1,63 @@
--@LIB
local api = ...
local ecs = api.ecs
-- this is an input extension which adds data the input-event
-- which is fed to update-calls of extensions and ECS
local button
button = {
name = "button",
enabled = true,
settings = { -- keep this settings serializable (no functions e.g.) + use lovr key-names
-- (SHIFT) TAB/ENTER as described in the Accessibility guidelines
-- of XR (URI) fragments: https://xrfragment.org/#accessibility
{ event={name="next"}, key="tab" },
{ event={name="previous"}, key="tab", shift=true},
{ event={name="select"}, key="return", pointer={released=true} },
{ event={name="select"}, key="space", pointer={released=true} },
},
state = { shift = false },
init = function()
if not api.settings.button then
api.settings.button = button.settings
end
end,
isShift = function(key) return key == "rshift" or key == "lshift" end,
matchKey = function(mapping, event)
return mapping.key ~= nil and
mapping.shift == (button.state.shift or nil) and
mapping.key == event.key
end,
input = function(dt,input)
-- sense shift state
if input.key.pressed and button.isShift(input.key.pressed.key) then button.state.shift = true end
if input.key.released and button.isShift(input.key.released.key) then button.state.shift = false end
local mappings = api.settings.button
-- process keymapping
if input.key.released then
foreach(mappings, function(k,mapping)
if button.matchKey( mapping, input.key.released ) then
input[ mapping.event.name ] = mapping.event -- add to input event (so update-calls can see it)
end
end)
end
-- process keymapping
if input.pointer.released then
foreach(mappings, function(k,mapping)
if mapping.pointer and mapping.pointer.released then
input[ mapping.event.name ] = mapping.event -- add to input event (so update-calls can see it)
end
end)
end
end,
}
return button

View file

@ -1,3 +1,4 @@
--@LIB
local api = ...
local ecs = api.ecs
@ -25,7 +26,6 @@ gctl = {
init = function()
--lovr.mouse.setRelativeMode(true)
gctl.initVisor()
gctl.extendECS()
api.player.matrix = lovr.math.newMat4()
end,
@ -84,23 +84,6 @@ gctl = {
pass:plane(x, y, z, scale, scale, angle, ax, ay, az)
end,
-- ECS API docs: https://bakpakin.github.io/tiny-ecs/doc
extendECS = function()
local visorSystem = ecs.processingSystem({
updatethread = true,
filter = ecs.requireAll('mycomponent'),
onAdd = function(self,obj) print_r(obj) end,
onRemove = function(self,obj) end,
process = function(self,obj) end,
})
ecs.addSystem( api.world, visorSystem )
ecs.add( api.world, { visor = gctl.visor } )
--gctl.visor.commit() -- notify other systems to update entitycache
end
}
return gctl

View file

@ -1,3 +1,4 @@
--@LIB
-- this extension will make network requests run async in LoVR runtime
local api = ...

View file

@ -1,5 +1,5 @@
--@LIB
local api = ...
local sample
return {

View file

@ -1,3 +1,4 @@
--@LIB
local api = ...
local ecs = api.ecs

View file

@ -1,3 +1,4 @@
--@LIB
local api = ...
local envTex
local shader

View file

@ -1,3 +1,4 @@
--@LIB
local api = ...
local tween = require "ext/tween/tween" -- https://github.com/kikito/tween.lua

View file

@ -1,3 +1,4 @@
--@LIB
local tween = {
_VERSION = 'tween 2.1.1',
_DESCRIPTION = 'tweening for lua',

View file

@ -1,3 +1,4 @@
--@LIB
-- The unixy extension allows proxying api-functions through cli apps.
-- Why?
-- Sometimes the host OS already has cli-apps which can make up for missing lua functionality.

View file

@ -1,3 +1,4 @@
--@LIB
local api = ...
local xrf = require("ext/xrfragments/lovr-xrf")
local url = require("url")

View file

@ -1,3 +1,4 @@
--@LIB
local xrf = require("ext/xrfragments/lovr-xrf")
local url = require("url")

View file

@ -1,3 +1,4 @@
--@LIB
-- Copyright 2026 Leon van Kammen / coderofsalvation. All rights reserved.
--
-- Redistribution and use in source and binary forms, with or without modification, are

View file

@ -1,3 +1,4 @@
--@LIB=xrfragment/main.lua
local api = ...
local url = require('url')
local ecs = api.ecs
@ -19,7 +20,7 @@ xrfimpl = {
local href = obj.node.extras.href
trace("\n[xrf] href was clicked: " .. href)
api.ext.exec("onClick", obj, collider)
local ok = xrf.level2.load( href, obj, api.ext.URI.current, xrfimpl.onLoad)
local ok = xrf.level2.load( href, obj, referer, xrfimpl.onLoad)
or
xrf.level4.import( href, obj, referer, xrfimpl.onImport)

View file

@ -1 +1,2 @@
--@LIB
print("hello!")

View file

@ -85,6 +85,7 @@ function url.parse(url_string)
URN = "",
isLocal = false
}
if not url_string then return URI end
local remaining = url_string
local protocol = string.match(remaining, "^(%w+://)") -- protocol
if protocol then
@ -152,11 +153,18 @@ url.getAbsolute = function(href,referer,keephash)
if referer ~= nil and URL.protocol == 'file' and URLref.protocol ~= 'file' then
URL.protocol = URLref.protocol or ''
URL.domain = URLref.domain or ''
URL.string = URL.protocol .. '://' .. URL.domain .. '/' .. URL.path .. URL.hash
if keephash then
URL.URN = URL.string
URL.string = URL.protocol .. '://' .. URL.domain .. '/'
if href[1] == "/" then
URL.string = URL.string .. URL.path
else
URL.URN = URL.string:gsub("#.*", "")
URL.string = URL.string ..
URLref.path:sub( 0, URLref.path:find( URLref.file,1,true)-1 ) ..
URL.file
end
URL.string = URL.string .. URL.hash
URL = url.parse(URL.string) -- reparse
if keephash then
URL.hash = URLref.hash
end
end
URL.referer = referer

View file

@ -1,6 +1,6 @@
---------------------------------------------------------------------------------
---------------------------------------------------------------------------------
--
--@LIB
-- xml.lua - XML parser for use with the Corona SDK.
-- https://github.com/Cluain/Lua-Simple-XML-Parser
--

View file

@ -1,4 +1,7 @@
#!/usr/bin/env lua
--please do not remove the following luaX tag:
--@MAIN
--
-- -- GLB URLS
-- ./lovr xurfer https://coderofsalvation.codeberg.page/xrfragment-haxe/example/assets/example.glb?bar=1&f=2#foo'
-- ./lovr xurfer https://codeberg.org/coderofsalvation/xrfragment/raw/branch/main/assets/template/website/website.glb'
@ -37,6 +40,10 @@ api = {
max = 10000, -- max plugins (used for sorting)
exec = function(...) util.exec(api.ext,...) end -- util function to call func on each extension
},
settings = { -- settings to store to ~/.config/xurfer/config.lua e.g.
version="0.0.1"
},
events = require('events'),
i = 1
}

View file

@ -81,6 +81,7 @@ function lovr.load()
end
function lovr.update(dt)
local event = interactions.collect()
ecs.update( api.world, dt, ecs.filterUpdate )
iui.beginFrame(dt)
@ -89,7 +90,7 @@ function lovr.update(dt)
-- In desktop mode, we use IUI's standard window API to fill the screen.
iui.beginWindow(lovr.system.getWindowDimensions())
api.ext.exec("update", dt, interactions.event )
api.ext.exec("update", dt, event )
iui.endWindow()
@ -99,7 +100,7 @@ function lovr.update(dt)
if mainWindow:beginFrame() then
iui.beginWindow(mainWindow.w, mainWindow.h)
api.ext.exec("update", dt, interactions.event )
api.ext.exec("update", dt, event )
iui.endWindow()
@ -108,6 +109,7 @@ function lovr.update(dt)
end
iui.endFrame()
interactions.clear()
end
function lovr.draw(pass)
@ -129,7 +131,7 @@ function lovr.draw(pass)
-- -- Dot
--if interactions.selectedBox then
--if interactions.hitpoint then
-- pass:setColor(0, 0, 1)
-- pass:sphere( interactions.hitpoint, .01)
--end
@ -173,7 +175,6 @@ end
function lovr.wheelmoved(x, y)
backend.wheelmoved(x, y)
interactions.pointer.wheel = {x=x, y=y}
print_r(interactions.pointer.wheel)
end
function lovr.keypressed(key, scancode, isRepeat)
@ -197,12 +198,13 @@ function initInteractions(ecs)
interactions = ecs.processingSystem()
interactions.updatethread = true
interactions.filter = function(obj) return true end -- always run
interactions.pointer = util.clone( api.events.input.pointer )
interactions.key = util.clone( api.events.input.key )
interactions.input = ""
-- collider vars
interactions.pointer = { released = false, pressed = false, moved = false, hover = false, wheel = false}
interactions.key = { released = false, pressed = false }
interactions.input = ""
interactions.hitpoint = vector()
interactions.selectedBox = nil
interactions.direction = quat():direction()
interactions.clearTable = function(o) foreach( o, function(k,v) o[k] = false end ) end
interactions.clear = function()
interactions.clearTable( interactions.pointer )
@ -210,23 +212,20 @@ function initInteractions(ecs)
interactions.input = false
end
function interactions:process(obj, dt)
ecs.worldPhysics:update(dt)
local castsrc = 'hand/left/point'
local castsrc = 'head'
interactions.collect = function(dt)
local castsrc = 'head' -- 'hand/left/point'
local event = util.clone(api.events.input)
interactions.event = event -- for convenience (ecs systems can always reach it)
local ox, oy, oz = lovr.headset.getPosition(castsrc)
local direction = quat(lovr.headset.getOrientation(castsrc)):direction()
if direction ~= nil then
local dx, dy, dz = vector( direction * 50 ):unpack()
interactions.direction = quat(lovr.headset.getOrientation(castsrc)):direction()
if interactions.direction ~= nil then
local dx, dy, dz = vector( interactions.direction * 50 ):unpack()
local collider, shape, x, y, z = ecs.worldPhysics:raycast(ox, oy, oz, ox + dx, oy + dy, oz + dz)
if collider then
interactions.pointer.hover = true
local node = collider:getUserData()
interactions.selectedBox = collider
interactions.pointer.hover = true
interactions.hitpoint.x = x
interactions.hitpoint.y = y
interactions.hitpoint.z = z
@ -239,22 +238,29 @@ function initInteractions(ecs)
interactions.pointer.released = true
end
end
if interactions.pointer.released and node.onclick ~= nil then
node.onclick(node,collider)
end
event.collider = collider
event.node = node
end
end
event.direction = util.clone(direction)
event.input = interactions.input
event.pointer = util.clone(interactions.pointer)
event.key = util.clone(interactions.key)
event.hitpoint = { x = interactions.hitpoint.x, y = interactions.hitpoint.y, z = interactions.z }
--event.head = { x = ox, y = oy, z = oz, direction = direction},
api.ext.exec('input', dt, event )
return event
end
function interactions:process(obj, dt)
ecs.worldPhysics:update(dt)
local event = interactions.event
if event.collider then
--if event.pointer.released and event.node and event.node.onclick ~= nil then
if event.select and event.node and event.node.onclick ~= nil then
event.node.onclick(event.node, event.collider)
end
end
-- backup & reset stuff
interactions.event = {
input = util.clone(interactions.input),
pointer = util.clone(interactions.pointer),
key = util.clone(interactions.key),
hitpoint = { x = interactions.hitpoint.x, y = interactions.hitpoint.y, z = interactions.z },
--head = { x = ox, y = oy, z = oz, direction = direction},
collider = collider
}
-- clear interactions
interactions.clear()
end
ecs.addSystem( api.world, interactions )
end

View file

@ -1,3 +1,4 @@
--@LIB
local lust = require 'lust'
local describe, it, expect = lust.describe, lust.it, lust.expect