Merge pull request #2 from coderofsalvation/dev

v1 href-fragment + docs
This commit is contained in:
Coder of Salvation / Leon van Kammen 2023-05-23 12:35:33 +02:00 committed by GitHub
commit 0b1fafc884
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 10925 additions and 859 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
dist/*.pyc
src/spec/tmp.json
tags

2
.vimrc
View file

@ -1,4 +1,4 @@
noremap <silent> <F8> :!haxe --no-output %<CR>
noremap <silent> <F9> :!./make doc<CR>
noremap <silent> <F9> :!./make build_js<CR>
noremap <silent> <F10> :!./make && echo OK && ./make tests<CR>
noremap <silent> <F11> :!./make tests \| less<CR>

View file

@ -3,19 +3,25 @@
[![Actions Status](https://github.com/coderofsalvation/xrfragment/workflows/test/badge.svg)](https://github.com/coderofsalvation/xrfragment/actions)
# usage
# Documentation
https://coderofsalvation.github.io/xrfragment
# available implementations
* [lua (handwritten)](dist/xrfragment.lua) [(+example)](src/xrfragment/Parser.lua)
* [haXe](src/xrfragment)
* [javascript](dist/xrfragment.js) [(+example)](test/test.js)
* [python](dist/xrfragment.py) [(+example)](test/test.py)
* [lua](dist/xrfragment.lua) [(+example)](test/test.lua)
* [javascript](dist/xrfragment.js) [(+example)](test/test.js)
* [python](dist/xrfragment.py) [(+example)](test/test.py)
* [lua](dist/xrfragment.lua) [(+example)](test/test.lua)
See documentation for more info
# development
Pre-build libraries can be found in [/dist folder](dist)<br>
If you really want to build from source:
```
$ ./make install
$ ./make && ./make runtest
$ ./make build && ./make runtest
```

3
dist/license.js vendored Normal file
View file

@ -0,0 +1,3 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 Leon van Kammen/NLNET

1431
dist/xrfragment.aframe.js vendored Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 Leon van Kammen/NLNET
var $hx_exports = typeof exports != "undefined" ? exports : typeof window != "undefined" ? window : typeof self != "undefined" ? self : this;
(function ($global) { "use strict";
$hx_exports["xrfragment"] = $hx_exports["xrfragment"] || {};

View file

@ -1,3 +1,6 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 Leon van Kammen/NLNET
var $hx_exports = typeof exports != "undefined" ? exports : typeof window != "undefined" ? window : typeof self != "undefined" ? self : this;
(function ($global) { "use strict";
$hx_exports["xrfragment"] = $hx_exports["xrfragment"] || {};
@ -593,183 +596,605 @@ xrfragment_XRF.isUrlOrPretypedView = new EReg("(^#|://)?\\..*","");
xrfragment_XRF.isString = new EReg(".*","");
})({});
var xrfragment = $hx_exports["xrfragment"];
xrfragment.xrf = {}
xrfragment.model = {}
// wrapper to survive in/outside modules
xrfragment.init = function(opts){
xrfragment.InteractiveGroup = function(THREE,renderer,camera){
let {
Group,
Matrix4,
Raycaster,
Vector2
} = THREE
const _pointer = new Vector2();
const _event = { type: '', data: _pointer };
class InteractiveGroup extends Group {
constructor( renderer, camera ) {
super();
if( !renderer || !camera ) return
// extract camera when camera-rig is passed
camera.traverse( (n) => String(n.type).match(/Camera/) ? camera = n : null )
const scope = this;
const raycaster = new Raycaster();
const tempMatrix = new Matrix4();
function nocollide(){
if( nocollide.tid ) return // ratelimit
_event.type = "nocollide"
scope.children.map( (c) => c.dispatchEvent(_event) )
nocollide.tid = setTimeout( () => nocollide.tid = null, 100 )
}
// Pointer Events
const element = renderer.domElement;
function onPointerEvent( event ) {
//event.stopPropagation();
const rect = renderer.domElement.getBoundingClientRect();
_pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
_pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1;
raycaster.setFromCamera( _pointer, camera );
const intersects = raycaster.intersectObjects( scope.children, false );
if ( intersects.length > 0 ) {
const intersection = intersects[ 0 ];
const object = intersection.object;
const uv = intersection.uv;
_event.type = event.type;
_event.data.set( uv.x, 1 - uv.y );
object.dispatchEvent( _event );
}else nocollide()
}
element.addEventListener( 'pointerdown', onPointerEvent );
element.addEventListener( 'pointerup', onPointerEvent );
element.addEventListener( 'pointermove', onPointerEvent );
element.addEventListener( 'mousedown', onPointerEvent );
element.addEventListener( 'mouseup', onPointerEvent );
element.addEventListener( 'mousemove', onPointerEvent );
element.addEventListener( 'click', onPointerEvent );
// WebXR Controller Events
// TODO: Dispatch pointerevents too
const events = {
'move': 'mousemove',
'select': 'click',
'selectstart': 'mousedown',
'selectend': 'mouseup'
};
function onXRControllerEvent( event ) {
const controller = event.target;
tempMatrix.identity().extractRotation( controller.matrixWorld );
raycaster.ray.origin.setFromMatrixPosition( controller.matrixWorld );
raycaster.ray.direction.set( 0, 0, - 1 ).applyMatrix4( tempMatrix );
const intersections = raycaster.intersectObjects( scope.children, false );
if ( intersections.length > 0 ) {
const intersection = intersections[ 0 ];
const object = intersection.object;
const uv = intersection.uv;
_event.type = events[ event.type ];
_event.data.set( uv.x, 1 - uv.y );
if( _event.type != "mousemove" ){
console.log(event.type+" => "+_event.type)
}
object.dispatchEvent( _event );
}else nocollide()
}
const controller1 = renderer.xr.getController( 0 );
controller1.addEventListener( 'move', onXRControllerEvent );
controller1.addEventListener( 'select', onXRControllerEvent );
controller1.addEventListener( 'selectstart', onXRControllerEvent );
controller1.addEventListener( 'selectend', onXRControllerEvent );
const controller2 = renderer.xr.getController( 1 );
controller2.addEventListener( 'move', onXRControllerEvent );
controller2.addEventListener( 'select', onXRControllerEvent );
controller2.addEventListener( 'selectstart', onXRControllerEvent );
controller2.addEventListener( 'selectend', onXRControllerEvent );
}
}
return new InteractiveGroup(renderer,camera)
}
let xrf = xrfragment
xrf.frag = {}
xrf.model = {}
xrf.init = function(opts){
opts = opts || {}
let XRF = function(){
alert("queries are not implemented (yet)")
}
for ( let i in opts ) XRF[i] = xrfragment[i] = opts[i]
for ( let i in xrfragment ) XRF[i] = xrfragment[i]
for ( let i in xrfragment.XRF ) XRF[i] = xrfragment.XRF[i] // shortcuts to constants (NAVIGATOR e.g.)
xrfragment.Parser.debug = xrfragment.debug
if( opts.loaders ) opts.loaders.map( xrfragment.patchLoader )
return XRF
for ( let i in opts ) xrf[i] = opts[i]
for ( let i in xrf.XRF ) xrf.XRF[i] // shortcuts to constants (NAVIGATOR e.g.)
xrf.Parser.debug = xrf.debug
if( opts.loaders ) Object.values(opts.loaders).map( xrf.patchLoader )
xrf.patchRenderer(opts.renderer)
xrf.navigator.init()
return xrf
}
xrfragment.patchLoader = function(loader){
xrf.patchRenderer = function(renderer){
renderer.xr.addEventListener( 'sessionstart', () => xrf.baseReferenceSpace = renderer.xr.getReferenceSpace() );
renderer.xr.enabled = true;
renderer.render = ((render) => function(scene,camera){
if( xrf.model && xrf.model.render )
xrf.model.render(scene,camera)
render(scene,camera)
})(renderer.render.bind(renderer))
}
xrf.patchLoader = function(loader){
loader.prototype.load = ((load) => function(url, onLoad, onProgress, onError){
load.call( this,
url,
(model) => { onLoad(model); xrfragment.parseModel(model,url) },
(model) => { onLoad(model); xrf.parseModel(model,url) },
onProgress,
onError)
})(loader.prototype.load)
}
xrfragment.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrfragment.parseModel = function(model,url){
let file = xrfragment.getFile(url)
xrf.parseModel = function(model,url){
let file = xrf.getFile(url)
model.file = file
model.render = function(){}
xrfragment.model[file] = model
console.log("scanning "+file)
model.scene.traverse( (mesh) => {
console.log(mesh.name)
if( mesh.userData ){
let frag = {}
for( let k in mesh.userData ){
xrfragment.Parser.parse( k, mesh.userData[k], frag )
// call native function (xrf/env.js e.g.), or pass it to user decorator
let func = xrfragment.xrf[k] || function(){}
let opts = {mesh, model, camera: xrfragment.camera, scene: xrfragment.scene, renderer: xrfragment.renderer, THREE: xrfragment.THREE }
if( xrfragment[k] ) xrfragment[k]( func, frag[k], opts)
else func( frag[k], opts)
}
}
})
// eval embedded XR fragments
model.scene.traverse( (mesh) => xrf.eval.mesh(mesh,model) )
}
xrfragment.getLastModel = () => Object.values(xrfragment.model)[ Object.values(xrfragment.model).length-1 ]
xrfragment.eval = function( url, model ){
xrf.getLastModel = () => xrf.model.last
xrf.eval = function( url, model ){
let notice = false
let { THREE, camera } = xrfragment
let frag = xrfragment.URI.parse( url, XRF.NAVIGATOR )
model = model || xrf.model
let { THREE, camera } = xrf
let frag = xrf.URI.parse( url, xrf.XRF.NAVIGATOR )
let meshes = frag.q ? [] : [camera]
for ( let i in frag ) {
if( !String(i).match(/(pos|rot)/) ) notice = true
if( i == "pos" ){
camera.position.x = frag.pos.x
camera.position.y = frag.pos.y
camera.position.z = frag.pos.z
}
if( i == "rot" ){
camera.rotation.x = THREE.MathUtils.degToRad( frag.pos.x )
camera.rotation.y = THREE.MathUtils.degToRad( frag.pos.y )
camera.rotation.z = THREE.MathUtils.degToRad( frag.pos.z )
for ( let i in meshes ) {
for ( let k in frag ){
let mesh = meshes[i]
if( !String(k).match(/(pos|rot)/) ) notice = true
let opts = {frag, mesh, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE }
xrf.eval.fragment(k,opts)
}
}
if( notice ) alert("only 'pos' and 'rot' XRF.NAVIGATOR-flagged XR fragments are supported (for now)")
console.dir({url,model,frag})
}
xrfragment.xrf.env = function(v, opts){
xrf.eval.mesh = (mesh,model) => {
if( mesh.userData ){
let frag = {}
for( let k in mesh.userData ) xrf.Parser.parse( k, mesh.userData[k], frag )
for( let k in frag ){
let opts = {frag, mesh, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE }
mesh.userData.XRF = frag // allow fragment impl to access XRF obj already
xrf.eval.fragment(k,opts)
}
}
}
xrf.eval.fragment = (k, opts ) => {
// call native function (xrf/env.js e.g.), or pass it to user decorator
let func = xrf.frag[k] || function(){}
if( xrf[k] ) xrf[k]( func, opts.frag[k], opts)
else func( opts.frag[k], opts)
}
xrf.reset = () => {
const disposeObject = (obj) => {
if (obj.children.length > 0) obj.children.forEach((child) => disposeObject(child));
if (obj.geometry) obj.geometry.dispose();
if (obj.material) {
if (obj.material.map) obj.material.map.dispose();
obj.material.dispose();
}
obj.clear()
obj.removeFromParent()
return true
};
let nodes = []
xrf.scene.traverse( (child) => child.isXRF ? nodes.push(child) : false )
nodes.map( disposeObject ) // leave non-XRF objects intact
xrf.interactive = xrf.InteractiveGroup( xrf.THREE, xrf.renderer, xrf.camera)
xrf.add( xrf.interactive)
}
xrf.parseUrl = (url) => {
const urlObj = new URL( url.match(/:\/\//) ? url : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
let dir = url.substring(0, url.lastIndexOf('/') + 1)
const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
const ext = file.split('.').pop()
return {urlObj,dir,file,hash,ext}
}
xrf.add = (object) => {
object.isXRF = true // mark for easy deletion when replacing scene
xrf.scene.add(object)
}
/*
* EVENTS
*/
xrf.addEventListener = function(eventName, callback) {
if( !this._listeners ) this._listeners = []
if (!this._listeners[eventName]) {
// create a new array for this event name if it doesn't exist yet
this._listeners[eventName] = [];
}
// add the callback to the listeners array for this event name
this._listeners[eventName].push(callback);
};
xrf.emit = function(eventName, data) {
if( !this._listeners ) this._listeners = []
var callbacks = this._listeners[eventName]
if (callbacks) {
for (var i = 0; i < callbacks.length; i++) {
callbacks[i](data);
}
}
};
xrf.navigator = {}
xrf.navigator.to = (url,event) => {
if( !url ) throw 'xrf.navigator.to(..) no url given'
return new Promise( (resolve,reject) => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
console.log("xrfragment: navigating to "+url)
if( !file || xrf.model.file == file ){ // we're already loaded
document.location.hash = `#${hash}` // just update the hash
xrf.eval( url, xrf.model ) // and eval URI XR fragments
return resolve(xrf.model)
}
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
const Loader = xrf.loaders[ext]
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext
xrf.reset() // clear xrf objects from scene
// force relative path
if( dir ) dir = dir[0] == '.' ? dir : `.${dir}`
const loader = new Loader().setPath( dir )
loader.load( file, function(model){
model.file = file
xrf.add( model.scene )
xrf.model = model
xrf.eval( url, model ) // and eval URI XR fragments
xrf.navigator.pushState( file, hash )
resolve(model)
})
})
}
xrf.navigator.init = () => {
if( xrf.navigator.init.inited ) return
window.addEventListener('popstate', function (event){
xrf.navigator.to( document.location.search.substr(1) + document.location.hash, event)
})
xrf.navigator.init.inited = true
}
xrf.navigator.pushState = (file,hash) => {
if( file == document.location.search.substr(1) ) return // page is in its default state
window.history.pushState({},`${file}#${hash}`, document.location.pathname + `?${file}#${hash}` )
}
xrf.frag.env = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
let env = mesh.getObjectByName(v.string)
env.material.map.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = env.material.map
scene.texture = env.material.map
//scene.texture = env.material.map
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
console.log(` └ applied image '${v.string}' as environtment map`)
renderer.toneMappingExposure = 2;
// apply to meshes *DISABLED* renderer.environment does this
const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
setTimeout( () => {
scene.traverse( (mesh) => {
//if (mesh.material && mesh.material.map && mesh.material.metalness == 1.0) {
// mesh.material = new THREE.MeshBasicMaterial({ map: mesh.material.map });
// mesh.material.dithering = true
// mesh.material.map.anisotropy = maxAnisotropy;
// mesh.material.needsUpdate = true;
//}
//if (mesh.material && mesh.material.metalness == 1.0 ){
// mesh.material = new THREE.MeshBasicMaterial({
// color:0xffffff,
// emissive: mesh.material.map,
// envMap: env.material.map,
// side: THREE.DoubleSide,
// flatShading: true
// })
// mesh.material.needsUpdate = true
// //mesh.material.envMap = env.material.map;
// //mesh.material.envMap.intensity = 5;
// //mesh.material.needsUpdate = true;
//}
});
},500)
console.log(` └ applied image '${v.string}' as environment map`)
}
xrfragment.xrf.href = function(v, opts){
/**
* navigation, portals & mutations
*
* | fragment | type | scope | example value |
* |`href`| string (uri or [predefined view](#predefined_view )) | 🔒 |`#pos=1,1,0`<br>`#pos=1,1,0&rot=90,0,0`<br>`#pos=pyramid`<br>`#pos=lastvisit\|pyramid`<br>`://somefile.gltf#pos=1,1,0`<br> |
*
* [img[xrfragment.jpg]]
*
* !!!spec 1.0
*
* 1. a ''external''- or ''file URI'' fully replaces the current scene and assumes `pos=0,0,0&rot=0,0,0` by default (unless specified)
*
* 2. navigation should not happen when queries (`q=`) are present in local url: queries will apply (`pos=`, `rot=` e.g.) to the targeted object(s) instead.
*
* 3. navigation should not happen ''immediately'' when user is more than 2 meter away from the portal/object containing the href (to prevent accidental navigation e.g.)
*/
xrf.frag.href = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
return
// Create a shader material that treats the texture as an equirectangular map
mesh.texture = mesh.material.map // backup texture
const equirectShader = THREE.ShaderLib[ 'equirect' ];
const equirectMaterial = new THREE.ShaderMaterial( {
uniforms: THREE.UniformsUtils.merge([
THREE.UniformsLib.equirect,
equirectShader.uniforms,
]),
vertexShader: equirectShader.vertexShader,
fragmentShader: equirectShader.fragmentShader,
side: THREE.DoubleSide //THREE.FrontSide //THREE.DoubleSide //THREE.BackSide
} );
equirectMaterial.uniforms[ 'tEquirect' ].value = mesh.texture
// Define the tEquirectInvProjection uniform
equirectMaterial.uniforms.tEquirectInvProjection = {
value: new THREE.Matrix4(),
};
// Assign the new material to the mesh
mesh.material = equirectMaterial;
console.dir(mesh.material)
mesh.texture.wrapS = THREE.RepeatWrapping;
const world = {
pos: new THREE.Vector3(),
scale: new THREE.Vector3(),
quat: new THREE.Quaternion()
}
mesh.getWorldPosition(world.pos)
mesh.getWorldScale(world.scale)
mesh.getWorldQuaternion(world.quat);
mesh.position.copy(world.pos)
mesh.scale.copy(world.scale)
mesh.setRotationFromQuaternion(world.quat);
// patch custom model renderloop
model.render = ((render) => (scene,camera) => {
// detect equirectangular image
let texture = mesh.material.map
if( texture && texture.source.data.height == texture.source.data.width/2 ){
texture.mapping = THREE.ClampToEdgeWrapping
texture.needsUpdate = true
// Store the original projection matrix of the camera
const originalProjectionMatrix = camera.projectionMatrix.clone();
// Calculate the current camera view matrix
const aspectRatio = mesh.texture.image.width / mesh.texture.image.height;
camera.projectionMatrix.makePerspective(camera.fov, aspectRatio, camera.near, camera.far);
// poor man's equi-portal
mesh.material = new THREE.ShaderMaterial( {
side: THREE.DoubleSide,
uniforms: {
pano: { value: texture },
selected: { value: false },
},
vertexShader: `
vec3 portalPosition;
varying vec3 vWorldPosition;
varying float vDistanceToCenter;
varying float vDistance;
void main() {
vDistanceToCenter = clamp(length(position - vec3(0.0, 0.0, 0.0)), 0.0, 1.0);
portalPosition = (modelMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;
vDistance = length(portalPosition - cameraPosition);
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
#define RECIPROCAL_PI2 0.15915494
uniform sampler2D pano;
uniform bool selected;
varying float vDistanceToCenter;
varying float vDistance;
varying vec3 vWorldPosition;
void main() {
vec3 direction = normalize(vWorldPosition - cameraPosition);
vec2 sampleUV;
sampleUV.y = -clamp(direction.y * 0.5 + 0.5, 0.0, 1.0);
sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2;
sampleUV.x += 0.33; // adjust focus to AFRAME's a-scene.components.screenshot.capture()
vec4 color = texture2D(pano, sampleUV);
// Convert color to grayscale (lazy lite approach to not having to match tonemapping/shaderstacking of THREE.js)
float luminance = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
vec4 grayscale_color = selected ? color : vec4(vec3(luminance) + vec3(0.33), color.a);
gl_FragColor = grayscale_color;
}
`,
});
mesh.material.needsUpdate = true
}
const viewMatrix = camera.matrixWorldInverse;
const worldMatrix = mesh.matrixWorld;
let teleport = mesh.userData.XRF.href.exec = (e) => {
const meshWorldPosition = new THREE.Vector3();
meshWorldPosition.setFromMatrixPosition(mesh.matrixWorld);
const equirectInvProjection = new THREE.Matrix4();
equirectInvProjection.copy(camera.projectionMatrix).multiply(viewMatrix).invert();
let portalArea = 1 // 2 meter
const cameraDirection = new THREE.Vector3();
camera.getWorldPosition(cameraDirection);
cameraDirection.sub(meshWorldPosition);
cameraDirection.normalize();
cameraDirection.multiplyScalar(portalArea); // move away from portal
const newPos = meshWorldPosition.clone().add(cameraDirection);
// Update the equirectangular material's tEquirect uniform
equirectMaterial.uniforms.tEquirect.value = mesh.texture;
equirectMaterial.uniforms.tEquirectInvProjection.value.copy(
equirectInvProjection
);
const distance = camera.position.distanceTo(newPos);
//if( distance > portalArea ){
if( !renderer.xr.isPresenting && !confirm("teleport to "+v.string+" ?") ) return
xrf.navigator.to(v.string) // ok let's surf to HREF!
console.log("teleport!")
xrf.emit('href',{click:true,mesh,xrf:v})
}
// Reset the camera projection matrix
camera.projectionMatrix.copy(originalProjectionMatrix);
let selected = (state) => () => {
if( mesh.selected == state ) return // nothing changed
if( mesh.material.uniforms ) mesh.material.uniforms.selected.value = state
else mesh.material.color.r = mesh.material.color.g = mesh.material.color.b = state ? 2.0 : 1.0
// update mouse cursor
if( !renderer.domElement.lastCursor )
renderer.domElement.lastCursor = renderer.domElement.style.cursor
renderer.domElement.style.cursor = state ? 'pointer' : renderer.domElement.lastCursor
xrf.emit('href',{selected:state,mesh,xrf:v})
mesh.selected = state
}
if( !opts.frag.q ){ // query means an action
mesh.addEventListener('click', teleport )
mesh.addEventListener('mousemove', selected(true) )
mesh.addEventListener('nocollide', selected(false) )
}
render(scene,camera)
})(model.render)
console.dir(mesh)
// lazy add mesh (because we're inside a recursive traverse)
setTimeout( (mesh) => {
xrf.interactive.add(mesh)
}, 20, mesh )
}
xrfragment.xrf.pos = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
/**
* > above was abducted from [[this|https://i.imgur.com/E3En0gJ.png]] and [[this|https://i.imgur.com/lpnTz3A.png]] survey result
*
* [[» discussion|https://github.com/coderofsalvation/xrfragment/issues/1]]<br>
* [[» implementation example|https://github.com/coderofsalvation/xrfragment/blob/main/src/three/xrf/pos.js]]<br>
*/
xrf.frag.pos = function(v, opts){
//if( renderer.xr.isPresenting ) return // too far away
let { frag, mesh, model, camera, scene, renderer, THREE} = opts
console.log(" └ setting camera position to "+v.string)
camera.position.x = v.x
camera.position.y = v.y
camera.position.z = v.z
if( !frag.q ){
if( true ){//!renderer.xr.isPresenting ){
console.dir(camera)
camera.position.x = v.x
camera.position.y = v.y
camera.position.z = v.z
}
/*
else{ // XR
let cameraWorldPosition = new THREE.Vector3()
camera.object3D.getWorldPosition(this.cameraWorldPosition)
let newRigWorldPosition = new THREE.Vector3(v.x,v.y,x.z)
// Finally update the cameras position
let newRigLocalPosition.copy(this.newRigWorldPosition)
if (camera.object3D.parent) {
camera.object3D.parent.worldToLocal(newRigLocalPosition)
}
camera.setAttribute('position', newRigLocalPosition)
// Also take the headset/camera rotation itself into account
if (this.data.rotateOnTeleport) {
this.teleportOcamerainQuaternion
.setFromEuler(new THREE.Euler(0, this.teleportOcamerain.object3D.rotation.y, 0))
this.teleportOcamerainQuaternion.invert()
this.teleportOcamerainQuaternion.multiply(this.hitEntityQuaternion)
// Rotate the camera based on calculated teleport ocamerain rotation
this.cameraRig.object3D.setRotationFromQuaternion(this.teleportOcamerainQuaternion)
}
console.log("XR")
const offsetPosition = { x: - v.x, y: - v.y, z: - v.z, w: 1 };
const offsetRotation = new THREE.Quaternion();
const transform = new XRRigidTransform( offsetPosition, offsetRotation );
const teleportSpaceOffset = xrf.baseReferenceSpace.getOffsetReferenceSpace( transform );
renderer.xr.setReferenceSpace( teleportSpaceOffset );
}
*/
}
}
xrfragment.xrf.src = function(v, opts){
xrf.frag.q = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts
console.log(" └ running query ")
for ( let i in v.query ) {
let target = v.query[i]
// remove objects if requested
if( target.id != undefined && (target.mesh = scene.getObjectByName(i)) ){
target.mesh.visible = target.id
target.mesh.parent.remove(target.mesh)
console.log(` └ removing mesh: ${i}`)
}else console.log(` └ mesh not found: ${i}`)
}
}
xrf.frag.rot = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
console.log(" └ setting camera rotation to "+v.string)
camera.rotation.set(
v.x * Math.PI / 180,
v.y * Math.PI / 180,
v.z * Math.PI / 180
)
}
// *TODO* use webgl instancing
xrf.frag.src = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
if( v.string[0] == "#" ){ // local
console.log(" └ instancing src")
let args = xrfragment.URI.parse(v.string)
let frag = xrfragment.URI.parse(v.string)
// Get an instance of the original model
const modelInstance = new THREE.Group();
modelInstance.add(model.scene.clone());
modelInstance.position.z = mesh.position.x
modelInstance.position.y = mesh.position.y
modelInstance.position.x = mesh.position.z
modelInstance.scale.z = mesh.scale.x
modelInstance.scale.y = mesh.scale.y
modelInstance.scale.x = mesh.scale.z
// now apply XR Fragments overrides from URI
// *TODO* move to a central location (pull-up)
for( var i in args ){
if( i == "scale" ){
console.log(" └ setting scale")
modelInstance.scale.x = args[i].x
modelInstance.scale.y = args[i].y
modelInstance.scale.z = args[i].z
}
let sceneInstance = new THREE.Group()
sceneInstance.isSrc = true
// prevent infinite recursion #1: skip src-instanced models
for ( let i in model.scene.children ) {
let child = model.scene.children[i]
if( child.isSrc ) continue;
sceneInstance.add( model.scene.children[i].clone() )
}
// Add the instance to the scene
scene.add(modelInstance);
console.dir(model)
console.dir(modelInstance)
sceneInstance.position.copy( mesh.position )
sceneInstance.scale.copy(mesh.scale)
sceneInstance.updateMatrixWorld(true) // needed because we're going to move portals to the interactive-group
// apply embedded XR fragments
setTimeout( () => {
sceneInstance.traverse( (m) => {
if( m.userData && m.userData.src ) return ;//delete m.userData.src // prevent infinite recursion
xrf.eval.mesh(m,{scene,recursive:true})
})
// apply URI XR Fragments inside src-value
for( var i in frag ){
xrf.eval.fragment(i, Object.assign(opts,{frag, model:{scene:sceneInstance},scene:sceneInstance}))
}
// Add the instance to the scene
model.scene.add(sceneInstance);
},10)
}
}
export default xrfragment;

