From 7162621efe0cbfed46ac1266fcb66d261c496cf8 Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Fri, 17 Nov 2023 15:30:18 +0100 Subject: [PATCH] stable filters: updated RFC & tests & index.glb --- doc/RFC_XR_Fragments.md | 109 +++++++++++----------- example/assets/index.glb | Bin 1700128 -> 1727452 bytes src/3rd/js/three/xrf/dynamic/filter.js | 53 +++++++---- src/3rd/js/three/xrf/src.js | 21 +---- src/3rd/js/three/xrf/src/non-euclidian.js | 2 + src/xrfragment/Filter.hx | 18 ++-- 6 files changed, 99 insertions(+), 104 deletions(-) diff --git a/doc/RFC_XR_Fragments.md b/doc/RFC_XR_Fragments.md index f6619d6..3e7f3fb 100644 --- a/doc/RFC_XR_Fragments.md +++ b/doc/RFC_XR_Fragments.md @@ -94,7 +94,7 @@ value: draft-XRFRAGMENTS-leonvankammen-00 .# Abstract This draft is a specification for 4D URLs & [hypermediatic](https://github.com/coderofsalvation/hypermediatic) navigation, which links together space, time & text together, for hypermedia browsers with- or without a network-connection.
-The specification promotes spatial addressibility, sharing, navigation, query-ing and databinding objects for (XR) Browsers.
+The specification promotes spatial addressibility, sharing, navigation, filtering and databinding objects for (XR) Browsers.
XR Fragments allows us to better use existing metadata inside 3D scene(files), by connecting it to proven technologies like [URI Fragments](https://en.wikipedia.org/wiki/URI_fragment). > Almost every idea in this document is demonstrated at [https://xrfragment.org](https://xrfragment.org) @@ -134,7 +134,7 @@ Instead of combining them (in a game-editor e.g.), XR Fragments **integrates all | href metadata | triggers predefined view | Media fragments | | href metadata | triggers camera/scene/object/projections | n/a | | href metadata | draws visible connection(s) for XRWG 'tag' | n/a | -| href metadata | queries certain (in)visible objects | n/a | +| href metadata | filters certain (in)visible objects | n/a | > XR Fragments does not look at XR (or the web) thru the lens of HTML.
But approaches things from a higherlevel feedbackloop/hypermedia browser-perspective: @@ -146,7 +146,7 @@ Instead of combining them (in a game-editor e.g.), XR Fragments **integrates all │ 2D URL: ://library.com /document ?search #chapter │ │ │ │ 4D URL: ://park.com /4Dscene.fbx ──> ?misc ──> #view ───> hashbus │ - │ │ #query │ │ + │ │ #filter │ │ │ │ #tag │ │ │ │ #material │ │ │ │ #animation │ │ @@ -187,7 +187,7 @@ sub-delims = "," / "=" | Demo | Explanation | |-------------------------------|---------------------------------| | `pos=1,2,3` | vector/coordinate argument e.g. | -| `pos=1,2,3&rot=0,90,0&q=foo` | combinators | +| `pos=1,2,3&rot=0,90,0&foo` | combinators | > this is already implemented in all browsers @@ -198,7 +198,6 @@ sub-delims = "," / "=" | `#pos` | vector3 | `#pos=0.5,0,0` | positions camera (or XR floor) to xyz-coord 0.5,0,0, | | `#rot` | vector3 | `#rot=0,90,0` | rotates camera to xyz-coord 0.5,0,0 | | `#t` | timevector | `#t=2,2000,1` | play animation-loop range between frame 2 and 2000 at (normal) speed 1 | -| `#q` | vector3 | `#q=-sky -tag:hide`| queries scene-graph (and removes object with name `cube` or `tag: hide`) | ## List of metadata for 3D nodes @@ -206,7 +205,7 @@ sub-delims = "," / "=" |--------------|----------|------------------------|---------------------|----------------------------------------| | `href` | string | `"href": "b.gltf"` | XR teleport | custom property in 3D fileformats | | `src` | string | `"src": "#cube"` | XR embed / teleport | custom property in 3D fileformats | -| `tag` | string | `"tag": "cubes geo"` | tag object (for query-use / XRWG highlighting) | custom property in 3D fileformats | +| `tag` | string | `"tag": "cubes geo"` | tag object (for filter-use / XRWG highlighting) | custom property in 3D fileformats | > Supported popular compatible 3D fileformats: `.gltf`, `.obj`, `.fbx`, `.usdz`, `.json` (THREE.js), `.dae` and so on. @@ -326,27 +325,27 @@ The URL-processing-flow for hypermedia browsers goes like this: 5. IF a `#cube` matches anything else in the XR Word Graph (XRWG) draw wires to them (text or related objects). -# Embedding XR content (src-instancing) +# Embedding XR content using src `src` is the 3D version of the iframe.
It instances content (in objects) in the current scene/asset. | fragment | type | example value | |----------|------|---------------| -|`src`| string (uri, hashtag/query) | `#cube`
`#sometag`
#q=-ball_inside_cube`
`#q=-/sky -rain`
`#q=-.language .english`
`#q=price:>2 price:<5`
`https://linux.org/penguin.png`
`https://linux.world/distrowatch.gltf#t=1,100`
`linuxapp://conference/nixworkshop/apply.gltf#q=flyer`
`androidapp://page1?tutorial#pos=0,0,1&t1,100`
`foo.mp3#0,0,0`| +|`src`| string (uri, hashtag/filter) | `#cube`
`#sometag`
#cube&-ball_inside_cube`
`#-sky&-rain`
`#-language&english`
`#price=>5`
`https://linux.org/penguin.png`
`https://linux.world/distrowatch.gltf#t=1,100`
`linuxapp://conference/nixworkshop/apply.gltf#-cta&cta_apply`
`androidapp://page1?tutorial#pos=0,0,1&t1,100`
`foo.mp3#0,0,0`| -Here's an ascii representation of a 3D scene-graph with 3D objects `◻` which embeds remote & local 3D objects `◻` with/out using queries: +Here's an ascii representation of a 3D scene-graph with 3D objects `◻` which embeds remote & local 3D objects `◻` with/out using filters: ``` +────────────────────────────────────────────────────────+ +─────────────────────────+ │ │ │ │ │ index.gltf │ │ ocean.com/aquarium.fbx │ - │ │ │ │ │ │ + │ │ │ │ ├ room │ │ ├── ◻ canvas │ │ └── ◻ fishbowl │ │ │ └ src: painting.png │ │ ├─ ◻ bass │ │ │ │ │ └─ ◻ tuna │ │ ├── ◻ aquariumcube │ │ │ - │ │ └ src: ://rescue.com/fish.gltf#bass%20tuna │ +─────────────────────────+ + │ │ └ src: ://rescue.com/fish.gltf#fishbowl │ +─────────────────────────+ │ │ │ │ ├── ◻ bedroom │ │ │ └ src: #canvas │ @@ -358,31 +357,32 @@ Here's an ascii representation of a 3D scene-graph with 3D objects `◻` which e ``` An XR Fragment-compatible browser viewing this scene, lazy-loads and projects `painting.png` onto the (plane) object called `canvas` (which is copy-instanced in the bed and livingroom).
-Also, after lazy-loading `ocean.com/aquarium.gltf`, only the queried objects `bass` and `tuna` will be instanced inside `aquariumcube`.
+Also, after lazy-loading `ocean.com/aquarium.gltf`, only the queried objects `fishbowl` (and `bass` and `tuna`) will be instanced inside `aquariumcube`.
Resizing will be happen accordingly to its placeholder object `aquariumcube`, see chapter Scaling.
-> Instead of cherrypicking objects with `#bass&tuna` thru `src`, queries can be used to import the whole scene (and filter out certain objects). See next chapter below. +> Instead of cherrypicking a rootobject `#fishbowl` with `src`, additional filters can be used to include/exclude certain objects. See next chapter on filtering below. **Specification**: -1. local/remote content is instanced by the `src` (query) value (and attaches it to the placeholder mesh containing the `src` property) -1. local `src` values (URL **starting** with `#`, like `#cube&foo`) means **only** the mentioned objectnames will be copied to the instanced scene (from the current scene) while preserving their names (to support recursive selectors). [(example code)](https://github.com/coderofsalvation/xrfragment/blob/main/src/3rd/js/three/xrf/src.js) -1. local `src` values indicating a query (`#q=`), means that all included objects (from the current scene) will be copied to the instanced scene (before applying the query) while preserving their names (to support recursive selectors). [(example code)](https://github.com/coderofsalvation/xrfragment/blob/main/src/3rd/js/three/xrf/src.js) -1. 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. -1. 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: -1. `src` values should make its placeholder object invisible, and only flush its children when the resolved content can succesfully be retrieved (see [broken links](#links)) -1. external `src` values should respect the fallback link mechanism (see [broken links](#broken-links) -1. when the placeholder object is a 2D plane, but the mimetype is 3D, then render the spatial content on that plane via a stencil buffer. -1. src-values are non-recursive: when linking to an external object (`src: foo.fbx#bar`), then `src`-metadata on object `bar` should be ignored. -1. clicking on external `src`-values always allow sourceportation: teleporting to the origin URI to which the object belongs. -1. when only one object was cherrypicked (`#cube` e.g.), set its position to `0,0,0` -1. equirectangular detection: when the width of an image is twice the height (aspect 2:1), an equirectangular projection is assumed. -1. when the enduser clicks an href with `#t=1,0,0` (play) will be applied to all src mediacontent with a timeline (mp4/mp3 e.g.) +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 +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: +6. `src` values should make its placeholder object invisible, and only flush its children when the resolved content can succesfully be retrieved (see [broken links](#links)) +7. external `src` values should respect the fallback link mechanism (see [broken links](#broken-links) +8. when the placeholder object is a 2D plane, but the mimetype is 3D, then render the spatial content on that plane via a stencil buffer. +9. src-values are non-recursive: when linking to an external object (`src: foo.fbx#bar`), then `src`-metadata on object `bar` should be ignored. +10. clicking on external `src`-values always allow sourceportation: teleporting to the origin URI to which the object belongs. +11. when only one object was cherrypicked (`#cube` e.g.), set its position to `0,0,0` +12. equirectangular detection: when the width of an image is twice the height (aspect 2:1), an equirectangular projection is assumed. +13. when the enduser clicks an href with `#t=1,0,0` (play) will be applied to all src mediacontent with a timeline (mp4/mp3 e.g.) * `model/gltf+json` * `image/png` * `image/jpg` -* `text/plain;charset=utf-8;bib=^@` +* `text/plain;charset=utf-8` [» example implementation](https://github.com/coderofsalvation/xrfragment/blob/main/src/3rd/js/three/xrf/src.js)
[» example 3D asset](https://github.com/coderofsalvation/xrfragment/blob/main/example/assets/src.gltf#L192)
@@ -484,58 +484,53 @@ To play global audio/video items: > NOTE: hardcoded framestart/framestop uses sampleRate/fps of embedded audio/video, otherwise the global fps applies. For more info see [[#t|t]]. -# XR Fragment queries +# XR Fragment filters Include, exclude, hide/shows objects using space-separated strings: | example | outcome | |----------------------------------|------------------------------------------------------------------------------------| -| `#q=-sky` | show everything except object named `sky` | -| `#q=-tag:language tag:english` | hide everything with tag `language`, but show all tag `english` objects | -| `#q=price:>2 price:<5` | of all objects with property `price`, show only objects with value between 2 and 5 | +| `#-sky` | show everything except object named `sky` | +| `#-language&english` | hide everything with tag `language`, but show all tag `english` objects | +| `#-price&price=>10` | hide all objects with property `price`, then only show object with price above 10 | It's simple but powerful syntax which allows filtering the scene using searchengine prompt-style feeling: -1. queries are a way to traverse a scene, and filter objects based on their tag- or property-values. -1. words like `german` match tag-metadata of 3D objects like `"tag":"german"` -1. words like `german` match (XR Text) objects with (Bib(s)TeX) tags like `#KarlHeinz@german` or `@german{KarlHeinz, ...` e.g. +1. filters are a way to traverse a scene, and filter objects based on their name, tag- or property-values. -* see [an (outdated) example video here](https://coderofsalvation.github.io/xrfragment.media/queries.mp4) +* see [an (outdated) example video here](https://coderofsalvation.github.io/xrfragment.media/queries.mp4) which used a dedicated `q=` variable (now deprecated) ## including/excluding | operator | info | |----------|-------------------------------------------------------------------------------------------------------------------------------| -| `-` | removes/hides object(s) | -| `:` | indicates an object-embedded custom property key/value | -| `>` `<` | compare float or int number | -| `/` | reference to root-scene.
Useful in case of (preventing) showing/hiding objects in nested scenes (instanced by `src`) (*) | +| `-` | 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`) (*) | > \* = `#q=-/cube` hides object `cube` only in the root-scene (not nested `cube` objects)
`#q=-cube` hides both object `cube` in the root-scene AND nested `skybox` objects | [» 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/query.gltf#L192) +[» example 3D asset](https://github.com/coderofsalvation/xrfragment/blob/main/example/assets/filter.gltf#L192) [» discussion](https://github.com/coderofsalvation/xrfragment/issues/3) -## Query Parser +## Filter Parser -Here's how to write a query parser: +Here's how to write a filter parser: -1. create an associative array/object to store query-arguments as objects -1. detect object id's & properties `foo:1` and `foo` (reference regex: `/^.*:[><=!]?/` ) -1. detect excluders like `-foo`,`-foo:1`,`-.foo`,`-/foo` (reference regex: `/^-/` ) -1. detect root selectors like `/foo` (reference regex: `/^[-]?\//` ) -1. detect number values like `foo:1` (reference regex: `/^[0-9\.]+$/` ) -1. for every query token split string on `:` -1. create an empty array `rules` -1. then strip key-operator: convert "-foo" into "foo" -1. add operator and value to rule-array -1. therefore we we set `id` to `true` or `false` (false=excluder `-`) +1. create an associative array/object to store filter-arguments as objects +1. detect object id's & properties `foo=1` and `foo` (reference regex= `~/^.*=[><=]?/` ) +1. detect excluders like `-foo`,`-foo=1`,`-.foo`,`-/foo` (reference regex= `/^-/` ) +1. detect root selectors like `/foo` (reference regex= `/^[-]?\//` ) +1. detect number values like `foo=1` (reference regex= `/^[0-9\.]+$/` ) +1. detect operators so you can easily strip keys (reference regex= `/(^-|\*$)/` ) +1. detect exclude keys like `-foo` (reference regex= `/^-/` ) +1. for every filter token split string on `=` 1. and we set `root` to `true` or `false` (true=`/` root selector is present) -1. we convert key '/foo' into 'foo' -1. finally we add the key/value to the store like `store.foo = {id:false,root:true}` e.g. +1. therefore we we set `show` to `true` or `false` (false=excluder `-`) -> An example query-parser (which compiles to many languages) can be [found here](https://github.com/coderofsalvation/xrfragment/blob/main/src/xrfragment/Query.hx) +> An example filter-parser (which compiles to many languages) can be [found here](https://github.com/coderofsalvation/xrfragment/blob/main/src/xrfragment/Filter.hx) # Visible links @@ -893,7 +888,7 @@ Consider 3D scenes linking to eachother using these `href` values: * `href: university.edu/projects.gltf#math` These links would all show visible links to math-tagged objects in the scene.
-To filter out non-related objects one could take it a step further using queries: +To filter out non-related objects one could take it a step further using filters: * `href: schoolA.edu/projects.gltf#math&q=-topics math` * `href: schoolB.edu/projects.gltf#math&q=-courses math` @@ -949,7 +944,7 @@ This document has no IANA actions. |placeholder object | a 3D object which with src-metadata (which will be replaced by the src-data.) | |src | (HTML-piggybacked) metadata of a 3D object which instances content | |href | (HTML-piggybacked) metadata of a 3D object which links to content | -|query | an URI Fragment-operator which queries object(s) from a scene like `#q=cube` | +|filter | URI Fragment(s) which show/hide object(s) in a scene based on name/tag/property (`#q=cube&-price=>3`) | |visual-meta | [visual-meta](https://visual.meta.info) data appended to text/books/papers which is indirectly visible/editable in XR. | |requestless metadata | metadata which never spawns new requests (unlike RDF/HTML, which can cause framerate-dropping, hence not used a lot in games) | |FPS | frames per second in spatial experiences (games,VR,AR e.g.), should be as high as possible | diff --git a/example/assets/index.glb b/example/assets/index.glb index 0a7c03d7ac4f5e6fcc310b3452be0511a4da5f45..e45c67daae51a89c54966afdd7cc11f1b168d76e 100644 GIT binary patch delta 25719 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 delta 9345 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 m.name == name - const hasNameOrTag = (m,name_or_tag,filter) => hasName(m,name_or_tag) || m.userData[filter.key] + 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[0].filter.get() let showers = frag.filters.filter( (v) => v.filter.get().show === true ) - // reparent scene based on object in case it matches a primary (non-negating) selector + // 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 if( !firstFilter.value && firstFilter.show === true ){ let obj scene.traverse( (n) => hasName(n, firstFilter.key,firstFilter) && (obj = n) ) @@ -44,34 +50,43 @@ 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 + } + // then show/hide things based on secondary selectors frag.filters.map( (v) => { const filter = v.filter.get() const name_or_tag = cleanupKey(v.fragment) - let seen = {} - - const setVisibleUnseen = (m,visible) => { - if( seen[m.uuid] ) return - m.visible = visible - seen[ m.uuid ] = true - } + let processed = {} scene.traverse( (m) => { - // filter on value(expression) #foo=>3 e.g. if( filter.value && m.userData[filter.key] ){ const visible = v.filter.testProperty(filter.key, m.userData[filter.key], filter.show === false ) - setVisibleUnseen(m,visible) - if( filter.deep ){ - m.traverse( (n) => setVisibleUnseen(n,visible) ) - } + setVisible(m,visible,processed) return } - // include/exclude object(s) when id/tag matches (#foo or #-foo e.g.) if( hasNameOrTag(m,name_or_tag,filter) ){ - m.visible = filter.show - if( filter.deep ) m.traverse( (n) => n.visible = m.visible ) + setVisible(m,filter.show) } }) }) diff --git a/src/3rd/js/three/xrf/src.js b/src/3rd/js/three/xrf/src.js index f921b08..a2b1625 100644 --- a/src/3rd/js/three/xrf/src.js +++ b/src/3rd/js/three/xrf/src.js @@ -22,8 +22,6 @@ xrf.frag.src = function(v, opts){ let scene = model.scene xrf.frag.src.filterScene(scene,{...opts,frag}) xrf.frag.src.scale( scene, opts, url ) - xrf.frag.src.eval( scene, opts, url ) - // allow 't'-fragment to setup separate animmixer //enableSourcePortation(scene) mesh.add(model.scene) mesh.traverse( (n) => n.isSRC = n.isXRF = true ) // mark everything SRC @@ -32,6 +30,7 @@ xrf.frag.src = function(v, opts){ } const enableSourcePortation = (src) => { + // show sourceportation clickable plane if( vfrag.href || v.string[0] == '#' ) return let scale = new THREE.Vector3() let size = new THREE.Vector3() @@ -45,7 +44,6 @@ xrf.frag.src = function(v, opts){ mat.opacity = 0 const cube = new THREE.Mesh( geo, mat ) console.log("todo: sourceportate") - //mesh.add(cube) } const externalSRC = (url,frag,src) => { @@ -75,20 +73,6 @@ xrf.frag.src = function(v, opts){ }else externalSRC(url,vfrag) // external file } -xrf.frag.src.eval = function(scene, opts, url){ - let { mesh, model, camera, renderer, THREE, hashbus} = opts - if( url ){ - //let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url) - //let frag = xrfragment.URI.parse(url) - //// scale URI XR Fragments (queries) inside src-value - //for( var i in frag ){ - // hashbus.pub.fragment(i, Object.assign(opts,{frag, model:{scene},scene})) - //} - //hashbus.pub( '#', {scene} ) // execute the default projection '#' (if exist) - //hashbus.pub( url, {scene} ) // and eval URI XR fragments - } -} - // scale embedded XR fragments https://xrfragment.org/#scaling%20of%20instanced%20objects xrf.frag.src.scale = function(scene, opts, url){ let { mesh, model, camera, renderer, THREE} = opts @@ -97,7 +81,8 @@ xrf.frag.src.scale = function(scene, opts, url){ let cleanScene = scene.clone() if( !cleanScene ) debugger let remove = [] - cleanScene.traverse( (n) => !n.visible && n.children.length == 0 && (remove.push(n)) ) + const notVisible = (n) => !n.visible || (n.material && !n.material.visible) + cleanScene.traverse( (n) => notVisible(n) && n.children.length == 0 && (remove.push(n)) ) remove.map( (n) => n.removeFromParent() ) let restrictTo3DBoundingBox = mesh.geometry diff --git a/src/3rd/js/three/xrf/src/non-euclidian.js b/src/3rd/js/three/xrf/src/non-euclidian.js index e632d64..36b7e96 100644 --- a/src/3rd/js/three/xrf/src/non-euclidian.js +++ b/src/3rd/js/three/xrf/src/non-euclidian.js @@ -1,3 +1,5 @@ +// spec 8: https://xrfragment.org/doc/RFC_XR_Macros.html#embedding-xr-content-using-src + xrf.portalNonEuclidian = function(opts){ let { frag, mesh, model, camera, scene, renderer} = opts diff --git a/src/xrfragment/Filter.hx b/src/xrfragment/Filter.hx index 02b0eca..f23e68d 100644 --- a/src/xrfragment/Filter.hx +++ b/src/xrfragment/Filter.hx @@ -52,11 +52,11 @@ 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 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 isDeepSelect:EReg = ~/(^-|\*$)/; // 1. detect nested keys like 'foo*' (reference regex= `/\*$/` ) + 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){ @@ -78,22 +78,21 @@ class Filter { function process(str,prefix = ""){ str = StringTools.trim(str); - var k:String = str.split("=")[0]; // 1. for every filter token split string on `=` + var k:String = str.split("=")[0]; // 1. for every filter token split string on `=` var v:String = str.split("=")[1]; // retrieve existing filter if any 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( isProp.match(str) ){ // 1. WHEN when a `=` key/value is detected: var oper:String = ""; - if( str.indexOf("*") != -1 ) oper = "*"; // 1. then scan for `*` operator (means include all objects for [src](#src) embedded fragment) if( str.indexOf(">") != -1 ) oper = ">"; // 1. then scan for `>` operator if( str.indexOf("<") != -1 ) oper = "<"; // 1. then scan for `<` operator if( isExclude.match(k) ){ - k = k.substr(1); // 1. then strip key-operator: 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 value operator: change value ">=foo" into "foo" - if( oper.length == 0 ) oper = "="; + 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); else rule[oper] = v; @@ -102,8 +101,7 @@ class Filter { 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", isDeepSelect.match(k) ? true : false ); // 1. set `deep` (for objectnames with * suffix or negative selectors) - q.set("key", isDeepSelect.replace(k,'') ); + q.set("key", operators.replace(k,'') ); q.set("value",v); } for( i in 0...token.length ) process( token[i] );