From 3219addf1b7eceef1659f4a2cdb31982f4a33944 Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Sun, 7 Jun 2026 14:48:52 +0200 Subject: [PATCH] wip: href clicks --- src/ext/gltf/main.lua | 2 +- src/ext/kitchensink/main.lua | 2 +- src/ext/xrfragments/lovr-xrf.lua | 96 ++++++++++++++++++++++++++++++++ src/ext/xrfragments/main.lua | 51 ++++------------- src/lovr/main.lua | 78 +++++++++++++++++++++++--- src/main.lua | 5 +- 6 files changed, 184 insertions(+), 50 deletions(-) create mode 100644 src/ext/xrfragments/lovr-xrf.lua diff --git a/src/ext/gltf/main.lua b/src/ext/gltf/main.lua index 114a0cf..6d2d110 100644 --- a/src/ext/gltf/main.lua +++ b/src/ext/gltf/main.lua @@ -14,7 +14,7 @@ local gltf = { local ecs = api.ecs ecs.add( ecs.world, { x = 0, - y = -7, + y = 0, z = 0, model = model }) diff --git a/src/ext/kitchensink/main.lua b/src/ext/kitchensink/main.lua index 8110087..60a4ec9 100644 --- a/src/ext/kitchensink/main.lua +++ b/src/ext/kitchensink/main.lua @@ -4,7 +4,7 @@ local sample = require "sample" return { name = "kitchensink", - enabled = true, + enabled = false, init = function() end, diff --git a/src/ext/xrfragments/lovr-xrf.lua b/src/ext/xrfragments/lovr-xrf.lua new file mode 100644 index 0000000..7d2ed17 --- /dev/null +++ b/src/ext/xrfragments/lovr-xrf.lua @@ -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 diff --git a/src/ext/xrfragments/main.lua b/src/ext/xrfragments/main.lua index 8b4437c..8fa3445 100644 --- a/src/ext/xrfragments/main.lua +++ b/src/ext/xrfragments/main.lua @@ -1,60 +1,33 @@ local api = ... -local xrf +local xrf = require("ext/xrfragments/lovr-xrf") +local xrfsystem return { enabled = true, init = function() -- create a (ecs) system which detects add/remove entities local ecs = api.ecs - xrf = ecs.system({ + xrfsystem = ecs.system({ filter = ecs.filter('x&y&z'), onAdd = function(self,obj) - function scanHrefs() - local tree = api.json.decode( obj['model']:getMetadata() ) - api.util.traverse( tree['nodes'], function(obj) - if obj['extras'] ~= nil then - if obj['extras']['href'] ~= nil then - 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 + xrf.traverseNodesContaining('href', obj, + xrf.makeClickable( api.ecs.worldPhysics, + function(obj, collider) + print(obj['name'] .. ".href => " .. obj['extras']['href'] ) + api.ecs.add( ecs.world, {collider = collider}) end - end) - end - if obj['model'] ~= nil then - pcall(scanHrefs) - end - end + ) + ) + end, onRemove = function(self,obj) end }) - ecs.addSystem( ecs.world, xrf ) + ecs.addSystem( ecs.world, xrfsystem ) end } diff --git a/src/lovr/main.lua b/src/lovr/main.lua index 2a745e7..444982a 100644 --- a/src/lovr/main.lua +++ b/src/lovr/main.lua @@ -15,6 +15,10 @@ api.iui = iui api.backend = backend local ecs = api.ecs +-- ecs systems +local interactionsUpdater +local renderer + --- @type Texture local envTex @@ -45,7 +49,7 @@ function lovr.update(dt) lovr.event.quit() end - ecs.update( ecs.world, pass, ecs.filter('updatesystem') ) + ecs.update( ecs.world, dt, ecs.filter('updatesystem') ) iui.beginFrame(dt) @@ -83,8 +87,22 @@ function lovr.draw(pass) if api.iui.idiom == "vr" then pass:setClear(0.5, 0.5, 0.5) end + api.exec(api.ext, "draw",pass) 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 api.mainWindow:draw(pass) end @@ -100,18 +118,23 @@ end if launch.mode == "desktop" then function lovr.mousemoved(x, y, dx, dy) backend.mousemoved(x, y, dx, dy) + interactionsUpdater['mouse']['moved'] = {x=x, y=y, button=button} end function lovr.mousepressed(x, y, button) backend.mousepressed(x, y, button) + interactionsUpdater['mouse']['pressed'] = {x=x, y=y, button=button} end function lovr.mousereleased(x, y, button) backend.mousereleased(x, y, button) + interactionsUpdater['mouse']['released'] = {x=x, y=y, button=button} + print("ja") end function lovr.wheelmoved(x, y) backend.wheelmoved(x, y) + interactionsUpdater['mouse']['wheel'] = {x=x, y=y} end function lovr.keypressed(key, scancode, isRepeat) @@ -131,8 +154,8 @@ local initECS = function(ecs) ecs.world = ecs.world() -- lovr.draw => ecs.drawsystem (render-logic thread) - local renderer = ecs.processingSystem() - renderer.filter = ecs.requireAny('model') -- add more types when needed + renderer = ecs.processingSystem() + renderer.filter = ecs.requireAny('model') renderer.drawsystem = true function renderer:process(obj, pass) if obj['model'] ~= nil then @@ -149,14 +172,53 @@ local initECS = function(ecs) ecs.addSystem( ecs.world, renderer ) -- lovr.update => ecs updatesystem (game-logic thread) - local update = ecs.processingSystem() - update.updatesystem = true - update.filter = ecs.requireAll('update') -- *TODO* might need filter later - function update:process(obj, dt) + local updater = ecs.processingSystem() + updater.updatesystem = true + updater.filter = ecs.requireAll('update') + function updater:process(obj, dt) print_r(obj['data']) end - ecs.addSystem( ecs.world, update ) + ecs.addSystem( ecs.world, updater ) 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) +initInteractions(api.ecs) diff --git a/src/main.lua b/src/main.lua index a0c0da3..76c1fce 100644 --- a/src/main.lua +++ b/src/main.lua @@ -20,4 +20,7 @@ if lovr ~= nil then api.exec( api.ext, 'init') 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)