View file

@ -0,0 +1 @@
../../assets

View file

@ -0,0 +1 @@
../../assets/example3.gltf

View file

@ -0,0 +1 @@
../../assets/href.gltf

View file

@ -0,0 +1,67 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>AFRAME - xrfragment sandbox</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="./../../assets/css/axist.min.css" />
<link type="text/css" rel="stylesheet" href="./../../assets/css/style.css"/>
<script async src="./../../assets/js/alpine.min.js" defer></script>
<script src="https://aframe.io/releases/1.4.2/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/aframe-blink-controls/dist/aframe-blink-controls.min.js"></script>
<script src="./../../../dist/xrfragment.aframe.js"></script>
</head>
<body>
<div id="overlay">
<img src="./../../assets/logo.png" class="logo"/>
<button id="navback" onclick="history.back()">&lt;</button>
<button id="navforward" onclick="history.forward()">&gt;</button>
<input type="submit" value="load 3D asset"></input>
<input type="text" id="uri" value="" onchange="AFRAME.XRF.navigator.to( $('#uri').value )"/>
</div>
<a class="btn-foot" id="source" target="_blank" href="https://github.com/coderofsalvation/xrfragment/blob/main/example/aframe/sandbox/index.html">sourcecode</a>
<a class="btn-foot" id="model" target="_blank" href="">⬇️ model</a>
<textarea style="display:none"></textarea>
<a-scene light="defaultLightsEnabled: false">
<a-entity id="player" wasd-controls look-controls>
<a-entity id="left-hand" laser-controls="hand: left" raycaster="objects:.ray;far:5500" oculus-touch-controls="hand: left" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: #floor">
<a-entity rotation="-90 0 0" position="0 0.1 0">
<a-entity id="back" xrf-button="label: <; width:0.05; action: history.back()" position="-0.025 0 0" class="ray"></a-entity>
<a-entity id="next" xrf-button="label: >; width:0.05; action: history.forward()" position=" 0.025 0 0" class="ray"></a-entity>
</a-entity>
</a-entity>
<a-entity id="right-hand" laser-controls="hand: right" raycaster="objects:.ray;far:5500" oculus-touch-controls="hand: right" blink-controls="cameraRig:#player; teleportOrigin: #camera; collisionEntities: #floor"></a-entity>
<a-entity camera="fov:90" position="0 1.6 0" id="camera"></a-entity>
</a-entity>
<a-entity id="home" xrf="example3.gltf#pos=0,0,0"></a-entity>
<a-entity id="floor" xrf-get="floor"></a-entity>
</a-scene>
<script type="module">
import { loadFile, setupConsole, setupUrlBar, notify } from './../../assets/js/utils.js';
window.$ = (s) => document.querySelector(s)
window.notify = notify(window)
console.log = ( (log) => function(str){
if( String(str).match(/(camera)/) ) window.notify(str)
log(str)
})(console.log)
if( document.location.search.length > 2 )
$('#home').setAttribute('xrf', document.location.search.substr(1)+document.location.hash )
$('a-scene').addEventListener('loaded', () => {
setupConsole( $('textarea') )
setupUrlBar( $('input#uri') )
// add screenshot component with camera to capture proper equirects
$('a-scene').setAttribute("screenshot",{camera: "[camera]",width: 4096*2, height:2048*2})
setTimeout( () => window.notify("use WASD-keys and mouse-drag to move around",{timeout:false}),2000 )
window.AFRAME.XRF.addEventListener('href', (data) => data.selected ? window.notify(`href: ${data.xrf.string}`) : false )
})
</script>
</body>
</html>

