diff --git a/doc/RFC_XR_Fragments.md b/doc/RFC_XR_Fragments.md
index a6519f0..d44d70a 100644
--- a/doc/RFC_XR_Fragments.md
+++ b/doc/RFC_XR_Fragments.md
@@ -279,7 +279,7 @@ For example, to render a portal with a preview-version of the scene, create an 3
| fragment | type | functionality |
|----------|--------|------------------------------|
-| #pos=0,0,0 | vector3 | (re)position camera |
+| #pos=0,0,0 | vector3 or string| (re)position camera based on coordinates directly, or indirectly using objectname (its worldposition) |
| #t=0,100 | vector3 | set playback speed, and (re)position looprange of scene-animation or `src`-mediacontent |
| #rot=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)
2. by default all objects are loaded into the instanced src (scene) object (but not shown yet)
-2. local `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. local `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. local `src` values should respect (negative) filters (`#-foo&price=>3`)
4. the instanced scene (from a `src` value) should be scaled accordingly to its placeholder object or scaled relatively based on the scale-property (of a geometry-less placeholder, an 'empty'-object in blender e.g.). For more info see Chapter Scaling.
5. external `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)
[» discussion](https://github.com/coderofsalvation/xrfragment/issues/1)
+## 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
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
+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 |
|----------|-------------------------------------------------------------------------------------------------------------------------------|
| `-` | hides object(s) (`#-myobject&-objects` 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.) |
-| `/` | reference to root-scene.
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)
`#-cube` hides both object `cube` in the root-scene AND 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 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)
* [Future of Text](https://futureoftext.org)
* [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
diff --git a/example/assets/index.glb b/example/assets/index.glb
index 72dae66..2f3cdbf 100644
Binary files a/example/assets/index.glb and b/example/assets/index.glb differ
diff --git a/src/3rd/js/aframe/index.js b/src/3rd/js/aframe/index.js
index 7eb62ed..4d3ed9d 100644
--- a/src/3rd/js/aframe/index.js
+++ b/src/3rd/js/aframe/index.js
@@ -26,7 +26,18 @@ window.AFRAME.registerComponent('xrf', {
})
if( !XRF.camera ) throw 'xrfragment: no camera detected, please declare 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) => {
if( opts.click){
let p = opts.promise()
diff --git a/src/3rd/js/three/hashbus.js b/src/3rd/js/three/hashbus.js
index 8bfe31f..1c9c158 100644
--- a/src/3rd/js/three/hashbus.js
+++ b/src/3rd/js/three/hashbus.js
@@ -48,7 +48,6 @@ pub.fragment = (k, opts ) => { // evaluate one fragment
pub.XRWG = (opts) => {
let {frag,scene,model,renderer} = opts
- console.dir(opts)
// if this query was triggered by an src-value, lets filter it
const isSRC = opts.embedded && opts.embedded.fragment == 'src'
diff --git a/src/3rd/js/three/index.js b/src/3rd/js/three/index.js
index e251960..a74c6a6 100644
--- a/src/3rd/js/three/index.js
+++ b/src/3rd/js/three/index.js
@@ -100,3 +100,9 @@ xrf.add = (object) => {
object.isXRF = true // mark for easy deletion when replacing scene
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
+}
diff --git a/src/3rd/js/three/util/collision.js b/src/3rd/js/three/util/collision.js
new file mode 100644
index 0000000..54895b8
--- /dev/null
+++ b/src/3rd/js/three/util/collision.js
@@ -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
+}
diff --git a/src/3rd/js/three/xrf/dynamic/filter.js b/src/3rd/js/three/xrf/dynamic/filter.js
index 4a976cc..510e9cf 100644
--- a/src/3rd/js/three/xrf/dynamic/filter.js
+++ b/src/3rd/js/three/xrf/dynamic/filter.js
@@ -42,13 +42,28 @@ xrf.filter.sort = function(frag){
}
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 hasNameOrTag = (m,name_or_tag,filter) => hasName(m,name_or_tag) ||
String(m.userData['tag']).match( new RegExp("(^| )"+name_or_tag) )
- const cleanupKey = (k) => k.replace(/[-\*\/]/g,'')
-
- let firstFilter = frag.filters.length ? frag.filters[0].filter.get() : false
- let showers = frag.filters.filter( (v) => v.filter.get().show === true )
+ // utility functions
+ const getOrCloneMaterial = (o) => {
+ if( o.material ){
+ 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
// 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
+ // we don't use the XRWG (everything) because we process only the given (sub)scene
frag.filters.map( (v) => {
const filter = v.filter.get()
const name_or_tag = cleanupKey(v.fragment)
let processed = {}
+ let extembeds = {}
+ // hide external objects temporarely
scene.traverse( (m) => {
- // filter on value(expression) #foo=>3 e.g. *TODO* do this in XRWG
- if( filter.value && m.userData[filter.key] ){
- 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
+ if( m.isSRCExternal ){
+ m.traverse( (n) => (extembeds[ n.uuid ] = m) && (n.visible = false) )
}
})
- // include/exclude object(s) when id/tag matches (#foo or #-foo e.g.)
- let matches = xrf.XRWG.match(name_or_tag)
- matches.map( (match) => {
- match.nodes.map( (node) => {
- if( filter.root && insideSRC(node) ) return
- setVisible(node,filter.show)
- })
+
+ scene.traverseVisible( (m) => {
+ // filter on value(expression) #foo=>3 e.g. *TODO* do this in XRWG
+ if( filter.value && m.userData[filter.key] ){
+ const visible = v.filter.testProperty(filter.key, m.userData[filter.key], filter.show === false )
+ 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
diff --git a/src/3rd/js/three/xrf/pos.js b/src/3rd/js/three/xrf/pos.js
index 4a00a78..0f0398e 100644
--- a/src/3rd/js/three/xrf/pos.js
+++ b/src/3rd/js/three/xrf/pos.js
@@ -1,6 +1,18 @@
xrf.frag.pos = function(v, 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
+ }
}
diff --git a/src/3rd/js/three/xrf/src.js b/src/3rd/js/three/xrf/src.js
index 641696b..a6ba327 100644
--- a/src/3rd/js/three/xrf/src.js
+++ b/src/3rd/js/three/xrf/src.js
@@ -18,7 +18,6 @@ xrf.frag.src.addModel = (model,url,frag,opts) => {
let scene = model.scene
xrf.frag.src.filterScene(scene,{...opts,frag}) // filter scene
if( mesh.material ) mesh.material.visible = false // hide placeholder object
- mesh.traverse( (n) => n.isSRC = n.isXRF = true ) // mark everything isSRC & isXRF
//enableSourcePortation(scene)
if( xrf.frag.src.renderAsPortal(mesh) ){
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
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.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 hasMaterialName = mesh.material && mesh.material.name.length > 0
- return mesh.geometry && !hasMaterialName && !hasTexture && isPlane
+ return xrf.hasNoMaterial(mesh) && isPlane
}
xrf.frag.src.enableSourcePortation = (src) => {
diff --git a/src/3rd/js/three/xrf/src/audio.js b/src/3rd/js/three/xrf/src/audio.js
index 7ee11b4..1f137a2 100644
--- a/src/3rd/js/three/xrf/src/audio.js
+++ b/src/3rd/js/three/xrf/src/audio.js
@@ -11,6 +11,8 @@ let loadAudio = (mimetype) => function(url,opts){
let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url)
let frag = xrf.URI.parse( url )
+ return
+
/* WebAudio: setup context via THREEjs */
if( !camera.listener ){
camera.listener = new THREE.AudioListener();
diff --git a/src/3rd/js/three/xrf/src/non-euclidian.js b/src/3rd/js/three/xrf/src/non-euclidian.js
index 1138cd9..99789d8 100644
--- a/src/3rd/js/three/xrf/src/non-euclidian.js
+++ b/src/3rd/js/three/xrf/src/non-euclidian.js
@@ -85,8 +85,8 @@ xrf.portalNonEuclidian = function(opts){
let stencilObject = mesh.portal.stencilObject
let newScale = mesh.scale
let cameraDirection = mesh.portal.cameraDirection
- let cameraPosition = mesh.portal.cameraPosition
- let raycaster = mesh.portal.raycaster
+ let cameraPosition = mesh.portal.cameraPosition
+ let raycaster = mesh.portal.raycaster
// init
if( !opts.isLocal ) stencilObject.visible = true
@@ -149,7 +149,7 @@ xrf.portalNonEuclidian.setMaterial = function(mesh){
mesh.material.colorWrite = false;
mesh.material.stencilWrite = true;
mesh.material.stencilRef = xrf.portalNonEuclidian.stencilRef;
- mesh.renderOrder = xrf.portalNonEuclidian.stencilRef;
+ mesh.renderOrder = 0;//xrf.portalNonEuclidian.stencilRef;
mesh.material.stencilFunc = THREE.AlwaysStencilFunc;
mesh.material.stencilZPass = THREE.ReplaceStencilOp;
//mesh.material.stencilFail = THREE.ReplaceStencilOp;
@@ -157,5 +157,10 @@ xrf.portalNonEuclidian.setMaterial = function(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
diff --git a/src/Test.hx b/src/Test.hx
index 7878d72..f77962f 100644
--- a/src/Test.hx
+++ b/src/Test.hx
@@ -16,6 +16,7 @@ class Test {
static public function main():Void {
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( "filter.selectors.json", Spec.load("src/spec/filter.selectors.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.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 == "testFilterDeep" ) valid = res.exists(item.expect.input[0]) && res.get(item.expect.input[0]).filter.get().deep == item.expect.out;
var ok:String = valid ? "[ ✔ ] " : "[ ❌] ";
trace( ok + item.fn + ": '" + item.data + "'" + (item.label ? " (" + (item.label?item.label:item.expect.fn) +")" : ""));
if( !valid ) errors += 1;
diff --git a/src/spec/filter.selectors.json b/src/spec/filter.selectors.json
index 5da1d62..7d6a253 100644
--- a/src/spec/filter.selectors.json
+++ b/src/spec/filter.selectors.json
@@ -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","5"],"out":false}},
{"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&foo","expect":{ "fn":"testFilterRoot","input":["foo"],"out":false},"label":"foo should recursively selected"},
- {"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"}
+ {"fn":"url","data":"#foo*", "expect":{ "fn":"testFilterDeep","input":["foo"],"out":1},"label":"foo should be deep"},
+ {"fn":"url","data":"#foo**", "expect":{ "fn":"testFilterDeep","input":["foo"],"out":2},"label":"foo should be deep incl. embeds"}
]
diff --git a/src/spec/pos.json b/src/spec/pos.json
new file mode 100644
index 0000000..bf533c6
--- /dev/null
+++ b/src/spec/pos.json
@@ -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"}
+]
diff --git a/src/spec/url.json b/src/spec/url.json
index 49957eb..95672b3 100644
--- a/src/spec/url.json
+++ b/src/spec/url.json
@@ -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&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)"},
diff --git a/src/xrfragment/Filter.hx b/src/xrfragment/Filter.hx
index 3810ba4..467680f 100644
--- a/src/xrfragment/Filter.hx
+++ b/src/xrfragment/Filter.hx
@@ -2,6 +2,7 @@
// Copyright (c) 2023 Leon van Kammen/NLNET
package xrfragment;
+import xrfragment.XRF;
//return untyped __js__("window.location.search");
#if js
@@ -52,12 +53,6 @@ class Filter {
private var str:String = "";
private var q:haxe.DynamicAccess = {}; // 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){
if( str != null ) this.parse(str);
@@ -84,24 +79,23 @@ class Filter {
var filter:haxe.DynamicAccess = {};
if( q.get(prefix+k) ) filter = q.get(prefix+k);
- if( isProp.match(str) ){ // 1. WHEN when a `=` key/value is detected:
+ if( XRF.isProp.match(str) ){ // 1. WHEN when a `=` key/value is detected:
var oper:String = "";
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"
}
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 '='
var rule:haxe.DynamicAccess = {};
- if( isNumber.match(v) ) rule[ oper ] = Std.parseFloat(v);
+ if( XRF.isNumber.match(v) ) rule[ oper ] = Std.parseFloat(v);
else rule[oper] = v;
q.set('expr',rule);
- }else{ // 1. ELSE 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("key", operators.replace(k,'') );
+ 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("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);
}
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;
}else{
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 && (
testprop( value == Reflect.field(f,'=')) ||
testprop( Std.parseFloat(value) == Std.parseFloat(Reflect.field(f,'=')))
diff --git a/src/xrfragment/Parser.hx b/src/xrfragment/Parser.hx
index c89e496..386bb67 100644
--- a/src/xrfragment/Parser.hx
+++ b/src/xrfragment/Parser.hx
@@ -9,7 +9,6 @@ import xrfragment.XRF;
class Parser {
public static var error:String = "";
public static var debug:Bool = false;
- public static var keyClean:EReg = ~/(^-)?(\/)?/; // 1. detect - and / operators so you can easily strip keys (reference regex= ~/(^-)?(\/)?/; )
@:keep
public static function parse(key:String,value:String,store:haxe.DynamicAccess,?index:Int):Bool {
@@ -22,7 +21,7 @@ class Parser {
Frag.set("tag", XRF.ASSET | XRF.T_STRING );
// 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 );
// category: animation
@@ -48,13 +47,15 @@ class Parser {
// 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
var isPVDynamic:Bool = key.length > 0 && !Frag.exists(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)
var v:XRF = new XRF(key, XRF.PV_EXECUTE | XRF.NAVIGATOR, index );
v.validate(value); // ignore failures (empty values are allowed)
- store.set( keyClean.replace(key,''), v );
+ store.set( keyStripped, v );
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
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);
}else{ // 1. expose (but mark) non-offical fragments too
if( Std.isOfType(value, String) ) v.guessType(v,value);
v.noXRF = true;
- store.set( keyClean.replace(key,'') ,v);
+ store.set( keyStripped ,v);
}
return true;
}
diff --git a/src/xrfragment/XRF.hx b/src/xrfragment/XRF.hx
index ab536b5..1c24914 100644
--- a/src/xrfragment/XRF.hx
+++ b/src/xrfragment/XRF.hx
@@ -36,13 +36,18 @@ class XRF {
public static var T_STRING_OBJ_PROP:Int = 4194304;
// 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 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 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 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 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 isUrl: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) // |------|------|--------|----------------------------------|
public var fragment:String;
@@ -83,7 +88,7 @@ class XRF {
// validate
var ok:Bool = true;
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;
}
diff --git a/test/aframe/filter.js b/test/aframe/filter.js
index d951857..a1f515a 100644
--- a/test/aframe/filter.js
+++ b/test/aframe/filter.js
@@ -2,33 +2,40 @@
THREE = AFRAME.THREE
createScene = (noadd) => {
- let obj = {a:{},b:{},c:{}}
+ let obj = {a:{},b:{},c:{},d:{},extembed:{}}
for ( let i in obj ){
obj[i] = new THREE.Object3D()
obj[i].name = i
obj[i].material = {visible:true, clone: () => ({visible:true}) }
}
- let {a,b,c} = obj
- let scene = new THREE.Scene()
+ let {a,b,c,d,extembed} = obj
+ let scene = xrf.scene = new THREE.Scene()
if( !noadd ){
a.add(b)
b.add(c)
scene.add(a)
+ extembed.add(d)
+ scene.add(extembed)
}
b.userData.score = 2
- b.userData.tag = "foo bar"
- c.userData.tag = "flop flap"
a.userData.tag = "VR"
+ b.userData.tag = "foo hide"
+ c.userData.tag = "flop flap VR"
a.userData.price = 1
b.userData.price = 5
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)
- var {a,b,c,scene} = createScene()
- xrf.filter.scene({scene,frag})
+ var {a,b,c,d,extembed,scene} = createScene()
+ xrf.filter.scene({...opts,scene,frag})
scene.visible = (objname, expected, checkMaterial) => {
let o = scene.getObjectByName(objname)
@@ -42,69 +49,46 @@ filterScene = (URI) => {
}
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 `})
-scn = filterScene("#-b")
-test = () => scn.visible("a",true) && scn.visible("b",false) && scn.visible("c",false)
-console.assert( test(), {scn,reason:`objectname: #-b `})
+scn = filterScene("#-b")
+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`})
-scn = filterScene("#a&-b")
-test = () => scn.visible("a",true) && scn.visible("b",false) && scn.visible("c",false)
-console.assert( test(), {scn,reason:`objectname: #a&-b `})
+scn = filterScene("#-b*")
+test = () => scn.visible("a",true,true) && scn.visible("b",false,true) && scn.visible("c",false,true)
+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)
console.assert( test(), {scn,reason:`objectname: #-b&b `})
-scn = filterScene("#-c")
-test = () => scn.visible("a",true) && scn.visible("b",true) && scn.visible("c",false)
-console.assert( test(), {scn,reason:`objectname: #-c `})
-
-scn = filterScene("#score")
-test = () => scn.visible("a",true) && scn.visible("b",true) && scn.visible("c",true)
+scn = filterScene("#-a&score*")
+test = () => scn.visible("a",false,true) && scn.visible("b",true,true) && scn.visible("c",true,true)
console.assert( test(), {scn,reason:`propertyfilter: #score `})
-scn = filterScene("#score=>1")
-test = () => scn.visible("a",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)
+scn = filterScene("#-a&score*=2")
+test = () => scn.visible("a",false,true) && scn.visible("b",true) && scn.visible("c",true)
console.assert( test(), {scn,reason:`propertyfilter: #score=2`})
-scn = filterScene("#score=>3")
-test = () => scn.visible("a",true) && scn.visible("b",false) && scn.visible("c",false)
-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)
+scn = filterScene("#-price*&price=>5")
+test = () => scn.visible("a",false,true) && scn.visible("b",true,true) && scn.visible("c",true,true)
console.assert( test(), {scn,reason:`tagfilter: #-price&price=>5"`})
-scn = filterScene("#-/VR&b")
-test = () => scn.visible("a",false,true) && scn.visible("b",true) && scn.visible("c",true)
-console.assert( test(), {scn,reason:`tagfilter: #-/VR&b"`})
+scn = filterScene("#-hide*")
+test = () => scn.visible("a",true,true) && scn.visible("b",false,true) && scn.visible("c",false,true)
+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*"`})
+