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*"`}) +