View file

@ -0,0 +1 @@
../../assets/other.gltf

View file

@ -0,0 +1,250 @@
:root {
--primary: #6839dc;
--light-primary: #ea23cf;
--secondary: #872eff;
--light-secondary: #ce7df2;
--red: red;
--black: #424280;
--white: #fdfdfd;
--dark-gray: #343334;
--gray: #ecf7ff47;
--light-gray: #efefef;
--lighter-gray: #e4e2fb96;
--font-sans-serif: system-ui, -apple-system, segoe ui, roboto, ubuntu, helvetica, cantarell, noto sans, sans-serif;
--font-monospace: menlo, monaco, lucida console, liberation mono, dejavu sans mono, bitstream vera sans mono, courier new, monospace, serif;
--border-radius: 0.2rem;
}
.small{
font-size:12px;
}
textarea, select, input[type="text"] {
background: transparent; /* linear-gradient( var(--lighter-gray), var(--gray) ) !important; */
}
input[type="submit"] {
color: var(--light-gray);
}
.title {
border-bottom: 2px solid var(--secondary);
padding-bottom: 20px;
}
#overlay{
background: #FFF;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 48px;
box-shadow: 0px 0px 10px #0004;
opacity: 0.9;
z-index:2000;
}
#overlay .logo{
width: 92px;
position: absolute;
top: 7px;
left: 93px;
}
#overlay > input[type="submit"] {
height: 32px;
font-size: 14px;
position: absolute;
right: 9px;
top: 8px;
}
#overlay > button#navback,
#overlay > button#navforward {
height: 32px;
font-size: 14px;
position: absolute;
left: 9px;
padding: 2px 13px;
top: 8px;
color: var(--light-gray);
}
#overlay > button#navforward {
left:49px;
}
#overlay > #uri {
display:none;
height: 29px;
font-size: 14px;
position: absolute;
left: 200px;
top: 9px;
max-width:550px;
padding: 5px 0px 5px 5px;
width: calc( 63% - 200px);
background: #f0f0f0;
border-color: #Ccc;
}
.btn-foot{
background: white;
border-radius: 10px;
border: 5px solid #1c1c3299;
padding: 0px 6px;
bottom:67px;
position: absolute;
}
a.btn-foot#source{
right: 10px;
color: #888;
font-weight: bold;
font-family: sans-serif;
z-index:2000;
bottom: 114px;
}
a.btn-foot#model{
position: absolute;
right: 10px;
color: #7c7c7c;
font-weight: bold;
font-family: sans-serif;
z-index:2000;
}
html.a-fullscreen a#model,
html.a-fullscreen a#source{
margin-right:10px;
}
.render {
position:absolute;
top:0;
left:0;
right:0;
bottom:0;
}
.lil-gui.autoPlace{
right:0px !important;
top:48px !important;
height:33vh;
}
#VRButton {
margin-bottom:20vh;
}
@media (max-width: 450px) {
#uri{ display:none; }
}
@media (max-width: 640px) {
.lil-gui.root{
top:auto !important;
left:auto !important;
}
}
/* notifications */
.js-snackbar-container {
position: absolute;
top: 10px;
left: 0px;
display: flex;
align-items: center;
width:100%;
max-width: 100%;
padding: 10px;
z-index:1001;
justify-content: center;
overflow: hidden;
}
.js-snackbar-container * {
box-sizing: border-box;
}
.js-snackbar__wrapper {
--color-c: #555;
--color-a: #EEE;
}
.js-snackbar__wrapper {
overflow: hidden;
height: auto;
margin: 5px 0;
transition: all ease .5s;
border-radius: 3px;
box-shadow: 0 0 4px 0 #0007;
left: 20px;
position: fixed;
bottom: 20px;
}
.js-snackbar {
display: inline-flex;
box-sizing: border-box;
border-radius: 3px;
color: var(--color-c);
font-size: 16px;
background-color: var(--color-a);
vertical-align: bottom;
}
.js-snackbar__close,
.js-snackbar__status,
.js-snackbar__message {
position: relative;
}
.js-snackbar__message {
padding: 12px;
}
.js-snackbar__status {
display: none;
width: 15px;
margin-right: 5px;
border-radius: 3px 0 0 3px;
background-color: transparent;
}
.js-snackbar__status.js-snackbar--success,
.js-snackbar__status.js-snackbar--warning,
.js-snackbar__status.js-snackbar--danger,
.js-snackbar__status.js-snackbar--info {
display: block;
}
.js-snackbar__status.js-snackbar--success {
background-color: #4caf50;
}
.js-snackbar__status.js-snackbar--warning {
background-color: #ff9800;
}
.js-snackbar__status.js-snackbar--danger {
background-color: #ff6060;
}
.js-snackbar__status.js-snackbar--info {
background-color: #CCC;
}
.js-snackbar__close {
cursor: pointer;
display: flex;
align-items: center;
padding: 0 10px;
user-select: none;
}
.js-snackbar__close:hover {
background-color: #4443;
}

