stable: new filters work + separated scenes + pos=world1 works

This commit is contained in:
Leon van Kammen 2023-11-24 17:32:53 +01:00
parent cc3f58f493
commit 2262168c3e
19 changed files with 202 additions and 156 deletions

View file

@ -279,7 +279,7 @@ For example, to render a portal with a preview-version of the scene, create an 3
| fragment | type | functionality | | fragment | type | functionality |
|----------|--------|------------------------------| |----------|--------|------------------------------|
| <b>#pos</b>=0,0,0 | vector3 | (re)position camera | | <b>#pos</b>=0,0,0 | vector3 or string| (re)position camera based on coordinates directly, or indirectly using objectname (its worldposition) |
| <b>#t</b>=0,100 | vector3 | set playback speed, and (re)position looprange of scene-animation or `src`-mediacontent | | <b>#t</b>=0,100 | vector3 | set playback speed, and (re)position looprange of scene-animation or `src`-mediacontent |
| <b>#rot</b>=0,90,0 | vector3 | rotate camera | | <b>#rot</b>=0,90,0 | vector3 | rotate camera |
@ -366,7 +366,7 @@ Resizing will be happen accordingly to its placeholder object `aquariumcube`, se
1. local/remote content is instanced by the `src` (filter) value (and attaches it to the placeholder mesh containing the `src` property) 1. local/remote content is instanced by the `src` (filter) value (and attaches it to the placeholder mesh containing the `src` property)
2. by default all objects are loaded into the instanced src (scene) object (but not shown yet) 2. by default all objects are loaded into the instanced src (scene) object (but not shown yet)
2. <b>local</b> `src` values (`#...` e.g.) starting with a non-negating filter (`#cube` e.g.) will make that object (with name `cube`) the new root of the scene at position 0,0,0 2. <b>local</b> `src` values (`#...` e.g.) starting with a non-negating filter (`#cube` e.g.) will (deep)reparent that object (with name `cube`) as the new root of the scene at position 0,0,0
3. <b>local</b> `src` values should respect (negative) filters (`#-foo&price=>3`) 3. <b>local</b> `src` values should respect (negative) filters (`#-foo&price=>3`)
4. the instanced scene (from a `src` value) should be <b>scaled accordingly</b> to its placeholder object or <b>scaled relatively</b> based on the scale-property (of a geometry-less placeholder, an 'empty'-object in blender e.g.). For more info see Chapter Scaling. 4. the instanced scene (from a `src` value) should be <b>scaled accordingly</b> to its placeholder object or <b>scaled relatively</b> based on the scale-property (of a geometry-less placeholder, an 'empty'-object in blender e.g.). For more info see Chapter Scaling.
5. <b>external</b> `src` values should be served with appropriate mimetype (so the XR Fragment-compatible browser will now how to render it). The bare minimum supported mimetypes are: 5. <b>external</b> `src` values should be served with appropriate mimetype (so the XR Fragment-compatible browser will now how to render it). The bare minimum supported mimetypes are:
@ -415,6 +415,14 @@ navigation, portals & mutations
[» example 3D asset](https://github.com/coderofsalvation/xrfragment/blob/main/example/assets/href.gltf#L192)<br> [» example 3D asset](https://github.com/coderofsalvation/xrfragment/blob/main/example/assets/href.gltf#L192)<br>
[» discussion](https://github.com/coderofsalvation/xrfragment/issues/1)<br> [» discussion](https://github.com/coderofsalvation/xrfragment/issues/1)<br>
## Walking surfaces
XR Fragment-compatible viewers can infer this data based scanning the scene for:
1. materialless (nameless & textureless) mesh-objects (without `src` and `href`)
> optionally the viewer can offer thumbstick, mouse or joystick teleport-tools for non-roomscale VR/AR setups.
## UX spec ## UX spec
End-users should always have read/write access to: End-users should always have read/write access to:
@ -503,16 +511,19 @@ It's simple but powerful syntax which allows filtering the scene using searcheng
## including/excluding ## including/excluding
By default, selectors work like photoshop-layers: they scan for matching layer(name/properties) within the scene-graph.
Each matched object (not their children) will be toggled (in)visible when selecting.
| operator | info | | operator | info |
|----------|-------------------------------------------------------------------------------------------------------------------------------| |----------|-------------------------------------------------------------------------------------------------------------------------------|
| `-` | hides object(s) (`#-myobject&-objects` e.g. | | `-` | hides object(s) (`#-myobject&-objects` e.g. |
| `=` | indicates an object-embedded custom property key/value (`#price=4&category=foo` e.g.) | | `=` | indicates an object-embedded custom property key/value (`#price=4&category=foo` e.g.) |
| `=>` `=<`| compare float or int number (`#price=>4` e.g.) | | `=>` `=<`| compare float or int number (`#price=>4` e.g.) |
| `/` | reference to root-scene.<br>Useful in case of (preventing) showing/hiding objects in nested scenes (instanced by `src`) (*) | | `*` | deepselect: automatically select children of selected object, including local (nonremote) embedded objects (starting with `#`)|
> \* = `#-/cube` hides object `cube` only in the root-scene (not nested `cube` objects)<br> `#-cube` hides both object `cube` in the root-scene <b>AND</b> nested `skybox` objects | > NOTE 1: after an external embedded object has been instanced (`src: https://y.com/bar.fbx#room` e.g.), filters do not affect them anymore (reason: local tag/name collisions can be mitigated easily, but not in case of remote content).
Nested selection is always implied (there's no `*` `>`/`<` css-like operators on purpose) which keeps XR Fragments easy to implement, and still allows fine-grained control chaining nested selectors (`#-sky&house&-table` e.g.). > NOTE 2: depending on the used 3D framework, toggling objects (in)visible should happen by enabling/disableing writing to the colorbuffer (to allow children being still visible while their parents are invisible).
[» example implementation](https://github.com/coderofsalvation/xrfragment/blob/main/src/3rd/js/three/xrf/q.js) [» example implementation](https://github.com/coderofsalvation/xrfragment/blob/main/src/3rd/js/three/xrf/q.js)
[» example 3D asset](https://github.com/coderofsalvation/xrfragment/blob/main/example/assets/filter.gltf#L192) [» example 3D asset](https://github.com/coderofsalvation/xrfragment/blob/main/example/assets/filter.gltf#L192)
@ -929,6 +940,15 @@ This document has no IANA actions.
* [NLNET](https://nlnet.nl) * [NLNET](https://nlnet.nl)
* [Future of Text](https://futureoftext.org) * [Future of Text](https://futureoftext.org)
* [visual-meta.info](https://visual-meta.info) * [visual-meta.info](https://visual-meta.info)
* Michiel Leenaars
* Gerben van der Broeke
* Mauve
* Jens Finkhäuser
* Marc Belmont
* Tim Gerritsen
* Frode Hegland
* Brandel Zackernuk
* Mark Anderson
# Appendix: Definitions # Appendix: Definitions

Binary file not shown.

View file

@ -26,7 +26,18 @@ window.AFRAME.registerComponent('xrf', {
}) })
if( !XRF.camera ) throw 'xrfragment: no camera detected, please declare <a-entity camera..> ABOVE entities with xrf-attributes' if( !XRF.camera ) throw 'xrfragment: no camera detected, please declare <a-entity camera..> ABOVE entities with xrf-attributes'
xrf.addEventListener('navigateLoaded', () => setTimeout( () => AFRAME.fade.out(),500) ) xrf.addEventListener('navigateLoaded', () => {
setTimeout( () => AFRAME.fade.out(),500)
// *TODO* this does not really belong here perhaps
let blinkControls = document.querySelector('[blink-controls]')
if( blinkControls ){
blinkControls = blinkControls.components['blink-controls']
blinkControls.defaultCollisionMeshes = xrf.getCollisionMeshes()
blinkControls.update()
}
})
xrf.addEventListener('href', (opts) => { xrf.addEventListener('href', (opts) => {
if( opts.click){ if( opts.click){
let p = opts.promise() let p = opts.promise()

View file

@ -48,7 +48,6 @@ pub.fragment = (k, opts ) => { // evaluate one fragment
pub.XRWG = (opts) => { pub.XRWG = (opts) => {
let {frag,scene,model,renderer} = opts let {frag,scene,model,renderer} = opts
console.dir(opts)
// if this query was triggered by an src-value, lets filter it // if this query was triggered by an src-value, lets filter it
const isSRC = opts.embedded && opts.embedded.fragment == 'src' const isSRC = opts.embedded && opts.embedded.fragment == 'src'

View file

@ -100,3 +100,9 @@ xrf.add = (object) => {
object.isXRF = true // mark for easy deletion when replacing scene object.isXRF = true // mark for easy deletion when replacing scene
xrf.scene.add(object) xrf.scene.add(object)
} }
xrf.hasNoMaterial = (mesh) => {
const hasTexture = mesh.material && mesh.material.map
const hasMaterialName = mesh.material && mesh.material.name.length > 0
return mesh.geometry && !hasMaterialName && !hasTexture
}

View file

@ -0,0 +1,9 @@
xrf.getCollisionMeshes = () => {
let meshes = []
xrf.scene.traverse( (n) => {
if( !n.userData.href && !n.userData.src && xrf.hasNoMaterial(n) ){
meshes.push(n)
}
})
return meshes
}

View file

@ -42,13 +42,28 @@ xrf.filter.sort = function(frag){
} }
xrf.filter.process = function(frag,scene,opts){ xrf.filter.process = function(frag,scene,opts){
const cleanupKey = (k) => k.replace(/[-\*\/]/g,'')
let firstFilter = frag.filters.length ? frag.filters[0].filter.get() : false
const hasName = (m,name,filter) => m.name == name const hasName = (m,name,filter) => m.name == name
const hasNameOrTag = (m,name_or_tag,filter) => hasName(m,name_or_tag) || const hasNameOrTag = (m,name_or_tag,filter) => hasName(m,name_or_tag) ||
String(m.userData['tag']).match( new RegExp("(^| )"+name_or_tag) ) String(m.userData['tag']).match( new RegExp("(^| )"+name_or_tag) )
const cleanupKey = (k) => k.replace(/[-\*\/]/g,'') // utility functions
const getOrCloneMaterial = (o) => {
let firstFilter = frag.filters.length ? frag.filters[0].filter.get() : false if( o.material ){
let showers = frag.filters.filter( (v) => v.filter.get().show === true ) if( o.material.isXRF ) return o.material
o.material = o.material.clone()
o.material.isXRF = true
return o.material
}
return {}
}
const setVisible = (n,visible,filter,processed) => {
if( processed && processed[n.uuid] ) return
getOrCloneMaterial(n).visible = visible
console.log(n.name+" => "+(visible?"show":"hide"))
if( filter.deep ) n.traverse( (m) => getOrCloneMaterial(m).visible = visible )
if( processed ) processed[n.uuid] == true
}
// spec 2: https://xrfragment.org/doc/RFC_XR_Macros.html#embedding-xr-content-using-src // spec 2: https://xrfragment.org/doc/RFC_XR_Macros.html#embedding-xr-content-using-src
// reparent scene based on objectname in case it matches a (non-negating) selector // reparent scene based on objectname in case it matches a (non-negating) selector
@ -63,56 +78,35 @@ xrf.filter.process = function(frag,scene,opts){
} }
} }
const setVisible = (n,visible,processed) => {
if( processed && processed[n.uuid] ) return
n.visible = visible
n.traverse( (n) => n.visible = visible )
// for hidden parents, clone material and set material to invisible
// otherwise n will not be rendered
if( visible ){
n.traverseAncestors( (parent) => {
if( !parent.visible ){
parent.visible = true
if( parent.material && !parent.material.isXRF ){
parent.material = parent.material.clone()
parent.material.visible = false
}
}
})
}
if( processed ) processed[n.uuid] == true
}
const insideSRC = (m) => {
let src = false
m.traverseAncestors( (n) => n.isSRC ? src = true : false )
return src
}
// then show/hide things based on secondary selectors // then show/hide things based on secondary selectors
// we don't use the XRWG (everything) because we process only the given (sub)scene
frag.filters.map( (v) => { frag.filters.map( (v) => {
const filter = v.filter.get() const filter = v.filter.get()
const name_or_tag = cleanupKey(v.fragment) const name_or_tag = cleanupKey(v.fragment)
let processed = {} let processed = {}
let extembeds = {}
// hide external objects temporarely
scene.traverse( (m) => { scene.traverse( (m) => {
// filter on value(expression) #foo=>3 e.g. *TODO* do this in XRWG if( m.isSRCExternal ){
if( filter.value && m.userData[filter.key] ){ m.traverse( (n) => (extembeds[ n.uuid ] = m) && (n.visible = false) )
if( filter.root && insideSRC(m) ) return
const visible = v.filter.testProperty(filter.key, m.userData[filter.key], filter.show === false )
setVisible(m,visible,processed)
return
} }
}) })
// include/exclude object(s) when id/tag matches (#foo or #-foo e.g.)
let matches = xrf.XRWG.match(name_or_tag) scene.traverseVisible( (m) => {
matches.map( (match) => { // filter on value(expression) #foo=>3 e.g. *TODO* do this in XRWG
match.nodes.map( (node) => { if( filter.value && m.userData[filter.key] ){
if( filter.root && insideSRC(node) ) return const visible = v.filter.testProperty(filter.key, m.userData[filter.key], filter.show === false )
setVisible(node,filter.show) setVisible(m,visible,filter,processed)
}) return
}
if( hasNameOrTag(m,name_or_tag,filter ) ){
setVisible(m,filter.show,filter)
}
}) })
// show external objects again
for ( let i in extembeds ) extembeds[i].visible = true
}) })
return xrf.filter return xrf.filter

View file

@ -1,6 +1,18 @@
xrf.frag.pos = function(v, opts){ xrf.frag.pos = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts let { frag, mesh, model, camera, scene, renderer, THREE} = opts
camera.position.x = v.x
camera.position.y = v.y
camera.position.z = v.z // spec: indirect coordinate using objectname: https://xrfragment.org/#navigating%203D
if( v.x == undefined ){
let obj = scene.getObjectByName(v.string)
if( !obj ) return
let pos = obj.position.clone()
obj.getWorldPosition(pos)
camera.position.copy(pos)
}else{
// spec: direct coordinate: https://xrfragment.org/#navigating%203D
camera.position.x = v.x
camera.position.y = v.y
camera.position.z = v.z
}
} }

View file

@ -18,7 +18,6 @@ xrf.frag.src.addModel = (model,url,frag,opts) => {
let scene = model.scene let scene = model.scene
xrf.frag.src.filterScene(scene,{...opts,frag}) // filter scene xrf.frag.src.filterScene(scene,{...opts,frag}) // filter scene
if( mesh.material ) mesh.material.visible = false // hide placeholder object if( mesh.material ) mesh.material.visible = false // hide placeholder object
mesh.traverse( (n) => n.isSRC = n.isXRF = true ) // mark everything isSRC & isXRF
//enableSourcePortation(scene) //enableSourcePortation(scene)
if( xrf.frag.src.renderAsPortal(mesh) ){ if( xrf.frag.src.renderAsPortal(mesh) ){
if( !opts.isLocal ) xrf.scene.add(scene) if( !opts.isLocal ) xrf.scene.add(scene)
@ -27,14 +26,15 @@ xrf.frag.src.addModel = (model,url,frag,opts) => {
xrf.frag.src.scale( scene, opts, url ) // scale scene xrf.frag.src.scale( scene, opts, url ) // scale scene
mesh.add(scene) mesh.add(scene)
} }
// flag everything isSRC & isXRF
mesh.traverse( (n) => { n.isSRC = n.isXRF = n[ opts.isLocal ? 'isSRCLocal' : 'isSRCExternal' ] = true })
xrf.emit('parseModel', {...opts, scene, model}) xrf.emit('parseModel', {...opts, scene, model})
} }
xrf.frag.src.renderAsPortal = (mesh) => { xrf.frag.src.renderAsPortal = (mesh) => {
const hasTexture = mesh.material && mesh.material.map // *TODO* should support better isFlat(mesh) check
const isPlane = mesh.geometry && mesh.geometry.attributes.uv && mesh.geometry.attributes.uv.count == 4 const isPlane = mesh.geometry && mesh.geometry.attributes.uv && mesh.geometry.attributes.uv.count == 4
const hasMaterialName = mesh.material && mesh.material.name.length > 0 return xrf.hasNoMaterial(mesh) && isPlane
return mesh.geometry && !hasMaterialName && !hasTexture && isPlane
} }
xrf.frag.src.enableSourcePortation = (src) => { xrf.frag.src.enableSourcePortation = (src) => {

View file

@ -11,6 +11,8 @@ let loadAudio = (mimetype) => function(url,opts){
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url) let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
let frag = xrf.URI.parse( url ) let frag = xrf.URI.parse( url )
return
/* WebAudio: setup context via THREEjs */ /* WebAudio: setup context via THREEjs */
if( !camera.listener ){ if( !camera.listener ){
camera.listener = new THREE.AudioListener(); camera.listener = new THREE.AudioListener();

View file

@ -85,8 +85,8 @@ xrf.portalNonEuclidian = function(opts){
let stencilObject = mesh.portal.stencilObject let stencilObject = mesh.portal.stencilObject
let newScale = mesh.scale let newScale = mesh.scale
let cameraDirection = mesh.portal.cameraDirection let cameraDirection = mesh.portal.cameraDirection
let cameraPosition = mesh.portal.cameraPosition let cameraPosition = mesh.portal.cameraPosition
let raycaster = mesh.portal.raycaster let raycaster = mesh.portal.raycaster
// init // init
if( !opts.isLocal ) stencilObject.visible = true if( !opts.isLocal ) stencilObject.visible = true
@ -149,7 +149,7 @@ xrf.portalNonEuclidian.setMaterial = function(mesh){
mesh.material.colorWrite = false; mesh.material.colorWrite = false;
mesh.material.stencilWrite = true; mesh.material.stencilWrite = true;
mesh.material.stencilRef = xrf.portalNonEuclidian.stencilRef; mesh.material.stencilRef = xrf.portalNonEuclidian.stencilRef;
mesh.renderOrder = xrf.portalNonEuclidian.stencilRef; mesh.renderOrder = 0;//xrf.portalNonEuclidian.stencilRef;
mesh.material.stencilFunc = THREE.AlwaysStencilFunc; mesh.material.stencilFunc = THREE.AlwaysStencilFunc;
mesh.material.stencilZPass = THREE.ReplaceStencilOp; mesh.material.stencilZPass = THREE.ReplaceStencilOp;
//mesh.material.stencilFail = THREE.ReplaceStencilOp; //mesh.material.stencilFail = THREE.ReplaceStencilOp;
@ -157,5 +157,10 @@ xrf.portalNonEuclidian.setMaterial = function(mesh){
return mesh return mesh
} }
xrf.addEventListener('parseModel',(opts) => {
const scene = opts.model.scene
scene.traverse( (n) => n.renderOrder = 10 ) // rendering everything *after* the stencil buffers
})
xrf.portalNonEuclidian.stencilRef = 1 xrf.portalNonEuclidian.stencilRef = 1

View file

@ -16,6 +16,7 @@ class Test {
static public function main():Void { static public function main():Void {
test( "url.json", Spec.load("src/spec/url.json") ); test( "url.json", Spec.load("src/spec/url.json") );
test( "pos.json", Spec.load("src/spec/pos.json") );
test( "t.json", Spec.load("src/spec/t.json") ); test( "t.json", Spec.load("src/spec/t.json") );
test( "filter.selectors.json", Spec.load("src/spec/filter.selectors.json") ); test( "filter.selectors.json", Spec.load("src/spec/filter.selectors.json") );
//test( Spec.load("src/spec/tmp.json") ); //test( Spec.load("src/spec/tmp.json") );
@ -46,6 +47,7 @@ class Test {
if( item.expect.fn == "equal.xy" ) valid = equalXY(res,item); if( item.expect.fn == "equal.xy" ) valid = equalXY(res,item);
if( item.expect.fn == "equal.xyz" ) valid = equalXYZ(res,item); if( item.expect.fn == "equal.xyz" ) valid = equalXYZ(res,item);
if( item.expect.fn == "testFilterRoot" ) valid = res.exists(item.expect.input[0]) && res.get(item.expect.input[0]).filter.get().root == item.expect.out; if( item.expect.fn == "testFilterRoot" ) valid = res.exists(item.expect.input[0]) && res.get(item.expect.input[0]).filter.get().root == item.expect.out;
if( item.expect.fn == "testFilterDeep" ) valid = res.exists(item.expect.input[0]) && res.get(item.expect.input[0]).filter.get().deep == item.expect.out;
var ok:String = valid ? "[ ] " : "[ ] "; var ok:String = valid ? "[ ] " : "[ ] ";
trace( ok + item.fn + ": '" + item.data + "'" + (item.label ? " (" + (item.label?item.label:item.expect.fn) +")" : "")); trace( ok + item.fn + ": '" + item.data + "'" + (item.label ? " (" + (item.label?item.label:item.expect.fn) +")" : ""));
if( !valid ) errors += 1; if( !valid ) errors += 1;

View file

@ -10,8 +10,6 @@
{"fn":"filter","data":"price=>2", "expect":{ "fn":"testProperty","input":["price","1"],"out":false}}, {"fn":"filter","data":"price=>2", "expect":{ "fn":"testProperty","input":["price","1"],"out":false}},
{"fn":"filter","data":"price=<2", "expect":{ "fn":"testProperty","input":["price","5"],"out":false}}, {"fn":"filter","data":"price=<2", "expect":{ "fn":"testProperty","input":["price","5"],"out":false}},
{"fn":"filter","data":"price=<2", "expect":{ "fn":"testProperty","input":["price","1"],"out":true}}, {"fn":"filter","data":"price=<2", "expect":{ "fn":"testProperty","input":["price","1"],"out":true}},
{"fn":"url","data":"#/foo", "expect":{ "fn":"testFilterRoot","input":["foo"],"out":true},"label":"foo should be root-only"}, {"fn":"url","data":"#foo*", "expect":{ "fn":"testFilterDeep","input":["foo"],"out":1},"label":"foo should be deep"},
{"fn":"url","data":"#/foo&foo","expect":{ "fn":"testFilterRoot","input":["foo"],"out":false},"label":"foo should recursively selected"}, {"fn":"url","data":"#foo**", "expect":{ "fn":"testFilterDeep","input":["foo"],"out":2},"label":"foo should be deep incl. embeds"}
{"fn":"url","data":"#/foo&foo&/bar", "expect":{ "fn":"testFilterRoot","input":["foo"],"out":false},"label":"bar should be root-only"},
{"fn":"url","data":"#-/foo", "expect":{ "fn":"testFilterRoot","input":["foo"],"out":true},"label":"foo should be root-only"}
] ]

6
src/spec/pos.json Normal file
View file

@ -0,0 +1,6 @@
[
{"fn":"url","data":"http://foo.com?foo=1#pos=1.2,2.2", "expect":{ "fn":"equal.string", "input":"pos","out":"1.2,2.2"},"label":"equal.string"},
{"fn":"url","data":"http://foo.com?foo=1#pos=1.2,2.2,3", "expect":{ "fn":"equal.xyz", "input":"pos","out":"1.2,2.2,3"},"label":"equal.xyz"},
{"fn":"url","data":"http://foo.com?foo=1#pos=1,2,3", "expect":{ "fn":"equal.xyz", "input":"pos","out":"1,2,3"},"label":"pos equal.xyz"},
{"fn":"url","data":"http://foo.com?foo=1#pos=world2", "expect":{ "fn":"equal.string", "input":"pos","out":"world2"},"label":"pos equal.xyz"}
]

View file

@ -1,6 +1,4 @@
[ [
{"fn":"url","data":"http://foo.com?foo=1#pos=1.2,2.2", "expect":{ "fn":"equal.xyz", "input":"pos","out":false},"label":"equal.xyz: should trigger incompatible type)"},
{"fn":"url","data":"http://foo.com?foo=1#pos=1.2,2.2,3", "expect":{ "fn":"equal.xyz", "input":"pos","out":"1.2,2.2,3"},"label":"equal.xyz"},
{"fn":"url","data":"http://foo.com?foo=1#mypredefinedview", "expect":{ "fn":"testPredefinedView", "input":"mypredefinedview","out":true},"label":"test predefined view executed"}, {"fn":"url","data":"http://foo.com?foo=1#mypredefinedview", "expect":{ "fn":"testPredefinedView", "input":"mypredefinedview","out":true},"label":"test predefined view executed"},
{"fn":"url","data":"http://foo.com?foo=1#mypredefinedview&another", "expect":{ "fn":"testPredefinedView", "input":"another","out":true},"label":"test predefined view executed (multiple)"}, {"fn":"url","data":"http://foo.com?foo=1#mypredefinedview&another", "expect":{ "fn":"testPredefinedView", "input":"another","out":true},"label":"test predefined view executed (multiple)"},
{"fn":"url","data":"http://foo.com?foo=1#mypredefinedview&another", "expect":{ "fn":"testPredefinedView", "input":"mypredefinedview","out":true},"label":"test predefined view executed (multiple)"}, {"fn":"url","data":"http://foo.com?foo=1#mypredefinedview&another", "expect":{ "fn":"testPredefinedView", "input":"mypredefinedview","out":true},"label":"test predefined view executed (multiple)"},

View file

@ -2,6 +2,7 @@
// Copyright (c) 2023 Leon van Kammen/NLNET // Copyright (c) 2023 Leon van Kammen/NLNET
package xrfragment; package xrfragment;
import xrfragment.XRF;
//return untyped __js__("window.location.search"); //return untyped __js__("window.location.search");
#if js #if js
@ -52,12 +53,6 @@ class Filter {
private var str:String = ""; private var str:String = "";
private var q:haxe.DynamicAccess<Dynamic> = {}; // 1. create an associative array/object to store filter-arguments as objects private var q:haxe.DynamicAccess<Dynamic> = {}; // 1. create an associative array/object to store filter-arguments as objects
private var isProp:EReg = ~/^.*=[><=]?/; // 1. detect object id's & properties `foo=1` and `foo` (reference regex= `~/^.*=[><=]?/` )
private var isExclude:EReg = ~/^-/; // 1. detect excluders like `-foo`,`-foo=1`,`-.foo`,`-/foo` (reference regex= `/^-/` )
private var isRoot:EReg = ~/^[-]?\//; // 1. detect root selectors like `/foo` (reference regex= `/^[-]?\//` )
private var isNumber:EReg = ~/^[0-9\.]+$/; // 1. detect number values like `foo=1` (reference regex= `/^[0-9\.]+$/` )
private var operators:EReg = ~/(^-)?(\/)?/; // 1. detect operators so you can easily strip keys (reference regex= `/(^-|\*$)/` )
private var isSelectorExclude:EReg = ~/^-/; // 1. detect exclude keys like `-foo` (reference regex= `/^-/` )
public function new(str:String){ public function new(str:String){
if( str != null ) this.parse(str); if( str != null ) this.parse(str);
@ -84,24 +79,23 @@ class Filter {
var filter:haxe.DynamicAccess<Dynamic> = {}; var filter:haxe.DynamicAccess<Dynamic> = {};
if( q.get(prefix+k) ) filter = q.get(prefix+k); if( q.get(prefix+k) ) filter = q.get(prefix+k);
if( isProp.match(str) ){ // 1. <b>WHEN</b></b> when a `=` key/value is detected: if( XRF.isProp.match(str) ){ // 1. <b>WHEN</b></b> when a `=` key/value is detected:
var oper:String = ""; var oper:String = "";
if( str.indexOf(">") != -1 ) oper = ">"; // 1. then scan for `>` operator if( str.indexOf(">") != -1 ) oper = ">"; // 1. then scan for `>` operator
if( str.indexOf("<") != -1 ) oper = "<"; // 1. then scan for `<` operator if( str.indexOf("<") != -1 ) oper = "<"; // 1. then scan for `<` operator
if( isExclude.match(k) ){ if( XRF.isExclude.match(k) ){
k = k.substr(1); // 1. then strip operators from key: convert "-foo" into "foo" k = k.substr(1); // 1. then strip operators from key: convert "-foo" into "foo"
} }
v = v.substr(oper.length); // 1. then strip operators from value: change value ">=foo" into "foo" v = v.substr(oper.length); // 1. then strip operators from value: change value ">=foo" into "foo"
if( oper.length == 0 ) oper = "="; // 1. when no operators detected, assume operator '=' if( oper.length == 0 ) oper = "="; // 1. when no operators detected, assume operator '='
var rule:haxe.DynamicAccess<Dynamic> = {}; var rule:haxe.DynamicAccess<Dynamic> = {};
if( isNumber.match(v) ) rule[ oper ] = Std.parseFloat(v); if( XRF.isNumber.match(v) ) rule[ oper ] = Std.parseFloat(v);
else rule[oper] = v; else rule[oper] = v;
q.set('expr',rule); q.set('expr',rule);
}else{ // 1. <b>ELSE </b> we are dealing with an object
q.set("root", isRoot.match(str) ? true : false ); // 1. and we set `root` to `true` or `false` (true=`/` root selector is present)
} }
q.set("show", isExclude.match(str) ? false : true ); // 1. therefore we we set `show` to `true` or `false` (false=excluder `-`) q.set("deep", XRF.isDeep.match(str) ? k.split("*").length-1 : 0 ); // 1. and we set `deep` to >0 or based on * occurences (true=`*` deep selector is present)
q.set("key", operators.replace(k,'') ); q.set("show", XRF.isExclude.match(str) ? false : true ); // 1. therefore we we set `show` to `true` or `false` (false=excluder `-`)
q.set("key", XRF.operators.replace(k,'') );
q.set("value",v); q.set("value",v);
} }
for( i in 0...token.length ) process( token[i] ); for( i in 0...token.length ) process( token[i] );
@ -153,8 +147,8 @@ class Filter {
if( Reflect.field(f,'!=') != null && testprop( Std.string(value) == Std.string(Reflect.field(f,'!='))) && exclude ) qualify += 1; if( Reflect.field(f,'!=') != null && testprop( Std.string(value) == Std.string(Reflect.field(f,'!='))) && exclude ) qualify += 1;
}else{ }else{
if( Reflect.field(f,'*') != null && testprop( Std.parseFloat(value) != null ) ) qualify += 1; if( Reflect.field(f,'*') != null && testprop( Std.parseFloat(value) != null ) ) qualify += 1;
if( Reflect.field(f,'>') != null && testprop( Std.parseFloat(value) > Std.parseFloat(Reflect.field(f,'>' )) ) ) qualify += 1; if( Reflect.field(f,'>') != null && testprop( Std.parseFloat(value) >= Std.parseFloat(Reflect.field(f,'>' )) ) ) qualify += 1;
if( Reflect.field(f,'<') != null && testprop( Std.parseFloat(value) < Std.parseFloat(Reflect.field(f,'<' )) ) ) qualify += 1; if( Reflect.field(f,'<') != null && testprop( Std.parseFloat(value) <= Std.parseFloat(Reflect.field(f,'<' )) ) ) qualify += 1;
if( Reflect.field(f,'=') != null && ( if( Reflect.field(f,'=') != null && (
testprop( value == Reflect.field(f,'=')) || testprop( value == Reflect.field(f,'=')) ||
testprop( Std.parseFloat(value) == Std.parseFloat(Reflect.field(f,'='))) testprop( Std.parseFloat(value) == Std.parseFloat(Reflect.field(f,'=')))

View file

@ -9,7 +9,6 @@ import xrfragment.XRF;
class Parser { class Parser {
public static var error:String = ""; public static var error:String = "";
public static var debug:Bool = false; public static var debug:Bool = false;
public static var keyClean:EReg = ~/(^-)?(\/)?/; // 1. detect - and / operators so you can easily strip keys (reference regex= ~/(^-)?(\/)?/; )
@:keep @:keep
public static function parse(key:String,value:String,store:haxe.DynamicAccess<Dynamic>,?index:Int):Bool { public static function parse(key:String,value:String,store:haxe.DynamicAccess<Dynamic>,?index:Int):Bool {
@ -22,7 +21,7 @@ class Parser {
Frag.set("tag", XRF.ASSET | XRF.T_STRING ); Frag.set("tag", XRF.ASSET | XRF.T_STRING );
// spatial category: query selector / object manipulation // spatial category: query selector / object manipulation
Frag.set("pos", XRF.PV_OVERRIDE | XRF.T_VECTOR3 | XRF.T_STRING_OBJ | XRF.METADATA | XRF.NAVIGATOR ); Frag.set("pos", XRF.PV_OVERRIDE | XRF.T_VECTOR3 | XRF.T_STRING | XRF.T_STRING_OBJ | XRF.METADATA | XRF.NAVIGATOR );
Frag.set("rot", XRF.QUERY_OPERATOR | XRF.PV_OVERRIDE | XRF.T_VECTOR3 | XRF.METADATA | XRF.NAVIGATOR ); Frag.set("rot", XRF.QUERY_OPERATOR | XRF.PV_OVERRIDE | XRF.T_VECTOR3 | XRF.METADATA | XRF.NAVIGATOR );
// category: animation // category: animation
@ -48,13 +47,15 @@ class Parser {
// 1. requirement: receive arguments: key (string), value (string), store (writable associative array/object) // 1. requirement: receive arguments: key (string), value (string), store (writable associative array/object)
var keyStripped:String = XRF.operators.replace( key, '' );
// dynamic fragments cases: predefined views & assign/binds // dynamic fragments cases: predefined views & assign/binds
var isPVDynamic:Bool = key.length > 0 && !Frag.exists(key); var isPVDynamic:Bool = key.length > 0 && !Frag.exists(key);
var isPVDefault:Bool = value.length == 0 && key.length > 0 && key == "#"; var isPVDefault:Bool = value.length == 0 && key.length > 0 && key == "#";
if( isPVDynamic ){ //|| isPVDefault ){ // 1. add keys without values to store as [predefined view](predefined_view) if( isPVDynamic ){ //|| isPVDefault ){ // 1. add keys without values to store as [predefined view](predefined_view)
var v:XRF = new XRF(key, XRF.PV_EXECUTE | XRF.NAVIGATOR, index ); var v:XRF = new XRF(key, XRF.PV_EXECUTE | XRF.NAVIGATOR, index );
v.validate(value); // ignore failures (empty values are allowed) v.validate(value); // ignore failures (empty values are allowed)
store.set( keyClean.replace(key,''), v ); store.set( keyStripped, v );
return true; return true;
} }
@ -65,12 +66,12 @@ class Parser {
trace(" fragment '"+key+"' has incompatible value ("+value+")");// 1. don't add to store if value-type is incorrect trace(" fragment '"+key+"' has incompatible value ("+value+")");// 1. don't add to store if value-type is incorrect
return false; return false;
} }
store.set( keyClean.replace(key,''), v); // 1. if valid, add to store store.set( keyStripped, v); // 1. if valid, add to store
if( debug ) trace(" "+key+": "+v.string); if( debug ) trace(" "+key+": "+v.string);
}else{ // 1. expose (but mark) non-offical fragments too }else{ // 1. expose (but mark) non-offical fragments too
if( Std.isOfType(value, String) ) v.guessType(v,value); if( Std.isOfType(value, String) ) v.guessType(v,value);
v.noXRF = true; v.noXRF = true;
store.set( keyClean.replace(key,'') ,v); store.set( keyStripped ,v);
} }
return true; return true;
} }

View file

@ -36,13 +36,18 @@ class XRF {
public static var T_STRING_OBJ_PROP:Int = 4194304; public static var T_STRING_OBJ_PROP:Int = 4194304;
// regexes // regexes
public static var isColor:EReg = ~/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; // 1. hex colors are detected using regex `/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/` public static var isColor:EReg = ~/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; // 1. hex colors are detected using regex `/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/`
public static var isInt:EReg = ~/^[-0-9]+$/; // 1. integers are detected using regex `/^[0-9]+$/` public static var isInt:EReg = ~/^[-0-9]+$/; // 1. integers are detected using regex `/^[0-9]+$/`
public static var isFloat:EReg = ~/^[-0-9]+\.[0-9]+$/; // 1. floats are detected using regex `/^[0-9]+\.[0-9]+$/` public static var isFloat:EReg = ~/^[-0-9]+\.[0-9]+$/; // 1. floats are detected using regex `/^[0-9]+\.[0-9]+$/`
public static var isVector:EReg = ~/([,]+|\w)/; // 1. vectors are detected using regex `/[,]/` (but can also be an string referring to an entity-ID in the asset) public static var isVector:EReg = ~/([,]+|\w)/; // 1. vectors are detected using regex `/[,]/` (but can also be an string referring to an entity-ID in the asset)
public static var isUrl:EReg = ~/(:\/\/)?\..*/; // 1. url/file */` public static var isUrl:EReg = ~/(:\/\/)?\..*/; // 1. url/file */`
public static var isUrlOrPretypedView:EReg = ~/(^#|:\/\/)?\..*/; // 1. url/file */` public static var isUrlOrPretypedView:EReg = ~/(^#|:\/\/)?\..*/; // 1. url/file */`
public static var isString:EReg = ~/.*/; // 1. anything else is string `/.*/` public static var isString:EReg = ~/.*/; // 1. anything else is string `/.*/`
public static var operators:EReg = ~/(^-|[\*]+)/; // 1. detect operators so you can easily strip keys (reference regex= `~/(^-)?(\/)?(\*)?/` )
public static var isProp:EReg = ~/^.*=[><=]?/; // 1. detect object id's & properties `foo=1` and `foo` (reference regex= `~/^.*=[><=]?/` )
public static var isExclude:EReg = ~/^-/; // 1. detect excluders like `-foo`,`-foo=1`,`-.foo`,`-/foo` (reference regex= `/^-/` )
public static var isDeep:EReg = ~/\*/; // 1. detect deep selectors like `foo*` (reference regex= `/\*$/` )
public static var isNumber:EReg = ~/^[0-9\.]+$/; // 1. detect number values like `foo=1` (reference regex= `/^[0-9\.]+$/` )
// value holder(s) // |------|------|--------|----------------------------------| // value holder(s) // |------|------|--------|----------------------------------|
public var fragment:String; public var fragment:String;
@ -83,7 +88,7 @@ class XRF {
// validate // validate
var ok:Bool = true; var ok:Bool = true;
if( !is(T_FLOAT) && is(T_VECTOR2) && !(Std.isOfType(x,Float) && Std.isOfType(y,Float)) ) ok = false; if( !is(T_FLOAT) && is(T_VECTOR2) && !(Std.isOfType(x,Float) && Std.isOfType(y,Float)) ) ok = false;
if( !is(T_VECTOR2) && is(T_VECTOR3) && !(Std.isOfType(x,Float) && Std.isOfType(y,Float) && Std.isOfType(z,Float)) ) ok = false; if( !(is(T_VECTOR2) || is(T_STRING)) && is(T_VECTOR3) && !(Std.isOfType(x,Float) && Std.isOfType(y,Float) && Std.isOfType(z,Float)) ) ok = false;
return ok; return ok;
} }

View file

@ -2,33 +2,40 @@
THREE = AFRAME.THREE THREE = AFRAME.THREE
createScene = (noadd) => { createScene = (noadd) => {
let obj = {a:{},b:{},c:{}} let obj = {a:{},b:{},c:{},d:{},extembed:{}}
for ( let i in obj ){ for ( let i in obj ){
obj[i] = new THREE.Object3D() obj[i] = new THREE.Object3D()
obj[i].name = i obj[i].name = i
obj[i].material = {visible:true, clone: () => ({visible:true}) } obj[i].material = {visible:true, clone: () => ({visible:true}) }
} }
let {a,b,c} = obj let {a,b,c,d,extembed} = obj
let scene = new THREE.Scene() let scene = xrf.scene = new THREE.Scene()
if( !noadd ){ if( !noadd ){
a.add(b) a.add(b)
b.add(c) b.add(c)
scene.add(a) scene.add(a)
extembed.add(d)
scene.add(extembed)
} }
b.userData.score = 2 b.userData.score = 2
b.userData.tag = "foo bar"
c.userData.tag = "flop flap"
a.userData.tag = "VR" a.userData.tag = "VR"
b.userData.tag = "foo hide"
c.userData.tag = "flop flap VR"
a.userData.price = 1 a.userData.price = 1
b.userData.price = 5 b.userData.price = 5
c.userData.price = 10 c.userData.price = 10
return {a,b,c,scene} b.isSRC = "local"
d.userData.tag = "VR"
extembed.isSRC = true
extembed.isSRCExternal = true
return {a,b,c,d,extembed,scene}
} }
filterScene = (URI) => { filterScene = (URI,opts) => {
opts = opts || {}
frag = xrf.URI.parse(URI) frag = xrf.URI.parse(URI)
var {a,b,c,scene} = createScene() var {a,b,c,d,extembed,scene} = createScene()
xrf.filter.scene({scene,frag}) xrf.filter.scene({...opts,scene,frag})
scene.visible = (objname, expected, checkMaterial) => { scene.visible = (objname, expected, checkMaterial) => {
let o = scene.getObjectByName(objname) let o = scene.getObjectByName(objname)
@ -42,69 +49,46 @@ filterScene = (URI) => {
} }
scn = filterScene("#b") scn = filterScene("#b")
test = () => !scn.visible("a") && scn.visible("b",true) && scn.visible("c",true) test = () => scn.visible("a",true,true) && scn.visible("b",true) && scn.visible("c",true)
console.assert( test(), {scn,reason:`objectname: #b `}) console.assert( test(), {scn,reason:`objectname: #b `})
scn = filterScene("#-b") scn = filterScene("#-b")
test = () => scn.visible("a",true) && scn.visible("b",false) && scn.visible("c",false) test = () => scn.visible("a",true,true) && scn.visible("b",false,true) && scn.visible("c",true) && scn.visible("c",true,true)
console.assert( test(), {scn,reason:`objectname: #-b `}) console.assert( test(), {scn,reason:`objectname: #-b`})
scn = filterScene("#a&-b") scn = filterScene("#-b*")
test = () => scn.visible("a",true) && scn.visible("b",false) && scn.visible("c",false) test = () => scn.visible("a",true,true) && scn.visible("b",false,true) && scn.visible("c",false,true)
console.assert( test(), {scn,reason:`objectname: #a&-b `}) console.assert( test(), {scn,reason:`objectname: #b*`})
scn = filterScene("#-b&b") scn = filterScene("#b",{reparent:true})
test = () => scn.visible("a",false) && scn.visible("b",true) && scn.visible("c",true)
console.assert( test(), {scn,reason:`objectname: #b (reparent scene)`})
scn = filterScene("#-b&b*")
test = () => scn.visible("a",true) && scn.visible("b",true) && scn.visible("c",true) test = () => scn.visible("a",true) && scn.visible("b",true) && scn.visible("c",true)
console.assert( test(), {scn,reason:`objectname: #-b&b `}) console.assert( test(), {scn,reason:`objectname: #-b&b `})
scn = filterScene("#-c") scn = filterScene("#-a&score*")
test = () => scn.visible("a",true) && scn.visible("b",true) && scn.visible("c",false) test = () => scn.visible("a",false,true) && scn.visible("b",true,true) && scn.visible("c",true,true)
console.assert( test(), {scn,reason:`objectname: #-c `})
scn = filterScene("#score")
test = () => scn.visible("a",true) && scn.visible("b",true) && scn.visible("c",true)
console.assert( test(), {scn,reason:`propertyfilter: #score `}) console.assert( test(), {scn,reason:`propertyfilter: #score `})
scn = filterScene("#score=>1") scn = filterScene("#-a&score*=2")
test = () => scn.visible("a",true) && scn.visible("b",true) && scn.visible("c",true) test = () => scn.visible("a",false,true) && scn.visible("b",true) && scn.visible("c",true)
console.assert( test(), {scn,reason:`propertyfilter: #score>=1`})
scn = filterScene("#score=2")
test = () => scn.visible("a",true) && scn.visible("b",true) && scn.visible("c",true)
console.assert( test(), {scn,reason:`propertyfilter: #score=2`}) console.assert( test(), {scn,reason:`propertyfilter: #score=2`})
scn = filterScene("#score=>3") scn = filterScene("#-price*&price=>5")
test = () => scn.visible("a",true) && scn.visible("b",false) && scn.visible("c",false) test = () => scn.visible("a",false,true) && scn.visible("b",true,true) && scn.visible("c",true,true)
console.assert( test(), {scn,reason:`propertyfilter: #score=>3`})
scn = filterScene("#-score=>1")
test = () => scn.visible("a",true) && scn.visible("b",false) && scn.visible("c",false)
console.assert( test(), {scn,reason:`propertyfilter: #-score=>1`})
scn = filterScene("#-score=>1&c")
test = () => scn.visible("a",true) && scn.visible("b",true) && scn.visible("b",false,true) && scn.visible("c",true)
console.assert( test(), {scn,reason:`propertyfilter: #-score=>1&c`})
scn = filterScene("#-foo")
test = () => scn.visible("a",true) && scn.visible("b",false) && scn.visible("b",false)
console.assert( test(), {scn,reason:`tagfilter: #-foo `})
scn = filterScene("#-c&flop")
test = () => scn.visible("a",true) && scn.visible("b",true) && scn.visible("c",true)
console.assert( test(), {scn,reason:`tagfilter: #-c&flop`})
scn = filterScene("#-b&-foo&bar&flop")
test = () => scn.visible("a",true) && scn.visible("b",true) && scn.visible("c",true)
console.assert( test(), {scn,reason:`tagfilter: #-b&-foo&bar&flop`})
scn = filterScene("#-b&-foo&bar&flop&-bar&flop")
test = () => scn.visible("a",true) && scn.visible("b",false,true) && scn.visible("c",true)
console.assert( test(), {scn,reason:`tagfilter: #-b&-foo&bar&flop&-bar&flop"`})
scn = filterScene("#-price&price=>5")
test = () => scn.visible("a",false,true) && scn.visible("b",true) && scn.visible("c",true)
console.assert( test(), {scn,reason:`tagfilter: #-price&price=>5"`}) console.assert( test(), {scn,reason:`tagfilter: #-price&price=>5"`})
scn = filterScene("#-/VR&b") scn = filterScene("#-hide*")
test = () => scn.visible("a",false,true) && scn.visible("b",true) && scn.visible("c",true) test = () => scn.visible("a",true,true) && scn.visible("b",false,true) && scn.visible("c",false,true)
console.assert( test(), {scn,reason:`tagfilter: #-/VR&b"`}) console.assert( test(), {scn,reason:`tagfilter: #-hide*"`})
scn = filterScene("#-VR")
test = () => scn.visible("a",false,true) && scn.visible("b",true,true) && scn.visible("c",false,true) && scn.visible("extembed",true,true) && scn.visible("d",true,true)
console.assert( test(), {scn,reason:`tagfilter: #-VR"`})
scn = filterScene("#-VR*")
test = () => scn.visible("a",false,true) && scn.visible("b",false,true) && scn.visible("c",false,true) && scn.visible("extembed",true,true) && scn.visible("d",true,true)
console.assert( test(), {scn,reason:`tagfilter: #-VR*"`})