wip: href clicks

This commit is contained in:
Leon van Kammen 2026-06-07 14:48:52 +02:00
parent 0d8c32a444
commit 3219addf1b
6 changed files with 184 additions and 50 deletions

View file

@ -14,7 +14,7 @@ local gltf = {
local ecs = api.ecs local ecs = api.ecs
ecs.add( ecs.world, { ecs.add( ecs.world, {
x = 0, x = 0,
y = -7, y = 0,
z = 0, z = 0,
model = model model = model
}) })

View file

@ -4,7 +4,7 @@ local sample = require "sample"
return { return {
name = "kitchensink", name = "kitchensink",
enabled = true, enabled = false,
init = function() end, init = function() end,

View file

@ -0,0 +1,96 @@
local json = require("json")
local xrf = {}
xrf.traverseNodesContaining = function(key,obj,cb)
local metadata = json.decode( obj['model']:getMetadata() )
-- Blender stores extras in single-object glb's to 'mesh' but multi-object glb's to 'nodes'
-- hence node_or_mesh
local keys = {'nodes','meshes'}
for k, tkey in pairs(keys) do
local i = 1
local tree = metadata[tkey]
xrf.traverse( tree, function(node)
if node['extras'] ~= nil then
if node['extras'][key] ~= nil then
cb(node,obj['model'], i, metadata)
end
end
i = i + 1
end)
end
end
xrf.getNodeFromMesh = function(mesh,metadata, i)
local node
if mesh['mesh'] == nil then
local extras = mesh['extras']
node = metadata['nodes'][i]
node['extras'] = extras
end
return node
end
xrf.calcDimensions = function(bbox)
local w = math.abs( bbox['maxx'] - bbox['minx'] )
local h = math.abs( bbox['maxy'] - bbox['miny'] )
local d = math.abs( bbox['maxz'] - bbox['minz'] )
return w, h, d
end
xrf.calcCenteredWorldPosition = function(node, model, bbox)
-- adjust for meshes which don't have origin in their center
local x, y, z = model:getNodePosition(node['name'], 'root') -- worldcoords
x = x + ( ( bbox['maxx'] + bbox['minx']) /2 )
y = y + ( ( bbox['maxy'] + bbox['miny']) /2 )
z = z + ( ( bbox['maxz'] + bbox['minz']) /2 )
return x, y, z
end
xrf.calcScale = function(node)
return {
node['scale'] and node['scale'][1] or 1,
node['scale'] and node['scale'][2] or 1,
node['scale'] and node['scale'][3] or 1,
}
end
xrf.makeClickable = function( physicsWorld, cb)
return function(node_or_mesh,model, i, metadata)
local node = node_or_mesh
if node['mesh'] == nil then node = xrf.getNodeFromMesh(node_or_mesh, metadata, i) end
print("making node " .. node['name'] .. " clickable")
local meshindex = 1 + node['mesh']
local mesh = model:getMesh( meshindex )
local minx, maxx, miny, maxy, minz, maxz = mesh:getBoundingBox()
local bbox = {maxx=maxx, minx=minx, maxy=maxy, miny=miny, maxz=maxz, minz=minz}
local w, h, d = xrf.calcDimensions( bbox )
local x, y, z = xrf.calcCenteredWorldPosition( node, model, bbox )
local scale = xrf.calcScale(node)
-- we cannot use newMeshCollider because storage type of modelmesh is not cpu
local collider = physicsWorld:newBoxCollider(
x, y, z,
scale[1] * (w + 0.01), -- add 0.01 to avoid 0
scale[2] * (h + 0.01), -- in case of
scale[3] * (d + 0.01) -- planes e.g.
)
collider:setUserData( {name = node['name'], node = node, onclick = cb })
end
end
-- utility function to traverse recursive table
xrf.traverse = function(arr, cb, key)
if key == nil then key = 'children' end
for k, child in pairs(arr) do
if type(child) == 'table' then
cb(child)
if type(child[key]) == 'table' then
if child[key][1] then
xrf.traverse( child[key], cb, key )
end
end
end
end
end
return xrf

View file

@ -1,60 +1,33 @@
local api = ... local api = ...
local xrf local xrf = require("ext/xrfragments/lovr-xrf")
local xrfsystem
return { return {
enabled = true, enabled = true,
init = function() init = function()
-- create a (ecs) system which detects add/remove entities -- create a (ecs) system which detects add/remove entities
local ecs = api.ecs local ecs = api.ecs
xrf = ecs.system({ xrfsystem = ecs.system({
filter = ecs.filter('x&y&z'), filter = ecs.filter('x&y&z'),
onAdd = function(self,obj) onAdd = function(self,obj)
function scanHrefs() xrf.traverseNodesContaining('href', obj,
local tree = api.json.decode( obj['model']:getMetadata() ) xrf.makeClickable( api.ecs.worldPhysics,
api.util.traverse( tree['nodes'], function(obj) function(obj, collider)
if obj['extras'] ~= nil then print(obj['name'] .. ".href => " .. obj['extras']['href'] )
if obj['extras']['href'] ~= nil then api.ecs.add( ecs.world, {collider = collider})
print(obj['name'] .. ".href => " .. obj['extras']['href'] )
-- create collider
-- The "trick" is to set collider:setUserData on creation, to something unique that will act as some sort of identifier for your object. So when a hit is detected from world:raycast you get to know which object this collider is attached to. Hope it makes sense!
--To achieve this in Lua, you can create a metatable that uses the __newindex metamethod to intercept assignments to the table's fields. Whenever x is updated, the onUpdate function will be called with the table itself as the argument.
--Here's how you can implement it:
--
--
--foo = { x = 0, y = 0, z = 0, onUpdate = function(self) print("Updated!") end }
--
---- Create a metatable
--local mt = {
-- __newindex = function(t, key, value)
-- rawset(t, key, value) -- Set the value
-- if key == "x" then
-- t.onUpdate(t) -- Call onUpdate if x is updated
-- end
-- end
--}
--
---- Set the metatable for foo
--setmetatable(foo, mt)
end
end end
end) )
end )
if obj['model'] ~= nil then end,
pcall(scanHrefs)
end
end
onRemove = function(self,obj) onRemove = function(self,obj)
end end
}) })
ecs.addSystem( ecs.world, xrf ) ecs.addSystem( ecs.world, xrfsystem )
end end
} }

View file

@ -15,6 +15,10 @@ api.iui = iui
api.backend = backend api.backend = backend
local ecs = api.ecs local ecs = api.ecs
-- ecs systems
local interactionsUpdater
local renderer
--- @type Texture --- @type Texture
local envTex local envTex
@ -45,7 +49,7 @@ function lovr.update(dt)
lovr.event.quit() lovr.event.quit()
end end
ecs.update( ecs.world, pass, ecs.filter('updatesystem') ) ecs.update( ecs.world, dt, ecs.filter('updatesystem') )
iui.beginFrame(dt) iui.beginFrame(dt)
@ -83,8 +87,22 @@ function lovr.draw(pass)
if api.iui.idiom == "vr" then if api.iui.idiom == "vr" then
pass:setClear(0.5, 0.5, 0.5) pass:setClear(0.5, 0.5, 0.5)
end end
api.exec(api.ext, "draw",pass) api.exec(api.ext, "draw",pass)
ecs.update( ecs.world, pass, ecs.filter('drawsystem') ) ecs.update( ecs.world, pass, ecs.filter('drawsystem') )
-- Dot
if selectedBox then
pass:setColor(0, 0, 1)
pass:sphere(hitpoint, .01)
end
-- Laser pointer
local hand = vec3(lovr.headset.getPosition('hand/left/point'))
local direction = quat(lovr.headset.getOrientation('hand/left/point')):direction()
pass:setColor(1, 1, 1)
pass:line(hand, selectedBox and hitpoint or (hand + direction * 50))
if api.mainWindow then if api.mainWindow then
api.mainWindow:draw(pass) api.mainWindow:draw(pass)
end end
@ -100,18 +118,23 @@ end
if launch.mode == "desktop" then if launch.mode == "desktop" then
function lovr.mousemoved(x, y, dx, dy) function lovr.mousemoved(x, y, dx, dy)
backend.mousemoved(x, y, dx, dy) backend.mousemoved(x, y, dx, dy)
interactionsUpdater['mouse']['moved'] = {x=x, y=y, button=button}
end end
function lovr.mousepressed(x, y, button) function lovr.mousepressed(x, y, button)
backend.mousepressed(x, y, button) backend.mousepressed(x, y, button)
interactionsUpdater['mouse']['pressed'] = {x=x, y=y, button=button}
end end
function lovr.mousereleased(x, y, button) function lovr.mousereleased(x, y, button)
backend.mousereleased(x, y, button) backend.mousereleased(x, y, button)
interactionsUpdater['mouse']['released'] = {x=x, y=y, button=button}
print("ja")
end end
function lovr.wheelmoved(x, y) function lovr.wheelmoved(x, y)
backend.wheelmoved(x, y) backend.wheelmoved(x, y)
interactionsUpdater['mouse']['wheel'] = {x=x, y=y}
end end
function lovr.keypressed(key, scancode, isRepeat) function lovr.keypressed(key, scancode, isRepeat)
@ -131,8 +154,8 @@ local initECS = function(ecs)
ecs.world = ecs.world() ecs.world = ecs.world()
-- lovr.draw => ecs.drawsystem (render-logic thread) -- lovr.draw => ecs.drawsystem (render-logic thread)
local renderer = ecs.processingSystem() renderer = ecs.processingSystem()
renderer.filter = ecs.requireAny('model') -- add more types when needed renderer.filter = ecs.requireAny('model')
renderer.drawsystem = true renderer.drawsystem = true
function renderer:process(obj, pass) function renderer:process(obj, pass)
if obj['model'] ~= nil then if obj['model'] ~= nil then
@ -149,14 +172,53 @@ local initECS = function(ecs)
ecs.addSystem( ecs.world, renderer ) ecs.addSystem( ecs.world, renderer )
-- lovr.update => ecs updatesystem (game-logic thread) -- lovr.update => ecs updatesystem (game-logic thread)
local update = ecs.processingSystem() local updater = ecs.processingSystem()
update.updatesystem = true updater.updatesystem = true
update.filter = ecs.requireAll('update') -- *TODO* might need filter later updater.filter = ecs.requireAll('update')
function update:process(obj, dt) function updater:process(obj, dt)
print_r(obj['data']) print_r(obj['data'])
end end
ecs.addSystem( ecs.world, update ) ecs.addSystem( ecs.world, updater )
end end
function initInteractions(ecs)
ecs.worldPhysics = lovr.physics.newWorld(0, 0, 0)
interactionsUpdater = ecs.processingSystem()
interactionsUpdater.updatesystem = true
interactionsUpdater.filter = ecs.requireAll('collider')
-- collider vars
interactionsUpdater.mouse = { released = false}
interactionsUpdater.hitpoint = lovr.math.newVec3()
interactionsUpdater.selectedBox = nil
function interactionsUpdater:process(obj, dt)
ecs.worldPhysics:update(dt)
local ox, oy, oz = lovr.headset.getPosition('hand/left/point')
local dx, dy, dz = quat(lovr.headset.getOrientation('hand/left/point')):direction():mul(50):unpack()
local collider, shape, x, y, z = ecs.worldPhysics:raycast(ox, oy, oz, ox + dx, oy + dy, oz + dz)
if collider then
if lovr.headset.isDown(hand, 'trigger') then
local node = collider:getUserData()
selectedBox = collider
hitpoint:set(x, y, z)
print( "collide with " .. node['name'] .. " => " .. node['extras']['href'] )
print("buttondown")
end
if lovr.headset.wasReleased(hand, 'trigger') or interactionsUpdater['mouse']['released'] then
print('click!')
end
end
-- reset mouse
mouse['released'] = false
mouse['pressed'] = false
end
ecs.addSystem( ecs.world, interactionsUpdater )
end
initECS(api.ecs) initECS(api.ecs)
initInteractions(api.ecs)

View file

@ -20,4 +20,7 @@ if lovr ~= nil then
api.exec( api.ext, 'init') api.exec( api.ext, 'init')
end end
api.browser.to("https://coderofsalvation.codeberg.page/xrfragment-haxe/example/assets/example.glb?bar=1&f=2#foo") local url='https://coderofsalvation.codeberg.page/xrfragment-haxe/example/assets/example.glb?bar=1&f=2#foo'
--local url = 'https://codeberg.org/coderofsalvation/xrfragment/raw/branch/main/assets/template/website/website.glb'
--local url = 'https://codeberg.org/coderofsalvation/xrfragment/raw/branch/main/assets/simple-a.glb'
api.browser.to(url)