File diff suppressed because one or more lines are too long

2209
example/assets/example3.gltf Normal file

File diff suppressed because one or more lines are too long

2209
example/assets/href.gltf Normal file

File diff suppressed because one or more lines are too long

267
example/assets/js/utils.js Normal file
View file

@ -0,0 +1,267 @@
// contentLoaders = {".gltf" : () => .....} and so on
export function loadFile(contentLoaders, multiple){
return () => {
let input = document.createElement('input');
input.type = 'file';
input.multiple = multiple;
input.accept = Object.keys(contentLoaders).join(",");
input.onchange = () => {
let files = Array.from(input.files);
let file = files.slice ? files[0] : files
for( var i in contentLoaders ){
let r = new RegExp('\\'+i+'$')
if( file.name.match(r) ) return contentLoaders[i](file)
}
alert(file.name+" is not supported")
};
input.click();
}
}
export function setupConsole(el){
if( !el ) return setTimeout( () => setupConsole( $('.lil-gui') ),200 )
let $console = document.createElement('textarea')
$console.style.position = 'absolute'
$console.style.display = 'block'
$console.style.zIndex = 2000;
$console.style.background = "transparent !important"
$console.style.pointerEvents = 'none'
$console.style.top = '70px'
$console.style.padding = '10px'
$console.style.margin = '10px'
$console.style.background = '#000'
$console.style.left = $console.style.right = $console.style.bottom = 0;
$console.style.color = '#A6F';
$console.style.fontSize = '10px';
$console.style.fontFamily = 'Courier'
$console.style.border = '0'
$console.innerHTML = "XRFRAGMENT CONSOLE OUTPUT:\n"
el.appendChild($console)
console.log = ( (log) => function(){
let str = ([...arguments]).join(" ")
let s = new Date().toISOString().substr(11).substr(0,8) + " " + str.replace(/.*[0-9]: /,"")
log(s)
let lines = String($console.innerHTML + "\n"+s).split("\n")
while( lines.length > 200 ) lines.shift()
$console.innerHTML = lines.join("\n")
$console.scrollTop = $console.scrollHeight;
})(console.log.bind(console))
}
export function setupUrlBar(el){
var isIframe = (window === window.parent || window.opener) ? false : true;
if( isIframe ){
// show internal URL bar to test XR fragments interactively
el.style.display = 'block'
let nav = window.AFRAME.XRF.navigator
AFRAME.XRF.navigator.to = ((to) => (url,e) => {
to(url,e)
reflectUrl(url)
})(AFRAME.XRF.navigator.to)
const reflectUrl = (url) => el.value = url || document.location.search.substr(1) + document.location.hash
reflectUrl()
}
}
function SnackBar(userOptions) {
var snackbar = this || (window.snackbar = {});
var _Interval;
var _Message;
var _Element;
var _Container;
var _OptionDefaults = {
message: "Operation performed successfully.",
dismissible: true,
timeout: 5000,
status: ""
}
var _Options = _OptionDefaults;
function _Create() {
_Container = document.getElementsByClassName("js-snackbar-container")[0];
if (!_Container) {
// need to create a new container for notifications
_Container = document.createElement("div");
_Container.classList.add("js-snackbar-container");
document.body.appendChild(_Container);
}
_Element = document.createElement("div");
_Element.classList.add("js-snackbar__wrapper");
let innerSnack = document.createElement("div");
innerSnack.classList.add("js-snackbar", "js-snackbar--show");
if (_Options.status) {
_Options.status = _Options.status.toLowerCase().trim();
let status = document.createElement("span");
status.classList.add("js-snackbar__status");
if (_Options.status === "success" || _Options.status === "green") {
status.classList.add("js-snackbar--success");
}
else if (_Options.status === "warning" || _Options.status === "alert" || _Options.status === "orange") {
status.classList.add("js-snackbar--warning");
}
else if (_Options.status === "danger" || _Options.status === "error" || _Options.status === "red") {
status.classList.add("js-snackbar--danger");
}
else {
status.classList.add("js-snackbar--info");
}
innerSnack.appendChild(status);
}
_Message = document.createElement("span");
_Message.classList.add("js-snackbar__message");
_Message.textContent = _Options.message;
innerSnack.appendChild(_Message);
if (_Options.dismissible) {
let closeBtn = document.createElement("span");
closeBtn.classList.add("js-snackbar__close");
closeBtn.innerText = "\u00D7";
closeBtn.onclick = snackbar.Close;
innerSnack.appendChild(closeBtn);
}
_Element.style.height = "0px";
_Element.style.opacity = "0";
_Element.style.marginTop = "0px";
_Element.style.marginBottom = "0px";
_Element.appendChild(innerSnack);
_Container.appendChild(_Element);
if (_Options.timeout !== false) {
_Interval = setTimeout(snackbar.Close, _Options.timeout);
}
}
var _ConfigureDefaults = function() {
// if no options given, revert to default
if (userOptions === undefined) {
return;
}
if (userOptions.message !== undefined) {
_Options.message = userOptions.message;
}
if (userOptions.dismissible !== undefined) {
if (typeof (userOptions.dismissible) === "string") {
_Options.dismissible = (userOptions.dismissible === "true");
}
else if (typeof (userOptions.dismissible) === "boolean") {
_Options.dismissible = userOptions.dismissible;
}
else {
console.debug("Invalid option provided for 'dismissable' [" + userOptions.dismissible + "] is of type " + (typeof userOptions.dismissible));
}
}
if (userOptions.timeout !== undefined) {
if (typeof (userOptions.timeout) === "boolean" && userOptions.timeout === false) {
_Options.timeout = false;
}
else if (typeof (userOptions.timeout) === "string") {
_Options.timeout = parseInt(userOptions.timeout);
}
if (typeof (userOptions.timeout) === "number") {
if (userOptions.timeout === Infinity) {
_Options.timeout = false;
}
else if (userOptions.timeout >= 0) {
_Options.timeout = userOptions.timeout;
}
else {
console.debug("Invalid timeout entered. Must be greater than or equal to 0.");
}
_Options.timeout = userOptions.timeout;
}
}
if (userOptions.status !== undefined) {
_Options.status = userOptions.status;
}
}
snackbar.Open = function() {
let contentHeight = _Element.firstElementChild.scrollHeight; // get the height of the content
_Element.style.height = contentHeight + "px";
_Element.style.opacity = 1;
_Element.style.marginTop = "5px";
_Element.style.marginBottom = "5px";
_Element.addEventListener("transitioned", function() {
_Element.removeEventListener("transitioned", arguments.callee);
_Element.style.height = null;
})
}
snackbar.Close = function () {
if (_Interval)
clearInterval(_Interval);
let snackbarHeight = _Element.scrollHeight; // get the auto height as a px value
let snackbarTransitions = _Element.style.transition;
_Element.style.transition = "";
requestAnimationFrame(function() {
_Element.style.height = snackbarHeight + "px"; // set the auto height to the px height
_Element.style.opacity = 1;
_Element.style.marginTop = "0px";
_Element.style.marginBottom = "0px";
_Element.style.transition = snackbarTransitions
requestAnimationFrame(function() {
_Element.style.height = "0px";
_Element.style.opacity = 0;
})
});
setTimeout(function() {
_Container.removeChild(_Element);
}, 1000);
};
_ConfigureDefaults();
_Create();
snackbar.Open();
}
export function notify(scope){
return function notify(str,opts){
str = String(str)
opts = opts || {}
if( !opts.status ){
opts.status = "info"
if( str.match(/error/g) ) opts.status = "danger"
if( str.match(/warning/g) ) opts.status = "warning"
}
opts = Object.assign({ message: str , status, timeout:2000 },opts)
SnackBar( opts )
}
}

638
example/assets/other.gltf Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,123 +0,0 @@
:root {
--primary: #6839dc;
--light-primary: #ea23cf;
--secondary: #872eff;
--light-secondary: #ce7df2;
--red: red;
--black: #424280;
--white: #fdfdfd;
--dark-gray: #343334;
--gray: #ecf7ff47;
--light-gray: #efefef;
--lighter-gray: #e4e2fb96;
--font-sans-serif: system-ui, -apple-system, segoe ui, roboto, ubuntu, helvetica, cantarell, noto sans, sans-serif;
--font-monospace: menlo, monaco, lucida console, liberation mono, dejavu sans mono, bitstream vera sans mono, courier new, monospace, serif;
--border-radius: 0.2rem;
}
.small{
font-size:12px;
}
textarea, select, input[type="text"] {
background: transparent; /* linear-gradient( var(--lighter-gray), var(--gray) ) !important; */
}
input[type="submit"] {
color: var(--light-gray);
}
.title {
border-bottom: 2px solid var(--secondary);
padding-bottom: 20px;
}
#overlay{
background: #FFF;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 48px;
box-shadow: 0px 0px 10px #0004;
opacity: 0.9;
}
#overlay .logo{
width: 92px;
position: absolute;
top: 7px;
left: 93px;
}
#overlay > input[type="submit"]{
height: 32px;
font-size: 14px;
position: absolute;
right: 9px;
top: 8px;
}
#overlay > #uri {
height: 29px;
font-size: 14px;
position: absolute;
left: 200px;
top: 9px;
max-width:550px;
padding: 5px 0px 5px 5px;
width: calc( 63% - 200px);
background: #f0f0f0;
border-color: #Ccc;
}
a#source{
position: absolute;
bottom: 29px;
right: 20px;
color: #888;
font-weight: bold;
font-family: sans-serif;
text-decoration: underline;
z-index:2000;
}
a#model{
position: absolute;
bottom: 29px;
right: 130px;
color: #888;
font-weight: bold;
font-family: sans-serif;
text-decoration: underline;
z-index:2000;
}
.render {
position:absolute;
top:0;
left:0;
right:0;
bottom:0;
}
.lil-gui.autoPlace{
right:0px !important;
top:auto !important;
bottom:0;
}
#VRButton {
margin-bottom:20vh;
}
@media (max-width: 450px) {
#uri{ display:none; }
}
@media (max-width: 640px) {
.lil-gui.root{
top:auto !important;
left:auto !important;
}
}

View file

@ -1,46 +0,0 @@
// contentLoaders = {".gltf" : () => .....} and so on
export function loadFile(contentLoaders, multiple){
return () => {
let input = document.createElement('input');
input.type = 'file';
input.multiple = multiple;
input.accept = Object.keys(contentLoaders).join(",");
input.onchange = () => {
let files = Array.from(input.files);
let file = files.slice ? files[0] : files
for( var i in contentLoaders ){
let r = new RegExp('\\'+i+'$')
if( file.name.match(r) ) return contentLoaders[i](file)
}
alert(file.name+" is not supported")
};
input.click();
}
}
export function setupConsole($console){
$console.style.position = 'absolute'
$console.style.display = 'block'
$console.style.zIndex = 1000;
$console.style.background = "transparent !important"
$console.style.pointerEvents = 'none'
$console.style.top = '70px'
$console.style.padding = '5px 20px 25px 25px'
$console.style.left = $console.style.right = $console.style.bottom = 0;
$console.style.color = '#0008';
$console.style.fontSize = '12px';
$console.style.fontFamily = 'Courier'
$console.style.border = '0'
$console.innerHTML = "XRFRAGMENT CONSOLE OUTPUT:\n"
console.log = ( (log) => function(){
let s = new Date().toISOString().substr(11).substr(0,8) + " " + ([...arguments]).join(" ").replace(/.*[0-9]: /,"")
log(s)
let lines = String($console.innerHTML + "\n"+s).split("\n")
while( lines.length > 200 ) lines.shift()
$console.innerHTML = lines.join("\n")
$console.scrollTop = $console.scrollHeight;
})(console.log.bind(console))
}

View file

@ -1,7 +1,7 @@
<html>
<head>
<link rel="stylesheet" href="./assets/axist.min.css" />
<link rel="stylesheet" href="./assets/style.css" />
<link rel="stylesheet" href="./assets/css/axist.min.css" />
<link rel="stylesheet" href="./assets/css/style.css" />
</head>
<body>
<script src="./../dist/xrfragment.js"></script>

View file

@ -0,0 +1 @@
../../assets/example3.gltf

View file

@ -1,26 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>three.js vr - sandbox</title>
<title>THREE.js - xrfragment sandbox</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" href="./../../assets/axist.min.css" />
<link type="text/css" rel="stylesheet" href="main.css">
<link type="text/css" rel="stylesheet" href="./../../assets/style.css"/>
<link rel="stylesheet" href="./../../assets/css/axist.min.css" />
<link type="text/css" rel="stylesheet" href="./../../assets/css/style.css"/>
</head>
<body>
<div id="overlay" x-data="{ urls: ['#pos=-1,1.6,10','#pos=-1,1.6,2'] }">
<div id="overlay" x-data="{ urls: ['#pos=0,1.6,15','#pos=0,1.6,15&rot=0,360,0'] }">
<img src="./../../assets/logo.png" class="logo"/>
<input type="submit" value="load 3D asset"></input>
<input type="text" id="uri" list="urls" value="#pos=-1,1.6,10" x-on:change="XRF.eval( $('#uri').value, XRF.getLastModel() )"/>
<datalist id="urls" >
<template x-for="url in urls">
<option x-bind:value="url" selected></option>
</template>
</datalist>
<input type="text" id="uri" value="" onchange="AFRAME.XRF.navigator.to( $('#uri').value )"/>
</div>
<a id="source" target="_blank" href="https://github.com/coderofsalvation/xrfragment/blob/main/example/threejs/sandbox/index.html#L92-L112">sourcecode</a>
<a id="model" target="_blank" href="">⬇️ model</a>
<a class="btn-foot" id="source" target="_blank" href="https://github.com/coderofsalvation/xrfragment/blob/main/example/threejs/sandbox/index.html#L92-L112">sourcecode</a>
<a class="btn-foot" id="model" target="_blank" href="">⬇️ model</a>
<textarea style="display:none"></textarea>
<script async src="./../../assets/alpine.min.js"></script>
@ -41,7 +35,7 @@
import xrfragment from './../../../dist/xrfragment.three.js';
import { loadFile, setupConsole } from './../../assets/utils.js';
import { loadFile, setupConsole, setupUrlBar, notify } from './../../assets/js/utils.js';
import { RGBELoader } from 'three/addons/loaders/RGBELoader.js';
import { Lensflare, LensflareElement } from 'three/addons/objects/Lensflare.js';
import { BoxLineGeometry } from 'three/addons/geometries/BoxLineGeometry.js';
@ -89,16 +83,23 @@
window.addEventListener( 'resize', onWindowResize );
let cameraRig = new THREE.Group()
cameraRig.position.set( 0, 0, 0 );
cameraRig.add(camera)
scene.add(cameraRig)
// enable XR fragments
let XRF = xrfragment.init({
THREE,
camera,
camera:cameraRig,
scene,
renderer,
debug: true,
loaders: [ GLTFLoader, FBXLoader ], // which 3D assets to check for XR fragments?
loaders: { gltf: GLTFLoader, fbx: FBXLoader }, // which 3D assets (extensions) to check for XR fragments?
})
// optional: react/extend/hook into XR fragment
XRF.env = (xrf,v,opts) => {
let { mesh, model, camera, scene, renderer, THREE} = opts
@ -111,6 +112,7 @@
console.log("hello custom property 'foobar'")
}
// *TODO* lowhanging fruit: during XR fragments-query milestone, target objects using XR fragment queries
// to provide jquery-ish interface for three.js)
//
@ -120,38 +122,9 @@
window.XRF = XRF // expose to form
// load 3D asset
let model;
const loader = new GLTFLoader().setPath( './../../assets/')
let loadGLTF = function ( gltf ) {
if( model ){
scene.remove(model)
//model.dispose()
}
gltf.scene.position.y = 1.5
gltf.scene.position.z = -4
gltf.scene.rotation.y = -0.5
gltf.scene.scale.x = gltf.scene.scale.y = gltf.scene.scale.z = 1;
const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
function recursivelySetChildrenUnlit(mesh,cb) {
cb(mesh)
if (mesh.children) {
for (var i = 0; i < mesh.children.length; i++) {
recursivelySetChildrenUnlit(mesh.children[i],cb);
}
}
}
scene.add( model = gltf.scene );
render();
};
let file = 'example2.gltf'
let file = document.location.search.length > 2 ? document.location.search.substr(1) + document.location.hash : './../../assets/example3.gltf#pos=1,0,4&rot=0,-30,0'
$('#model').setAttribute("href","./../../asset/"+file)
loader.load( file, loadGLTF );
XRF.navigator.to( file )
// setup mouse controls
@ -167,32 +140,33 @@
//controls.maxPolarAngle = Math.PI / 2;
//controls.target = new THREE.Vector3(0,1.6,0)
camera.position.set( -1, 1.6, 10 );
//controls.update()
//controls.update()
const geometry = new THREE.BufferGeometry();
geometry.setFromPoints( [ new THREE.Vector3( 0, 0, 0 ), new THREE.Vector3( 0, 0, - 5 ) ] );
const controller1 = renderer.xr.getController( 0 );
controller1.add( new THREE.Line( geometry ) );
scene.add( controller1 );
cameraRig.add( controller1 );
const controller2 = renderer.xr.getController( 1 );
controller2.add( new THREE.Line( geometry ) );
scene.add( controller2 );
cameraRig.add( controller2 );
const controllerModelFactory = new XRControllerModelFactory();
const controllerGrip1 = renderer.xr.getControllerGrip( 0 );
controllerGrip1.add( controllerModelFactory.createControllerModel( controllerGrip1 ) );
scene.add( controllerGrip1 );
cameraRig.add( controllerGrip1 );
const controllerGrip2 = renderer.xr.getControllerGrip( 1 );
controllerGrip2.add( controllerModelFactory.createControllerModel( controllerGrip2 ) );
scene.add( controllerGrip2 );
cameraRig.add( controllerGrip2 );
setupConsole( $('textarea') )
setupConsole()
setupUrlBar( $('input#uri') )
// GUI
@ -203,21 +177,12 @@
const gui = new GUI( { width: 300 } );
gui.add( parameters, 'env', 0.2, 3.0, 0.1 ).onChange( onChange );
const group = new InteractiveGroup( renderer, camera );
vrbutton.addEventListener('click', () => {
// show gui inside VR scene
gui.domElement.style.visibility = 'hidden';
scene.add( group );
})
const mesh = new HTMLMesh( gui.domElement );
mesh.position.x = - 0.75;
mesh.position.y = 1.5;
mesh.position.z = 0.3;
mesh.rotation.y = Math.PI / 4;
mesh.scale.setScalar( 2 );
group.add( mesh );
// Add stats.js
@ -232,7 +197,13 @@
statsMesh.position.z = 0.3;
statsMesh.rotation.y = Math.PI / 4;
statsMesh.scale.setScalar( 2.5 );
group.add( statsMesh );
vrbutton.addEventListener('click', () => {
// show gui inside VR scene
gui.domElement.style.visibility = 'hidden';
XRF.interactive.add( mesh );
XRF.interactive.add( statsMesh );
})
let fileLoaders = loadFile({
".gltf": (file) => file.arrayBuffer().then( (data) => loader.parse( data, '', loadGLTF, console.error ) ),
@ -264,8 +235,6 @@
//torus.rotation.x = time * 0.4;
//torus.rotation.y = time;
if( XRF.getLastModel() ) XRF.getLastModel().render(scene,camera)
//controls.update()
renderer.render( scene, camera );
stats.update();

View file

@ -1,91 +0,0 @@
body {
margin: 0;
background-color: #000;
color: #fff;
font-family: Monospace;
font-size: 13px;
line-height: 24px;
overscroll-behavior: none;
}
a {
color: #ff0;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
button {
cursor: pointer;
text-transform: uppercase;
}
#info {
position: absolute;
top: 0px;
width: 100%;
padding: 10px;
box-sizing: border-box;
text-align: center;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
z-index: 1; /* TODO Solve this in HTML */
}
a, button, input, select {
pointer-events: auto;
}
.lil-gui {
z-index: 2 !important; /* TODO Solve this in HTML */
}
@media all and ( max-width: 640px ) {
.lil-gui.root {
right: auto;
top: auto;
max-height: 50%;
max-width: 80%;
bottom: 0;
left: 0;
}
}
#overlay {
position: absolute;
font-size: 16px;
z-index: 2;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
background: rgba(0,0,0,0.7);
}
#overlay button {
background: transparent;
border: 0;
border: 1px solid rgb(255, 255, 255);
border-radius: 4px;
color: #ffffff;
padding: 12px 18px;
text-transform: uppercase;
cursor: pointer;
}
#notSupported {
width: 50%;
margin: auto;
background-color: #f00;
margin-top: 20px;
padding: 10px;
}

View file

@ -0,0 +1 @@
../../assets/other.gltf

File diff suppressed because one or more lines are too long

16
make
View file

@ -50,11 +50,21 @@ build(){
build_js(){
# add js module
cp dist/xrfragment.js dist/xrfragment.module.js
echo "export default xrfragment;" >> dist/xrfragment.module.js
cat dist/license.js dist/xrfragment.js > dist/xrfragment.module.js
echo "export default xrfragment;" >> dist/xrfragment.module.js
# add THREE module
cat dist/xrfragment.js src/three/*.js src/three/xrf/*.js > dist/xrfragment.three.js
cat dist/license.js \
dist/xrfragment.js \
src/3rd/three/*.js \
src/3rd/three/xrf/*.js > dist/xrfragment.three.js
echo "export default xrfragment;" >> dist/xrfragment.three.js
# add AFRAME
cat dist/license.js \
dist/xrfragment.js \
src/3rd/three/*.js \
src/3rd/three/xrf/*.js \
src/3rd/aframe/*.js > dist/xrfragment.aframe.js
ls -la dist | grep js
exit $ok
}

63
src/3rd/aframe/index.js Normal file
View file

@ -0,0 +1,63 @@
window.AFRAME.registerComponent('xrf', {
schema: {
},
init: function () {
if( !AFRAME.XRF ) this.initXRFragments()
if( this.data ){
AFRAME.XRF.navigator.to(this.data)
.then( (model) => {
let gets = [ ...document.querySelectorAll('[xrf-get]') ]
gets.map( (g) => g.emit('update') )
})
}
},
initXRFragments: function(){
//window.addEventListener('popstate', clear )
//window.addEventListener('pushstate', clear )
// enable XR fragments
let aScene = document.querySelector('a-scene')
let XRF = AFRAME.XRF = xrfragment.init({
THREE,
camera: aScene.camera,
scene: aScene.object3D,
renderer: aScene.renderer,
debug: true,
loaders: { gltf: THREE.GLTFLoader } // which 3D assets (exts) to check for XR fragments?
})
if( !XRF.camera ) throw 'xrfragment: no camera detected, please declare <a-entity camera..> ABOVE entities with xrf-attributes'
// override the camera-related XR Fragments so the camera-rig is affected
let camOverride = (xrf,v,opts) => {
opts.camera = document.querySelector('[camera]').object3D.parent
console.dir(opts.camera)
xrf(v,opts)
}
XRF.pos = camOverride
XRF.rot = camOverride
XRF.href = (xrf,v,opts) => { // convert portal to a-entity so AFRAME
camOverride(xrf,v,opts) // raycaster can find & execute it
let {mesh,camera} = opts;
let el = document.createElement("a-entity")
el.setAttribute("xrf-get",mesh.name )
el.setAttribute("class","ray")
el.addEventListener("click", mesh.userData.XRF.href.exec )
$('a-scene').appendChild(el)
}
// cleanup xrf-get objects when resetting scene
XRF.reset = ((reset) => () => {
reset()
console.log("aframe reset")
let els = [...document.querySelectorAll('[xrf-get]')]
els.map( (el) => document.querySelector('a-scene').removeChild(el) )
})(XRF.reset)
// undo lookup-control shenanigans (which blocks updating camerarig position in VR)
aScene.addEventListener('enter-vr', () => document.querySelector('[camera]').object3D.parent.matrixAutoUpdate = true )
},
})

View file

@ -0,0 +1,108 @@
window.AFRAME.registerComponent('xrf-button', {
schema: {
label: {
default: 'label'
},
width: {
default: 0.11
},
toggable: {
default: false
},
textSize: {
default: 0.66
},
color:{
default: '#111'
},
textColor:{
default: '#fff'
},
hicolor:{
default: '#555555'
},
action:{
default: ''
}
},
init: function() {
var el = this.el;
var labelEl = this.labelEl = document.createElement('a-entity');
this.color = this.data.color
el.setAttribute('geometry', {
primitive: 'box',
width: this.data.width,
height: 0.05,
depth: 0.005
});
el.setAttribute('material', {
color: this.color,
transparent:true,
opacity:0.3
});
el.setAttribute('pressable', '');
labelEl.setAttribute('position', '0 0 0.01');
labelEl.setAttribute('text', {
value: this.data.label,
color: this.data.textColor,
align: 'center'
});
labelEl.setAttribute('scale', `${this.data.textSize} ${this.data.textSize} ${this.data.textSize}`);
this.el.appendChild(labelEl);
this.bindMethods();
this.el.addEventListener('stateadded', this.stateChanged);
this.el.addEventListener('stateremoved', this.stateChanged);
this.el.addEventListener('pressedstarted', this.onPressedStarted);
this.el.addEventListener('pressedended', this.onPressedEnded);
this.el.addEventListener('mouseenter', (e) => this.onMouseEnter(e) );
this.el.addEventListener('mouseleave', (e) => this.onMouseLeave(e) );
if( this.data.action ){
this.el.addEventListener('click', new Function(this.data.action) )
}
},
bindMethods: function() {
this.stateChanged = this.stateChanged.bind(this);
this.onPressedStarted = this.onPressedStarted.bind(this);
this.onPressedEnded = this.onPressedEnded.bind(this);
},
update: function(oldData) {
if (oldData.label !== this.data.label) {
this.labelEl.setAttribute('text', 'value', this.data.label);
}
},
stateChanged: function() {
var color = this.el.is('pressed') ? this.data.hicolor : this.color;
this.el.setAttribute('material', {
color: color
});
},
onMouseEnter: function(){
this.el.setAttribute('material', { color: this.data.hicolor });
},
onMouseLeave: function(){
this.el.setAttribute('material', { color: this.color });
},
onPressedStarted: function() {
var el = this.el;
el.setAttribute('material', {
color: this.data.hicolor
});
el.emit('click');
if (this.data.togabble) {
if (el.is('pressed')) {
el.removeState('pressed');
} else {
el.addState('pressed');
}
}
},
onPressedEnded: function() {
if (this.el.is('pressed')) {
return;
}
this.el.setAttribute('material', {
color: this.color
});
}
});

35
src/3rd/aframe/xrf-get.js Normal file
View file

@ -0,0 +1,35 @@
window.AFRAME.registerComponent('xrf-get', {
schema: {
name: {type: 'string'},
clone: {type: 'boolean', default:false}
},
init: function () {
var el = this.el;
var meshname = this.data.name || this.data;
this.el.addEventListener('update', (evt) => {
let scene = AFRAME.XRF.scene
let mesh = scene.getObjectByName(meshname);
if (!mesh){
console.error("mesh with name '"+meshname+"' not found in model")
return;
}
if( !this.data.clone ) mesh.parent.remove(mesh)
////mesh.updateMatrixWorld();
this.el.object3D.position.setFromMatrixPosition(scene.matrixWorld);
this.el.object3D.quaternion.setFromRotationMatrix(scene.matrixWorld);
mesh.xrf = true // mark for deletion by xrf
this.el.setObject3D('mesh', mesh );
if( !this.el.id ) this.el.setAttribute("id",`xrf-${mesh.name}`)
})
if( this.el.className == "ray" ) this.el.emit("update")
}
});

View file

@ -0,0 +1,26 @@
window.AFRAME.registerComponent('xrf-wear', {
schema:{
el: {type:"selector"},
position: {type:"vec3"},
rotation: {type:"vec3"}
},
init: function(){
$('a-scene').addEventListener('enter-vr', (e) => this.wear(e) )
$('a-scene').addEventListener('exit-vr', (e) => this.unwear(e) )
},
wear: function(){
if( !this.wearable ){
let d = this.data
this.wearable = new THREE.Group()
this.el.object3D.children.map( (c) => this.wearable.add(c) )
this.wearable.position.set( d.position.x, d.position.y, d.position.z)
this.wearable.rotation.set( d.rotation.x, d.rotation.y, d.rotation.z)
}
this.data.el.object3D.add(this.wearable)
},
unwear: function(){
this.data.el.remove(this.wearable)
this.wearable.children.map( (c) => this.el.object3D.add(c) )
delete this.wearable
}
})

View file

@ -0,0 +1,136 @@
// wrapper to survive in/outside modules
xrfragment.InteractiveGroup = function(THREE,renderer,camera){
let {
Group,
Matrix4,
Raycaster,
Vector2
} = THREE
const _pointer = new Vector2();
const _event = { type: '', data: _pointer };
class InteractiveGroup extends Group {
constructor( renderer, camera ) {
super();
if( !renderer || !camera ) return
// extract camera when camera-rig is passed
camera.traverse( (n) => String(n.type).match(/Camera/) ? camera = n : null )
const scope = this;
const raycaster = new Raycaster();
const tempMatrix = new Matrix4();
function nocollide(){
if( nocollide.tid ) return // ratelimit
_event.type = "nocollide"
scope.children.map( (c) => c.dispatchEvent(_event) )
nocollide.tid = setTimeout( () => nocollide.tid = null, 100 )
}
// Pointer Events
const element = renderer.domElement;
function onPointerEvent( event ) {
//event.stopPropagation();
const rect = renderer.domElement.getBoundingClientRect();
_pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1;
_pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1;
raycaster.setFromCamera( _pointer, camera );
const intersects = raycaster.intersectObjects( scope.children, false );
if ( intersects.length > 0 ) {
const intersection = intersects[ 0 ];
const object = intersection.object;
const uv = intersection.uv;
_event.type = event.type;
_event.data.set( uv.x, 1 - uv.y );
object.dispatchEvent( _event );
}else nocollide()
}
element.addEventListener( 'pointerdown', onPointerEvent );
element.addEventListener( 'pointerup', onPointerEvent );
element.addEventListener( 'pointermove', onPointerEvent );
element.addEventListener( 'mousedown', onPointerEvent );
element.addEventListener( 'mouseup', onPointerEvent );
element.addEventListener( 'mousemove', onPointerEvent );
element.addEventListener( 'click', onPointerEvent );
// WebXR Controller Events
// TODO: Dispatch pointerevents too
const events = {
'move': 'mousemove',
'select': 'click',
'selectstart': 'mousedown',
'selectend': 'mouseup'
};
function onXRControllerEvent( event ) {
const controller = event.target;
tempMatrix.identity().extractRotation( controller.matrixWorld );
raycaster.ray.origin.setFromMatrixPosition( controller.matrixWorld );
raycaster.ray.direction.set( 0, 0, - 1 ).applyMatrix4( tempMatrix );
const intersections = raycaster.intersectObjects( scope.children, false );
if ( intersections.length > 0 ) {
const intersection = intersections[ 0 ];
const object = intersection.object;
const uv = intersection.uv;
_event.type = events[ event.type ];
_event.data.set( uv.x, 1 - uv.y );
if( _event.type != "mousemove" ){
console.log(event.type+" => "+_event.type)
}
object.dispatchEvent( _event );
}else nocollide()
}
const controller1 = renderer.xr.getController( 0 );
controller1.addEventListener( 'move', onXRControllerEvent );
controller1.addEventListener( 'select', onXRControllerEvent );
controller1.addEventListener( 'selectstart', onXRControllerEvent );
controller1.addEventListener( 'selectend', onXRControllerEvent );
const controller2 = renderer.xr.getController( 1 );
controller2.addEventListener( 'move', onXRControllerEvent );
controller2.addEventListener( 'select', onXRControllerEvent );
controller2.addEventListener( 'selectstart', onXRControllerEvent );
controller2.addEventListener( 'selectend', onXRControllerEvent );
}
}
return new InteractiveGroup(renderer,camera)
}

144
src/3rd/three/index.js Normal file
View file

@ -0,0 +1,144 @@
let xrf = xrfragment
xrf.frag = {}
xrf.model = {}
xrf.init = function(opts){
opts = opts || {}
let XRF = function(){
alert("queries are not implemented (yet)")
}
for ( let i in opts ) xrf[i] = opts[i]
for ( let i in xrf.XRF ) xrf.XRF[i] // shortcuts to constants (NAVIGATOR e.g.)
xrf.Parser.debug = xrf.debug
if( opts.loaders ) Object.values(opts.loaders).map( xrf.patchLoader )
xrf.patchRenderer(opts.renderer)
xrf.navigator.init()
return xrf
}
xrf.patchRenderer = function(renderer){
renderer.xr.addEventListener( 'sessionstart', () => xrf.baseReferenceSpace = renderer.xr.getReferenceSpace() );
renderer.xr.enabled = true;
renderer.render = ((render) => function(scene,camera){
if( xrf.model && xrf.model.render )
xrf.model.render(scene,camera)
render(scene,camera)
})(renderer.render.bind(renderer))
}
xrf.patchLoader = function(loader){
loader.prototype.load = ((load) => function(url, onLoad, onProgress, onError){
load.call( this,
url,
(model) => { onLoad(model); xrf.parseModel(model,url) },
onProgress,
onError)
})(loader.prototype.load)
}
xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrf.parseModel = function(model,url){
let file = xrf.getFile(url)
model.file = file
model.render = function(){}
// eval embedded XR fragments
model.scene.traverse( (mesh) => xrf.eval.mesh(mesh,model) )
}
xrf.getLastModel = () => xrf.model.last
xrf.eval = function( url, model ){
let notice = false
model = model || xrf.model
let { THREE, camera } = xrf
let frag = xrf.URI.parse( url, xrf.XRF.NAVIGATOR )
let meshes = frag.q ? [] : [camera]
for ( let i in meshes ) {
for ( let k in frag ){
let mesh = meshes[i]
if( !String(k).match(/(pos|rot)/) ) notice = true
let opts = {frag, mesh, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE }
xrf.eval.fragment(k,opts)
}
}
if( notice ) alert("only 'pos' and 'rot' XRF.NAVIGATOR-flagged XR fragments are supported (for now)")
}
xrf.eval.mesh = (mesh,model) => {
if( mesh.userData ){
let frag = {}
for( let k in mesh.userData ) xrf.Parser.parse( k, mesh.userData[k], frag )
for( let k in frag ){
let opts = {frag, mesh, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE }
mesh.userData.XRF = frag // allow fragment impl to access XRF obj already
xrf.eval.fragment(k,opts)
}
}
}
xrf.eval.fragment = (k, opts ) => {
// call native function (xrf/env.js e.g.), or pass it to user decorator
let func = xrf.frag[k] || function(){}
if( xrf[k] ) xrf[k]( func, opts.frag[k], opts)
else func( opts.frag[k], opts)
}
xrf.reset = () => {
const disposeObject = (obj) => {
if (obj.children.length > 0) obj.children.forEach((child) => disposeObject(child));
if (obj.geometry) obj.geometry.dispose();
if (obj.material) {
if (obj.material.map) obj.material.map.dispose();
obj.material.dispose();
}
obj.clear()
obj.removeFromParent()
return true
};
let nodes = []
xrf.scene.traverse( (child) => child.isXRF ? nodes.push(child) : false )
nodes.map( disposeObject ) // leave non-XRF objects intact
xrf.interactive = xrf.InteractiveGroup( xrf.THREE, xrf.renderer, xrf.camera)
xrf.add( xrf.interactive)
}
xrf.parseUrl = (url) => {
const urlObj = new URL( url.match(/:\/\//) ? url : String(`https://fake.com/${url}`).replace(/\/\//,'/') )
let dir = url.substring(0, url.lastIndexOf('/') + 1)
const file = urlObj.pathname.substring(urlObj.pathname.lastIndexOf('/') + 1);
const hash = url.match(/#/) ? url.replace(/.*#/,'') : ''
const ext = file.split('.').pop()
return {urlObj,dir,file,hash,ext}
}
xrf.add = (object) => {
object.isXRF = true // mark for easy deletion when replacing scene
xrf.scene.add(object)
}
/*
* EVENTS
*/
xrf.addEventListener = function(eventName, callback) {
if( !this._listeners ) this._listeners = []
if (!this._listeners[eventName]) {
// create a new array for this event name if it doesn't exist yet
this._listeners[eventName] = [];
}
// add the callback to the listeners array for this event name
this._listeners[eventName].push(callback);
};
xrf.emit = function(eventName, data) {
if( !this._listeners ) this._listeners = []
var callbacks = this._listeners[eventName]
if (callbacks) {
for (var i = 0; i < callbacks.length; i++) {
callbacks[i](data);
}
}
};

View file

@ -0,0 +1,44 @@
xrf.navigator = {}
xrf.navigator.to = (url,event) => {
if( !url ) throw 'xrf.navigator.to(..) no url given'
return new Promise( (resolve,reject) => {
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
console.log("xrfragment: navigating to "+url)
if( !file || xrf.model.file == file ){ // we're already loaded
document.location.hash = `#${hash}` // just update the hash
xrf.eval( url, xrf.model ) // and eval URI XR fragments
return resolve(xrf.model)
}
if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false
const Loader = xrf.loaders[ext]
if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext
xrf.reset() // clear xrf objects from scene
// force relative path
if( dir ) dir = dir[0] == '.' ? dir : `.${dir}`
const loader = new Loader().setPath( dir )
loader.load( file, function(model){
model.file = file
xrf.add( model.scene )
xrf.model = model
xrf.eval( url, model ) // and eval URI XR fragments
xrf.navigator.pushState( file, hash )
resolve(model)
})
})
}
xrf.navigator.init = () => {
if( xrf.navigator.init.inited ) return
window.addEventListener('popstate', function (event){
xrf.navigator.to( document.location.search.substr(1) + document.location.hash, event)
})
xrf.navigator.init.inited = true
}
xrf.navigator.pushState = (file,hash) => {
if( file == document.location.search.substr(1) ) return // page is in its default state
window.history.pushState({},`${file}#${hash}`, document.location.pathname + `?${file}#${hash}` )
}

35
src/3rd/three/xrf/env.js Normal file
View file

@ -0,0 +1,35 @@
xrf.frag.env = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
let env = mesh.getObjectByName(v.string)
env.material.map.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = env.material.map
//scene.texture = env.material.map
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 2;
// apply to meshes *DISABLED* renderer.environment does this
const maxAnisotropy = renderer.capabilities.getMaxAnisotropy();
setTimeout( () => {
scene.traverse( (mesh) => {
//if (mesh.material && mesh.material.map && mesh.material.metalness == 1.0) {
// mesh.material = new THREE.MeshBasicMaterial({ map: mesh.material.map });
// mesh.material.dithering = true
// mesh.material.map.anisotropy = maxAnisotropy;
// mesh.material.needsUpdate = true;
//}
//if (mesh.material && mesh.material.metalness == 1.0 ){
// mesh.material = new THREE.MeshBasicMaterial({
// color:0xffffff,
// emissive: mesh.material.map,
// envMap: env.material.map,
// side: THREE.DoubleSide,
// flatShading: true
// })
// mesh.material.needsUpdate = true
// //mesh.material.envMap = env.material.map;
// //mesh.material.envMap.intensity = 5;
// //mesh.material.needsUpdate = true;
//}
});
},500)
console.log(` └ applied image '${v.string}' as environment map`)
}

152
src/3rd/three/xrf/href.js Normal file
View file

@ -0,0 +1,152 @@
/**
*
* navigation, portals & mutations
*
* | fragment | type | scope | example value |
* |`href`| string (uri or predefined view) | 🔒 |`#pos=1,1,0`<br>`#pos=1,1,0&rot=90,0,0`<br>`#pos=pyramid`<br>`#pos=lastvisit\|pyramid`<br>`://somefile.gltf#pos=1,1,0`<br> |
*
* [[» example implementation|https://github.com/coderofsalvation/xrfragment/blob/main/src/three/xrf/pos.js]]<br>
* [[» example 3D asset|https://github.com/coderofsalvation/xrfragment/blobl/main/src/example/assets/href.gltf]]<br>
* [[» discussion|https://github.com/coderofsalvation/xrfragment/issues/1]]<br>
*
* [img[xrfragment.jpg]]
*
*
* !!!spec 1.0
*
* 1. an ''external''- or ''file URI'' fully replaces the current scene and assumes `pos=0,0,0&rot=0,0,0` by default (unless specified)
*
* 2. navigation should not happen when queries (`q=`) are present in local url: queries will apply (`pos=`, `rot=` e.g.) to the targeted object(s) instead.
*
* 3. navigation should not happen ''immediately'' when user is more than 2 meter away from the portal/object containing the href (to prevent accidental navigation e.g.)
*
* 4. URL navigation should always be reflected in the client (in case of javascript: see [[here|https://github.com/coderofsalvation/xrfragment/blob/dev/src/3rd/three/navigator.js]] for an example navigator).
*
* 5. In XR mode, the navigator back/forward-buttons should be always visible (using a wearable e.g., see [[here|https://github.com/coderofsalvation/xrfragment/blob/dev/example/aframe/sandbox/index.html#L26-L29]] for an example wearable)
*
* [img[navigation.png]]
*
*/
xrf.frag.href = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
const world = {
pos: new THREE.Vector3(),
scale: new THREE.Vector3(),
quat: new THREE.Quaternion()
}
mesh.getWorldPosition(world.pos)
mesh.getWorldScale(world.scale)
mesh.getWorldQuaternion(world.quat);
mesh.position.copy(world.pos)
mesh.scale.copy(world.scale)
mesh.setRotationFromQuaternion(world.quat);
// detect equirectangular image
let texture = mesh.material.map
if( texture && texture.source.data.height == texture.source.data.width/2 ){
texture.mapping = THREE.ClampToEdgeWrapping
texture.needsUpdate = true
// poor man's equi-portal
mesh.material = new THREE.ShaderMaterial( {
side: THREE.DoubleSide,
uniforms: {
pano: { value: texture },
selected: { value: false },
},
vertexShader: `
vec3 portalPosition;
varying vec3 vWorldPosition;
varying float vDistanceToCenter;
varying float vDistance;
void main() {
vDistanceToCenter = clamp(length(position - vec3(0.0, 0.0, 0.0)), 0.0, 1.0);
portalPosition = (modelMatrix * vec4(0.0, 0.0, 0.0, 1.0)).xyz;
vDistance = length(portalPosition - cameraPosition);
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
#define RECIPROCAL_PI2 0.15915494
uniform sampler2D pano;
uniform bool selected;
varying float vDistanceToCenter;
varying float vDistance;
varying vec3 vWorldPosition;
void main() {
vec3 direction = normalize(vWorldPosition - cameraPosition);
vec2 sampleUV;
sampleUV.y = -clamp(direction.y * 0.5 + 0.5, 0.0, 1.0);
sampleUV.x = atan(direction.z, -direction.x) * -RECIPROCAL_PI2;
sampleUV.x += 0.33; // adjust focus to AFRAME's a-scene.components.screenshot.capture()
vec4 color = texture2D(pano, sampleUV);
// Convert color to grayscale (lazy lite approach to not having to match tonemapping/shaderstacking of THREE.js)
float luminance = 0.2126 * color.r + 0.7152 * color.g + 0.0722 * color.b;
vec4 grayscale_color = selected ? color : vec4(vec3(luminance) + vec3(0.33), color.a);
gl_FragColor = grayscale_color;
}
`,
});
mesh.material.needsUpdate = true
}
let teleport = mesh.userData.XRF.href.exec = (e) => {
const meshWorldPosition = new THREE.Vector3();
meshWorldPosition.setFromMatrixPosition(mesh.matrixWorld);
let portalArea = 1 // 2 meter
const cameraDirection = new THREE.Vector3();
camera.getWorldPosition(cameraDirection);
cameraDirection.sub(meshWorldPosition);
cameraDirection.normalize();
cameraDirection.multiplyScalar(portalArea); // move away from portal
const newPos = meshWorldPosition.clone().add(cameraDirection);
const distance = camera.position.distanceTo(newPos);
//if( distance > portalArea ){
if( !renderer.xr.isPresenting && !confirm("teleport to "+v.string+" ?") ) return
xrf.navigator.to(v.string) // ok let's surf to HREF!
console.log("teleport!")
xrf.emit('href',{click:true,mesh,xrf:v})
}
let selected = (state) => () => {
if( mesh.selected == state ) return // nothing changed
if( mesh.material.uniforms ) mesh.material.uniforms.selected.value = state
else mesh.material.color.r = mesh.material.color.g = mesh.material.color.b = state ? 2.0 : 1.0
// update mouse cursor
if( !renderer.domElement.lastCursor )
renderer.domElement.lastCursor = renderer.domElement.style.cursor
renderer.domElement.style.cursor = state ? 'pointer' : renderer.domElement.lastCursor
xrf.emit('href',{selected:state,mesh,xrf:v})
mesh.selected = state
}
if( !opts.frag.q ){ // query means an action
mesh.addEventListener('click', teleport )
mesh.addEventListener('mousemove', selected(true) )
mesh.addEventListener('nocollide', selected(false) )
}
// lazy add mesh (because we're inside a recursive traverse)
setTimeout( (mesh) => {
xrf.interactive.add(mesh)
}, 20, mesh )
}
/**
* > above solutions were abducted from [[this|https://i.imgur.com/E3En0gJ.png]] and [[this|https://i.imgur.com/lpnTz3A.png]] survey result
*
* !!!Demo
*
* > taken from <a href="./example/aframe/sandbox" target="_blank">aframe/sandbox</a>
*
* <video width="100%" autoplay="" muted="" loop="">
* <source src="https://coderofsalvation.github.io/xrfragment.media/href.mp4" type="video/mp4">Your browser does not support the video element.
* </video>
*/

47
src/3rd/three/xrf/pos.js Normal file
View file

@ -0,0 +1,47 @@
xrf.frag.pos = function(v, opts){
//if( renderer.xr.isPresenting ) return // too far away
let { frag, mesh, model, camera, scene, renderer, THREE} = opts
console.log(" └ setting camera position to "+v.string)
if( !frag.q ){
if( true ){//!renderer.xr.isPresenting ){
console.dir(camera)
camera.position.x = v.x
camera.position.y = v.y
camera.position.z = v.z
}
/*
else{ // XR
let cameraWorldPosition = new THREE.Vector3()
camera.object3D.getWorldPosition(this.cameraWorldPosition)
let newRigWorldPosition = new THREE.Vector3(v.x,v.y,x.z)
// Finally update the cameras position
let newRigLocalPosition.copy(this.newRigWorldPosition)
if (camera.object3D.parent) {
camera.object3D.parent.worldToLocal(newRigLocalPosition)
}
camera.setAttribute('position', newRigLocalPosition)
// Also take the headset/camera rotation itself into account
if (this.data.rotateOnTeleport) {
this.teleportOcamerainQuaternion
.setFromEuler(new THREE.Euler(0, this.teleportOcamerain.object3D.rotation.y, 0))
this.teleportOcamerainQuaternion.invert()
this.teleportOcamerainQuaternion.multiply(this.hitEntityQuaternion)
// Rotate the camera based on calculated teleport ocamerain rotation
this.cameraRig.object3D.setRotationFromQuaternion(this.teleportOcamerainQuaternion)
}
console.log("XR")
const offsetPosition = { x: - v.x, y: - v.y, z: - v.z, w: 1 };
const offsetRotation = new THREE.Quaternion();
const transform = new XRRigidTransform( offsetPosition, offsetRotation );
const teleportSpaceOffset = xrf.baseReferenceSpace.getOffsetReferenceSpace( transform );
renderer.xr.setReferenceSpace( teleportSpaceOffset );
}
*/
}
}

14
src/3rd/three/xrf/q.js Normal file
View file

@ -0,0 +1,14 @@
xrf.frag.q = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts
console.log(" └ running query ")
for ( let i in v.query ) {
let target = v.query[i]
// remove objects if requested
if( target.id != undefined && (target.mesh = scene.getObjectByName(i)) ){
target.mesh.visible = target.id
target.mesh.parent.remove(target.mesh)
console.log(` └ removing mesh: ${i}`)
}else console.log(` └ mesh not found: ${i}`)
}
}

9
src/3rd/three/xrf/rot.js Normal file
View file

@ -0,0 +1,9 @@
xrf.frag.rot = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
console.log(" └ setting camera rotation to "+v.string)
camera.rotation.set(
v.x * Math.PI / 180,
v.y * Math.PI / 180,
v.z * Math.PI / 180
)
}

38
src/3rd/three/xrf/src.js Normal file
View file

@ -0,0 +1,38 @@
// *TODO* use webgl instancing
xrf.frag.src = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
if( v.string[0] == "#" ){ // local
console.log(" └ instancing src")
let frag = xrfragment.URI.parse(v.string)
// Get an instance of the original model
let sceneInstance = new THREE.Group()
sceneInstance.isSrc = true
// prevent infinite recursion #1: skip src-instanced models
for ( let i in model.scene.children ) {
let child = model.scene.children[i]
if( child.isSrc ) continue;
sceneInstance.add( model.scene.children[i].clone() )
}
sceneInstance.position.copy( mesh.position )
sceneInstance.scale.copy(mesh.scale)
sceneInstance.updateMatrixWorld(true) // needed because we're going to move portals to the interactive-group
// apply embedded XR fragments
setTimeout( () => {
sceneInstance.traverse( (m) => {
if( m.userData && m.userData.src ) return ;//delete m.userData.src // prevent infinite recursion
xrf.eval.mesh(m,{scene,recursive:true})
})
// apply URI XR Fragments inside src-value
for( var i in frag ){
xrf.eval.fragment(i, Object.assign(opts,{frag, model:{scene:sceneInstance},scene:sceneInstance}))
}
// Add the instance to the scene
model.scene.add(sceneInstance);
},10)
}
}

2
src/3rd/wasm/README.md Normal file
View file

@ -0,0 +1,2 @@
javy compile index.js -o index.wasm
echo '{ "n": 2, "bar": "baz" }' | wasmtime index.wasm

50
src/3rd/wasm/index.js Normal file
View file

@ -0,0 +1,50 @@
// Read input from stdin
const input = readInput();
// Call the function with the input
const result = foo(input);
// Write the result to stdout
writeOutput(result);
// The main function.
function foo(input) {
return { foo: input.n + 1, newBar: input.bar + "!" };
}
// Read input from stdin
function readInput() {
const chunkSize = 1024;
const inputChunks = [];
let totalBytes = 0;
// Read all the available bytes
while (1) {
const buffer = new Uint8Array(chunkSize);
// Stdin file descriptor
const fd = 0;
const bytesRead = Javy.IO.readSync(fd, buffer);
totalBytes += bytesRead;
if (bytesRead === 0) {
break;
}
inputChunks.push(buffer.subarray(0, bytesRead));
}
// Assemble input into a single Uint8Array
const { finalBuffer } = inputChunks.reduce((context, chunk) => {
context.finalBuffer.set(chunk, context.bufferOffset);
context.bufferOffset += chunk.length;
return context;
}, { bufferOffset: 0, finalBuffer: new Uint8Array(totalBytes) });
return JSON.parse(new TextDecoder().decode(finalBuffer));
}
// Write output to stdout
function writeOutput(output) {
const encodedOutput = new TextEncoder().encode(JSON.stringify(output));
const buffer = new Uint8Array(encodedOutput);
// Stdout file descriptor
const fd = 1;
Javy.IO.writeSync(fd, buffer);
}

BIN
src/3rd/wasm/index.wasm Normal file

Binary file not shown.

View file

@ -1,74 +0,0 @@
xrfragment.xrf = {}
xrfragment.model = {}
xrfragment.init = function(opts){
opts = opts || {}
let XRF = function(){
alert("queries are not implemented (yet)")
}
for ( let i in opts ) XRF[i] = xrfragment[i] = opts[i]
for ( let i in xrfragment ) XRF[i] = xrfragment[i]
for ( let i in xrfragment.XRF ) XRF[i] = xrfragment.XRF[i] // shortcuts to constants (NAVIGATOR e.g.)
xrfragment.Parser.debug = xrfragment.debug
if( opts.loaders ) opts.loaders.map( xrfragment.patchLoader )
return XRF
}
xrfragment.patchLoader = function(loader){
loader.prototype.load = ((load) => function(url, onLoad, onProgress, onError){
load.call( this,
url,
(model) => { onLoad(model); xrfragment.parseModel(model,url) },
onProgress,
onError)
})(loader.prototype.load)
}
xrfragment.getFile = (url) => url.split("/").pop().replace(/#.*/,'')
xrfragment.parseModel = function(model,url){
let file = xrfragment.getFile(url)
model.file = file
model.render = function(){}
xrfragment.model[file] = model
console.log("scanning "+file)
model.scene.traverse( (mesh) => {
console.log(mesh.name)
if( mesh.userData ){
let frag = {}
for( let k in mesh.userData ){
xrfragment.Parser.parse( k, mesh.userData[k], frag )
// call native function (xrf/env.js e.g.), or pass it to user decorator
let func = xrfragment.xrf[k] || function(){}
let opts = {mesh, model, camera: xrfragment.camera, scene: xrfragment.scene, renderer: xrfragment.renderer, THREE: xrfragment.THREE }
if( xrfragment[k] ) xrfragment[k]( func, frag[k], opts)
else func( frag[k], opts)
}
}
})
}
xrfragment.getLastModel = () => Object.values(xrfragment.model)[ Object.values(xrfragment.model).length-1 ]
xrfragment.eval = function( url, model ){
let notice = false
let { THREE, camera } = xrfragment
let frag = xrfragment.URI.parse( url, XRF.NAVIGATOR )
for ( let i in frag ) {
if( !String(i).match(/(pos|rot)/) ) notice = true
if( i == "pos" ){
camera.position.x = frag.pos.x
camera.position.y = frag.pos.y
camera.position.z = frag.pos.z
}
if( i == "rot" ){
camera.rotation.x = THREE.MathUtils.degToRad( frag.pos.x )
camera.rotation.y = THREE.MathUtils.degToRad( frag.pos.y )
camera.rotation.z = THREE.MathUtils.degToRad( frag.pos.z )
}
}
if( notice ) alert("only 'pos' and 'rot' XRF.NAVIGATOR-flagged XR fragments are supported (for now)")
console.dir({url,model,frag})
}

View file

@ -1,10 +0,0 @@
xrfragment.xrf.env = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
let env = mesh.getObjectByName(v.string)
env.material.map.mapping = THREE.EquirectangularReflectionMapping;
scene.environment = env.material.map
scene.texture = env.material.map
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1;
console.log(` └ applied image '${v.string}' as environtment map`)
}

View file

@ -1,57 +0,0 @@
xrfragment.xrf.href = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
return
// Create a shader material that treats the texture as an equirectangular map
mesh.texture = mesh.material.map // backup texture
const equirectShader = THREE.ShaderLib[ 'equirect' ];
const equirectMaterial = new THREE.ShaderMaterial( {
uniforms: THREE.UniformsUtils.merge([
THREE.UniformsLib.equirect,
equirectShader.uniforms,
]),
vertexShader: equirectShader.vertexShader,
fragmentShader: equirectShader.fragmentShader,
side: THREE.DoubleSide //THREE.FrontSide //THREE.DoubleSide //THREE.BackSide
} );
equirectMaterial.uniforms[ 'tEquirect' ].value = mesh.texture
// Define the tEquirectInvProjection uniform
equirectMaterial.uniforms.tEquirectInvProjection = {
value: new THREE.Matrix4(),
};
// Assign the new material to the mesh
mesh.material = equirectMaterial;
console.dir(mesh.material)
mesh.texture.wrapS = THREE.RepeatWrapping;
// patch custom model renderloop
model.render = ((render) => (scene,camera) => {
// Store the original projection matrix of the camera
const originalProjectionMatrix = camera.projectionMatrix.clone();
// Calculate the current camera view matrix
const aspectRatio = mesh.texture.image.width / mesh.texture.image.height;
camera.projectionMatrix.makePerspective(camera.fov, aspectRatio, camera.near, camera.far);
const viewMatrix = camera.matrixWorldInverse;
const worldMatrix = mesh.matrixWorld;
const equirectInvProjection = new THREE.Matrix4();
equirectInvProjection.copy(camera.projectionMatrix).multiply(viewMatrix).invert();
// Update the equirectangular material's tEquirect uniform
equirectMaterial.uniforms.tEquirect.value = mesh.texture;
equirectMaterial.uniforms.tEquirectInvProjection.value.copy(
equirectInvProjection
);
// Reset the camera projection matrix
camera.projectionMatrix.copy(originalProjectionMatrix);
render(scene,camera)
})(model.render)
console.dir(mesh)
}

View file

@ -1,7 +0,0 @@
xrfragment.xrf.pos = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
console.log(" └ setting camera position to "+v.string)
camera.position.x = v.x
camera.position.y = v.y
camera.position.z = v.z
}

View file

@ -1,31 +0,0 @@
xrfragment.xrf.src = function(v, opts){
let { mesh, model, camera, scene, renderer, THREE} = opts
if( v.string[0] == "#" ){ // local
console.log(" └ instancing src")
let args = xrfragment.URI.parse(v.string)
// Get an instance of the original model
const modelInstance = new THREE.Group();
modelInstance.add(model.scene.clone());
modelInstance.position.z = mesh.position.x
modelInstance.position.y = mesh.position.y
modelInstance.position.x = mesh.position.z
modelInstance.scale.z = mesh.scale.x
modelInstance.scale.y = mesh.scale.y
modelInstance.scale.x = mesh.scale.z
// now apply XR Fragments overrides from URI
// *TODO* move to a central location (pull-up)
for( var i in args ){
if( i == "scale" ){
console.log(" └ setting scale")
modelInstance.scale.x = args[i].x
modelInstance.scale.y = args[i].y
modelInstance.scale.z = args[i].z
}
}
// Add the instance to the scene
scene.add(modelInstance);
console.dir(model)
console.dir(modelInstance)
}
}

View file

@ -1,3 +1,5 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 Leon van Kammen/NLNET
package xrfragment;
import xrfragment.XRF;

View file

@ -1,3 +1,6 @@
-- SPDX-License-Identifier: MPL-2.0
-- Copyright (c) 2023 Leon van Kammen/NLNET
XF = {}
function split (inputstr, sep)

View file

@ -1,3 +1,5 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 Leon van Kammen/NLNET
package xrfragment;
//return untyped __js__("window.location.search");

View file

@ -1,3 +1,5 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 Leon van Kammen/NLNET
package xrfragment;
import xrfragment.Parser;

View file

@ -1,3 +1,5 @@
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 Leon van Kammen/NLNET
package xrfragment;
@:expose // <- makes the class reachable from plain JavaScript

View file

@ -1,3 +1,6 @@
-- SPDX-License-Identifier: MPL-2.0
-- Copyright (c) 2023 Leon van Kammen/NLNET
local XRF = {}
XRF.ASSET = 1