wip: query=>filter + updated index.gltf (portals work)

This commit is contained in:
Leon van Kammen 2023-11-10 18:22:47 +01:00
parent 0f60ebc1e8
commit 7249584dfa
16 changed files with 2128 additions and 1875 deletions

File diff suppressed because one or more lines are too long

4
make
View File

@ -27,8 +27,8 @@ install(){
tests(){
{
which python3 && python3 test/generated/test.py src/spec/*.json | awk '{ print "py: "$0 } END{ print "\n"}'
which node && node test/generated/test.js src/spec/*.json | awk '{ print "js: "$0 } END{ print "\n"}'
which python3 && python3 test/generated/test.py src/spec/*.json | awk '{ print "py: "$0 } END{ print "\n"}'
} | awk '$2 ~ /src/ { $2=sprintf("%-30s",$2); print $0; next; } 1' | tee /tmp/log.txt
grep error /tmp/log.txt && exit 1 || exit 0
}
@ -55,7 +55,7 @@ build(){
parser(){
try rm dist/*
trace haxe build.hxml
haxe build.hxml
ok=$?
sed -i 's|.*nonlocal .*||g' dist/xrfragment.py
ls -lah dist/*

View File

@ -1,74 +1,71 @@
xrf.portalNonEuclidian = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts
xrf.portalNonEuclidian = function(opts){
let { frag, mesh, model, camera, scene, renderer, stencilObjects} = opts
let toFrag = xrf.URI.parse( v.string )
// turn plane into stencilplane
mesh.material = new xrf.THREE.MeshPhongMaterial({ color: 'green' });
mesh.material.depthWrite = false;
mesh.material.stencilWrite = true;
mesh.material.stencilRef = xrf.portalNonEuclidian.stencilRef;
mesh.material.stencilFunc = THREE.AlwaysStencilFunc;
mesh.material.stencilZPass = THREE.ReplaceStencilOp;
//mesh.material.side = THREE.DoubleSide // *TODO* this requires flipping normals based on camera orientation
mesh.portal = {
stencilRef: xrf.portalNonEuclidian.stencilRef
}
// turn plane into stencilplane
mesh.material = new THREE.MeshPhongMaterial({ color: 'green' });
mesh.material.depthWrite = false;
mesh.material.stencilWrite = true;
mesh.material.stencilRef = xrf.portalNonEuclidian.stencilRef;
mesh.material.stencilFunc = THREE.AlwaysStencilFunc;
mesh.material.stencilZPass = THREE.ReplaceStencilOp;
//mesh.material.side = THREE.DoubleSide // *TODO* this requires flipping normals based on camera orientation
mesh.portal = {
stencilRef: xrf.portalNonEuclidian.stencilRef
}
let stencilPos = new xrf.THREE.Vector3()
mesh.getWorldPosition(stencilPos)
let stencilPos = new xrf.THREE.Vector3()
mesh.getWorldPosition(stencilPos)
// allow objects to flip between original and stencil position (which puts them behind stencilplane)
const addStencilFeature = (n) => {
n.stencil = (
(pos,stencilPos, stencilMat, mat ) => (enabled) => {
let sRef = enabled ? mesh.portal.stencilRef : 0
stencilMat.depthTest = false
n.position.copy( enabled ? stencilPos : pos )
n.material = enabled ? stencilMat : mat
xrf.portalNonEuclidian.selectStencil(n, sRef ) // disable depthtest of world-container (sky e.g.)
n.traverse( (c) => !c.portal && (xrf.portalNonEuclidian.selectStencil(c,sRef)) )
}
)( n.position.clone(), stencilPos, n.material.clone(), n.material )
return n
}
// allow objects to flip between original and stencil position (which puts them behind stencilplane)
const addStencilFeature = (n) => {
n.stencil = (
(pos,stencilPos, stencilMat, mat ) => (enabled) => {
let sRef = enabled ? mesh.portal.stencilRef : 0
stencilMat.depthTest = false
n.position.copy( enabled ? stencilPos : pos )
n.material = enabled ? stencilMat : mat
xrf.portalNonEuclidian.selectStencil(n, sRef ) // disable depthtest of world-container (sky e.g.)
n.traverse( (c) => !c.portal && (xrf.portalNonEuclidian.selectStencil(c,sRef)) )
}
)( n.position.clone(), stencilPos, n.material.clone(), n.material )
return n
}
// collect related objects from XRWG to render inside stencilplane
if( stencilObjects.length == 0 ) return console.warn(`no objects are tagged with (portal)object name '${mesh.name}'`)
stencilObjects = stencilObjects
.filter( (n) => !n.portal ) // filter out (self)references to portals (prevent recursion)
.map(addStencilFeature)
// add missing lights to make sure things get lit properly
xrf.scene.traverse( (n) => n.isLight &&
!stencilObjects.find( (o) => o.uuid == n.uuid ) &&
(stencilObjects.push(n))
)
// collect related objects from XRWG to render inside stencilplane
let objs = XRWG.match(mesh.name,0)
if( objs.length == 0 ) return console.warn(`no objects are tagged with (portal)object name '${mesh.name}'`)
objs = objs[0].nodes
.filter( (n) => !n.portal ) // filter out (self)references to portals (prevent recursion)
.map(addStencilFeature)
// add missing lights to make sure things get lit properly
xrf.scene.traverse( (n) => n.isLight &&
!objs.find( (o) => o.uuid == n.uuid ) &&
(objs.push(n))
)
// put it into a scene (without .add() because it reparents objects) so we can render it separately
mesh.stencilObjects = new xrf.THREE.Scene()
mesh.stencilObjects.children = stencilObjects
// put it into a scene (without .add() because it reparents objects) so we can render it separately
mesh.stencilObjects = new xrf.THREE.Scene()
mesh.stencilObjects.children = objs
// *TODO* stencilize any tagged plane without material
// *TODO* stencilize any tagged plane without material
// enable the stencil-material of the stencil objects
const showPortal = (n,show) => {
if( n.portal ) n.visible = show
return true
}
mesh.onAfterRender = function(renderer, scene, camera, geometry, material, group ){
mesh.stencilObjects.traverse( (n) => showPortal(n,false) && n.stencil && (n.stencil(true)) )
renderer.autoClear = false
renderer.autoClearStencil = false
//renderer.sortObjects = false
renderer.render( mesh.stencilObjects, camera )
//renderer.sortObjects = true
mesh.stencilObjects.traverse( (n) => showPortal(n,true) && n.stencil && (n.stencil(false)) )
}
// enable the stencil-material of the stencil objects
const showPortal = (n,show) => {
if( n.portal ) n.visible = show
return true
}
mesh.onAfterRender = function(renderer, scene, camera, geometry, material, group ){
mesh.stencilObjects.traverse( (n) => showPortal(n,false) && n.stencil && (n.stencil(true)) )
renderer.autoClear = false
renderer.autoClearStencil = false
//renderer.sortObjects = false
renderer.render( mesh.stencilObjects, camera )
//renderer.sortObjects = true
mesh.stencilObjects.traverse( (n) => showPortal(n,true) && n.stencil && (n.stencil(false)) )
}
xrf.portalNonEuclidian.stencilRef += 1 // each portal has unique stencil id
console.log("enabling portal for object '${mesh.name}'`")
xrf.portalNonEuclidian.stencilRef += 1 // each portal has unique stencil id
console.log("enabling portal for object '${mesh.name}'`")
}
xrf.portalNonEuclidian.selectStencil = (n, stencilRef, depthTest) => {
@ -81,3 +78,22 @@ xrf.portalNonEuclidian.selectStencil = (n, stencilRef, depthTest) => {
}
xrf.portalNonEuclidian.stencilRef = 1
// scan for non-euclidian portals (planes with nameless & textureless material which are tagged)
xrf.addEventListener('parseModel', (opts) => {
let {model} = opts
model.scene.traverse( (n) => {
const hasMaterialName = n.material && n.material.name.length > 0
const hasTexture = n.material && n.material.map
const isPlane = n.geometry && n.geometry.attributes.uv && n.geometry.attributes.uv.count == 4
const isHref = n.userData.href != undefined
const isSRC = n.userData.src != undefined || n.isSRC
let stencilObjects = XRWG.match(n.name,0)
const hasReferences = stencilObjects.length && stencilObjects[0].nodes.length > 1
if( n.geometry && hasReferences && !hasMaterialName && !hasTexture && !isSRC && !n.isSRC){
xrf.portalNonEuclidian({...opts, mesh:n, stencilObjects: stencilObjects[0].nodes})
}
})
})

View File

@ -1,12 +1,15 @@
/*
* TODO: refactor/fix this (queries are being refactored to filters)
*/
// spec: https://xrfragment.org/#queries
xrf.frag.q = function(v, opts){
xrf.filter = function(v, opts){
let { frag, mesh, model, camera, scene, renderer, THREE} = opts
console.log(" └ running query ")
let qobjs = Object.keys(v.query)
// convience function for other fragments (which apply to the query)
frag.q.getObjects = () => {
frag.filter.getObjects = () => {
let objs = []
scene.traverse( (o) => {
for ( let name in v.query ) {
@ -21,20 +24,20 @@ xrf.frag.q = function(v, opts){
return o
})
}
xrf.frag.q.filter(scene,frag) // spec : https://xrfragment.org/#queries
xrf.filter.scene(scene,frag) // spec : https://xrfragment.org/#queries
}
xrf.frag.q.filter = function(scene,frag){
xrf.filter.scene = function(scene,frag){
// spec: https://xrfragment.org/#queries
let q = frag.q.query
scene.traverse( (mesh) => {
for ( let i in q ) {
let isMeshId = q[i].id != undefined
let isMeshProperty = q[i].rules != undefined && q[i].rules.length && !isMeshId
let isMeshId = q[i].id != undefined
let isMeshProperty = q[i].filter != undefined && !isMeshId
if( q[i].root && mesh.isSRC ) continue; // ignore nested object for root-items (queryseletor '/foo' e.g.)
if( isMeshId &&
(i == mesh.name || xrf.hasTag(i,mesh.userData.tag))) mesh.visible = q[i].id
if( isMeshProperty && mesh.userData[i] ) mesh.visible = (new xrf.Query(frag.q.string)).testProperty(i,mesh.userData[i])
//if( isMeshProperty && mesh.userData[i] ) mesh.visible = (new xrf.Query(frag.q.string)).testProperty(i,mesh.userData[i])
}
})
}

View File

@ -33,11 +33,6 @@ xrf.frag.href = function(v, opts){
if( mesh.userData.XRF.href.exec ) return // mesh already initialized
// derived properties
const isLocal = v.string[0] == '#'
const isPlane = mesh.geometry && mesh.geometry.attributes.uv && mesh.geometry.attributes.uv.count == 4
const hasSrc = mesh.userData.src != undefined
let click = mesh.userData.XRF.href.exec = (e) => {
let lastPos = `pos=${camera.position.x.toFixed(2)},${camera.position.y.toFixed(2)},${camera.position.z.toFixed(2)}`
@ -82,8 +77,6 @@ xrf.frag.href = function(v, opts){
mesh.addEventListener('mouseenter', selected(true) )
mesh.addEventListener('mouseleave', selected(false) )
if( isLocal && isPlane && !hasSrc && !mesh.material.map ) xrf.portalNonEuclidian(v,opts)
// lazy add mesh (because we're inside a recursive traverse)
setTimeout( (mesh) => {
xrf.interactive.add(mesh)

View File

@ -7,6 +7,7 @@ xrf.frag.src = function(v, opts){
let src;
let url = v.string
let vfrag = xrfragment.URI.parse(url)
console.dir({url,vfrag})
opts.isPlane = mesh.geometry && mesh.geometry.attributes.uv && mesh.geometry.attributes.uv.count == 4
const addModel = (model,url,frag) => {
@ -15,10 +16,11 @@ xrf.frag.src = function(v, opts){
xrf.frag.src.scale( src, opts, url )
xrf.frag.src.eval( src, opts, url )
// allow 't'-fragment to setup separate animmixer
xrf.emit('parseModel', {...opts, scene:src, model})
enableSourcePortation(src)
mesh.add(src)
mesh.traverse( (n) => n.isSRC = n.isXRF = true )
model.scene = src
mesh.add(model.scene)
mesh.traverse( (n) => n.isSRC = n.isXRF = true ) // mark everything SRC
xrf.emit('parseModel', {...opts, scene:src, model})
if( mesh.material ) mesh.material.visible = false
}
@ -118,7 +120,7 @@ xrf.frag.src.filterScene = (scene,opts) => {
let { mesh, model, camera, renderer, THREE, hashbus, frag} = opts
let obj, src
// cherrypicking of object(s)
if( !frag.q ){
if( !frag.filter ){
src = new THREE.Group()
if( Object.keys(frag).length > 0 ){
for( var i in frag ){
@ -132,9 +134,10 @@ xrf.frag.src.filterScene = (scene,opts) => {
}
// filtering of objects using query
if( frag.q ){
if( frag.filter ){
console.warn("TODO: filter scene");
src = scene
xrf.frag.q.filter(src,frag)
xrf.filter.scene(src,frag)
}
src.traverse( (m) => {
if( m.userData && (m.userData.src || m.userData.href) ) return ; // prevent infinite recursion

View File

@ -1,4 +1,4 @@
import xrfragment.Query;
import xrfragment.Filter;
import xrfragment.URI;
import xrfragment.XRF;
@ -17,26 +17,25 @@ class Test {
static public function main():Void {
test( "url.json", Spec.load("src/spec/url.json") );
test( "t.json", Spec.load("src/spec/t.json") );
test( "q.selectors.json", Spec.load("src/spec/query.selectors.json") );
test( "q.root.json", Spec.load("src/spec/query.root.json") );
test( "q.rules.json", Spec.load("src/spec/query.rules.json") );
test( "filter.selectors.json", Spec.load("src/spec/filter.selectors.json") );
//test( Spec.load("src/spec/tmp.json") );
if( errors > 1 ) trace("\n-----\n[ ] "+errors+" errors :/");
}
static public function test( topic:String, spec:Array<Dynamic>):Void {
trace("\n[.] running "+topic);
var Query = xrfragment.Query;
var Filter = xrfragment.Filter;
for( i in 0...spec.length ){
var q:Query = null;
var f:Filter = null;
var res:haxe.DynamicAccess<Dynamic> = null;
var valid:Bool = false;
var item:Dynamic = spec[i];
if( item.fn == "query" ) q = new Query(item.data);
if( item.fn == "url" ) res = URI.parse(item.data,0);
if( item.expect.fn == "test" ) valid = item.expect.out == q.test( item.expect.input[0] );
if( item.expect.fn == "testProperty" ) valid = item.expect.out == q.testProperty( item.expect.input[0], item.expect.input[1] );
if( item.expect.fn == "testPropertyExclude" ) valid = item.expect.out == q.testProperty( item.expect.input[0], item.expect.input[1], true );
f = new Filter(item.data);
res = URI.parse(item.data,0);
if( item.expect.fn == "test" ) valid = item.expect.out == f.test( item.expect.input[0] );
if( item.expect.fn == "testProperty" ) valid = item.expect.out == f.testProperty( item.expect.input[0], item.expect.input[1] );
if( item.expect.fn == "testPropertyInt" ) valid = item.expect.out == f.testProperty( item.expect.input[0], item.expect.input[1] );
if( item.expect.fn == "testPropertyExclude" ) valid = item.expect.out == f.testProperty( item.expect.input[0], item.expect.input[1], true );
if( item.expect.fn == "testParsed" ) valid = item.expect.out == res.exists(item.expect.input);
if( item.expect.fn == "testPredefinedView" ) valid = res.exists(item.expect.input) && item.expect.out == res.get(item.expect.input).is( XRF.PV_EXECUTE) ;
if( item.expect.fn == "testPropertyAssign" ) valid = res.exists(item.expect.input) && item.expect.out == res.get(item.expect.input).is( XRF.PROP_BIND) ;
@ -46,7 +45,7 @@ class Test {
if( item.expect.fn == "equal.x" ) valid = equalX(res,item);
if( item.expect.fn == "equal.xy" ) valid = equalXY(res,item);
if( item.expect.fn == "equal.xyz" ) valid = equalXYZ(res,item);
if( item.expect.fn == "testQueryRoot" ) valid = item.expect.out == q.get()[ item.expect.input[0] ].root;
if( item.expect.fn == "testFilterRoot" ) valid = item.expect.out == f.get()[ item.expect.input[0] ].root;
var ok:String = valid ? "[ ] " : "[ ] ";
trace( ok + item.fn + ": '" + item.data + "'" + (item.label ? " (" + (item.label?item.label:item.expect.fn) +")" : ""));
if( !valid ) errors += 1;
@ -75,114 +74,18 @@ class Test {
trace( Uri.parse(url,0) );
}
static public function testQuery():Void {
var Query = xrfragment.Query;
static public function testFilter():Void {
var Filter = xrfragment.Filter;
trace( (new Query("foo or bar")).toObject() );
trace( (new Query("class:fopoer or bar foo:bar")).toObject().or[0] );
trace( (new Query("-skybox class:foo")).toObject().or[0] );
trace( (new Query("foo/flop moo or bar")).toObject().or[0] );
trace( (new Query("-foo/flop moo or bar")).toObject().or[0] );
trace( (new Query("price:>4 moo or bar")).toObject().or[0] );
trace( (new Query("price:>=4 moo or bar")).toObject().or[0] );
trace( (new Query("price:<=4 moo or bar")).toObject().or[0] );
trace( (new Query("price:!=4 moo or bar")).toObject().or[0] );
var q:Dynamic = new Query("price:!=4 moo or bar");
var obj:Dynamic = q.toObject();
q.test( "price", 4);
var ok = !q.selected("slkklskdf");
if( !ok ) throw 'node should not be allowed';
q = new Query("price:!=3 moo or bar");
var obj:Dynamic = q.toObject();
q.test( "price", 4);
var ok = q.selected("slkklskdf");
if( !ok ) throw 'non-mentioned node should be allowed';
q = new Query("moo or bar");
var obj:Dynamic = q.toObject();
var ok = !q.selected("slkklskdf");
if( !ok ) throw 'node should not be allowed';
obj = q.toObject();
var ok = q.selected("moo");
if( !ok ) throw 'moo should be allowed';
var ok = q.selected("bar");
if( !ok ) throw 'bar should be allowed';
q = new Query("price:>3 moo or bar");
var obj:Dynamic = q.toObject();
q.test( "price", 4);
var ok = q.selected("foo");
if( !ok ) throw 'node should be allowed';
var ok = q.selected("bar");
if( !ok ) throw 'node should be allowed';
var ok = q.selected("moo");
if( !ok ) throw 'node should be allowed';
q = new Query("price:>3 price:<10 -bar");
var obj:Dynamic = q.toObject();
q.test( "price", 4);
var ok = q.selected("foo");
if( !ok ) throw 'node should be allowed';
var ok = !q.selected("bar");
if( !ok ) throw 'bar should not be allowed';
q.test("price", 20);
var ok = !q.selected("foo");
if( !ok ) throw 'price 20 should not be allowed';
q = new Query("-bar");
var obj:Dynamic = q.toObject();
var ok = q.selected("foo");
if( !ok ) throw 'node should be allowed';
var ok = !q.selected("bar");
if( !ok ) throw 'bar should not be allowed';
q = new Query("title:*");
var obj:Dynamic = q.toObject();
var ok = !q.selected("foo");
if( !ok ) throw 'node should not be allowed';
q.test("foo","bar");
var ok = !q.selected("foo");
if( !ok ) throw 'node should not be allowed';
q.test("title","bar");
var ok = q.selected("foo");
if( !ok ) throw 'node should be allowed';
q = new Query("-bar +bar");
var obj:Dynamic = q.toObject();
var ok = q.selected("foo");
if( !ok ) throw 'node should be allowed';
var ok = q.selected("bar");
if( !ok ) throw 'bar should be allowed';
q = new Query("?discount");
var obj:Dynamic = q.toObject();
q.test("?discount","-foo");
var ok = !q.selected("foo");
if( !ok ) throw 'foo should not be allowed';
q = new Query("?");
q.test("?","-foo");
var ok = !q.selected("foo");
if( !ok ) throw 'foo should not be allowed';
q = new Query("?");
var ok = q.selected("foo");
if( !ok ) throw 'foo should not be allowed';
q = new Query("?discount");
q.test("?discount","-foo");
var ok = !q.selected("foo");
if( !ok ) throw 'foo should not be allowed';
q = new Query("?discount +foo");
var obj:Dynamic = q.toObject();
q.test("?discount","-foo");
var ok = !q.selected("foo");
if( !ok ) throw 'foo should not be allowed';
var ok = !q.selected("foo");
if( !ok ) throw 'foo should not be allowed';
trace( (new Filter("foo or bar")).toObject() );
trace( (new Filter("class:fopoer or bar foo:bar")).toObject().or[0] );
trace( (new Filter("-skybox class:foo")).toObject().or[0] );
trace( (new Filter("foo/flop moo or bar")).toObject().or[0] );
trace( (new Filter("-foo/flop moo or bar")).toObject().or[0] );
trace( (new Filter("price:>4 moo or bar")).toObject().or[0] );
trace( (new Filter("price:>=4 moo or bar")).toObject().or[0] );
trace( (new Filter("price:<=4 moo or bar")).toObject().or[0] );
trace( (new Filter("price:!=4 moo or bar")).toObject().or[0] );
trace("all tests passed");
}

View File

@ -0,0 +1,11 @@
[
{"fn":"url","data":"http://foo.com?foo=1#foo*&-sometag&-someid&myid", "expect":{ "fn":"testParsed", "input":"myid","out":true},"label":"myid exists"},
{"fn":"url","data":"http://foo.com?foo=1#tag=bar", "expect":{ "fn":"testParsed", "input":"tag", "out":true},"label":"tag exists"},
{"fn":"url","data":"http://foo.com?foo=1#-tag=bar", "expect":{ "fn":"testParsed", "input":"tag", "out":true},"label":"tag exists"},
{"fn":"url","data":"http://foo.com?foo=1#price=>2", "expect":{ "fn":"testParsed", "input":"price","out":true},"label":"query test"},
{"fn":"query","data":"tag=bar", "expect":{ "fn":"testProperty","input":["tag","bar"],"out":true}},
{"fn":"query","data":"-tag=foo", "expect":{ "fn":"testProperty","input":["tag","foo"],"out":false}},
{"fn":"query","data":"price=>2", "expect":{ "fn":"testProperty","input":["price","1"],"out":false}},
{"fn":"query","data":"price=<2", "expect":{ "fn":"testProperty","input":["price","5"],"out":false}},
{"fn":"query","data":"price=<2", "expect":{ "fn":"testProperty","input":["price","1"],"out":true}}
]

View File

@ -1,2 +0,0 @@
[
]

View File

@ -1,15 +0,0 @@
[
{"fn":"query","data":"price:>=5", "expect":{ "fn":"testProperty","input":["price","10"],"out":true}},
{"fn":"query","data":"price:>=15", "expect":{ "fn":"testProperty","input":["price","10"],"out":false}},
{"fn":"query","data":"price:>=5", "expect":{ "fn":"testProperty","input":["price","4"],"out":false}},
{"fn":"query","data":"price:>=5", "expect":{ "fn":"testProperty","input":["price","0"],"out":false}},
{"fn":"query","data":"price:>=2", "expect":{ "fn":"testProperty","input":["price","2"],"out":true}},
{"fn":"query","data":"price:>=5 price:0", "expect":{ "fn":"testProperty","input":["price","1"],"out":false},"label":"price=1"},
{"fn":"query","data":"price:>=5 price:0", "expect":{ "fn":"testProperty","input":["price","0"],"out":true},"label":"price=0"},
{"fn":"query","data":"price:>=5 price:0", "expect":{ "fn":"testProperty","input":["price","6"],"out":true},"label":"price=6"},
{"fn":"query","data":"tag:foo", "expect":{ "fn":"testProperty","input":["tag","foo"],"out":true}},
{"fn":"query","data":"-tag:foo", "expect":{ "fn":"testProperty","input":["tag","foo"],"out":false}},
{"fn":"query","data":"-tag:foo", "expect":{ "fn":"testPropertyExclude","input":["tag","foo"],"out":true},"label":"testExclude"},
{"fn":"query","data":".foo price:5 -tag:foo", "expect":{ "fn":"test","input":[{"price":5}],"out":true}},
{"fn":"query","data":".foo price:5 -tag:foo", "expect":{ "fn":"test","input":[{"tag":"foo","price":5}],"out":false}}
]

View File

@ -1,15 +0,0 @@
[
{"fn":"query","data":"tag:bar", "expect":{ "fn":"testProperty","input":["tag","bar"],"out":true}},
{"fn":"query","data":"tag:bar -tag:foo", "expect":{ "fn":"testProperty","input":["tag","foo"],"out":false}},
{"fn":"query","data":"tag:bar -tag:foo tag:foo", "expect":{ "fn":"testProperty","input":["tag","foo"],"out":true}},
{"fn":"query","data":"tag:bar -tag:bar tag:bar", "expect":{ "fn":"testProperty","input":["tag","bar"],"out":true}},
{"fn":"query","data":"tag:foo -tag:foo tag:foo", "expect":{ "fn":"testProperty","input":["tag","foo"],"out":true},"label":"tag:foo"},
{"fn":"query","data":"tag:foo -tag:foo bar:5 tag:foo", "expect":{ "fn":"testProperty","input":["tag","foo"],"out":true},"label":"tag:foo"},
{"fn":"query","data":"tag:foo -tag:foo bar:>5 tag:foo", "expect":{ "fn":"testProperty","input":["tag","foo"],"out":true},"label":"tag:foo"},
{"fn":"query","data":"tag:foo -tag:foo bar:>5 tag:foo", "expect":{ "fn":"testProperty","input":["tag","foo"],"out":true},"label":"tag:foo"},
{"fn":"query","data":"tag:foo -tag:foo tag:foo", "expect":{ "fn":"testProperty","input":["tag","foo"],"out":true},"label":"tag:foo"},
{"fn":"query","data":"tag:foo -tag:foo tag:foo", "expect":{ "fn":"testProperty","input":["id","foo"],"out":true},"label":"id:foo"},
{"fn":"query","data":"tag:foo -foo foo", "expect":{ "fn":"testProperty","input":["id","foo"],"out":true},"label":"id:foo?"},
{"fn":"query","data":"/foo", "expect":{ "fn":"testQueryRoot","input":["foo"],"out":true},"label":"foo should be root-only"},
{"fn":"query","data":"/foo foo", "expect":{ "fn":"testQueryRoot","input":["foo"],"out":false},"label":"foo should recursively selected"}
]

View File

@ -1,7 +1,6 @@
[
{"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#q=-bar", "expect":{ "fn":"testBrowserOverride", "input":"q","out":false},"label":"browser URI cannot override q (defined in asset)"},
{"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)"},

View File

@ -38,24 +38,26 @@ package xrfragment;
@:expose // <- makes the class reachable from plain JavaScript
@:keep // <- avoids accidental removal by dead code elimination
class Query {
class Filter {
/**
* # Spec
*
* > version 1.0.0 [![Actions Status](https://github.com/coderofsalvation/xrfragment/workflows/test/badge.svg)](https://github.com/coderofsalvation/xrfragment/actions) generated by `make doc` @ $(date +"%Y-%m-%dT%H:%M:%S%z")
*
* In case your programming language has no parser ([check here](https://github.com/coderofsalvation/xrfragment/tree/main/dist)) you can [crosscompile it](https://github.com/coderofsalvation/xrfragment/blob/main/build.hxml), or roll your own `Query.parse(str)` using the spec:
* In case your programming language has no parser ([check here](https://github.com/coderofsalvation/xrfragment/tree/main/dist)) you can [crosscompile it](https://github.com/coderofsalvation/xrfragment/blob/main/build.hxml), or roll your own `Filter.parse(str)` using the spec:
*/
// 1. requirement: receive arguments: query (string)
// 1. requirement: receive arguments: filter (string)
private var str:String = "";
private var q:haxe.DynamicAccess<Dynamic> = {}; // 1. create an associative array/object to store query-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 q:haxe.DynamicAccess<Dynamic> = {}; // 1. create an associative array/object to store filter-arguments as objects
private var isProp:EReg = ~/^.*=[><=!]?/; // 1. detect object id's & properties `foo=1` and `foo` (reference regex= `/^.*=[><=!]?/` )
private var isExclude:EReg = ~/^-/; // 1. detect excluders like `-foo`,`-foo=1`,`-.foo`,`-/foo` (reference regex= `/^-/` )
private var isRoot:EReg = ~/^[-]?\//; // 1. detect root selectors like `/foo` (reference regex= `/^[-]?\//` )
private var isNumber:EReg = ~/^[0-9\.]+$/; // 1. detect number values like `foo=1` (reference regex= `/^[0-9\.]+$/` )
private var isDeepSelect:EReg = ~/\*$/; // 1. detect nested keys like 'foo*' (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);
@ -76,34 +78,32 @@ class Query {
function process(str,prefix = ""){
str = StringTools.trim(str);
var k:String = str.split(":")[0]; // 1. for every query token split string on `:`
var v:String = str.split(":")[1];
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<Dynamic> = {};
if( q.get(prefix+k) ) filter = q.get(prefix+k);
filter['rules'] = filter['rules'] != null ? filter['rules'] : new Array<Dynamic>(); // 1. create an empty array `rules`
if( isProp.match(str) ){ // 1. <b>WHEN</b></b> 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( str.indexOf(">=") != -1 ) oper = ">="; // 1. then scan for `>=` operator
if( str.indexOf("<=") != -1 ) oper = "<="; // 1. then scan for `<=` operator
if( isExclude.match(k) ){
oper = "!=";
oper = "=!";
k = k.substr(1); // 1. then strip key-operator: convert "-foo" into "foo"
}else v = v.substr(oper.length); // 1. then strip value operator: change value ">=foo" into "foo"
if( oper.length == 0 ) oper = "=";
var rule:haxe.DynamicAccess<Dynamic> = {};
if( isNumber.match(v) ) rule[ oper ] = Std.parseFloat(v);
else rule[oper] = v;
filter['rules'].push( rule ); // 1. add operator and value to rule-array
filter['filter'] = rule; // 1. add filter rule
q.set( k, filter );
return;
}else{ // 1. <b>ELSE </b> we are dealing with an object
filter[ "id" ] = isExclude.match(str) ? false: true; // 1. therefore we we set `id` to `true` or `false` (false=excluder `-`)
filter[ "root" ] = isRoot.match(str) ? true: false; // 1. and we set `root` to `true` or `false` (true=`/` root selector is present)
filter[ "id" ] = isExclude.match(str) ? false: true; // 1. therefore we we set `id` to `true` or `false` (false=excluder `-`)
filter[ "root" ] = isRoot.match(str) ? true: false; // 1. and we set `root` to `true` or `false` (true=`/` root selector is present)
filter[ "deep" ] = isDeepSelect.match(str) ? true: false; // 1. and we set `deep` to `true` or `false` (for objectnames with * suffix)
if( isExclude.match(str) ) str = str.substr(1); // convert '-foo' into 'foo'
if( isRoot.match(str) ) str = str.substr(1); // 1. we convert key '/foo' into 'foo'
q.set( str ,filter ); // 1. finally we add the key/value to the store (`store.foo = {id:false,root:true}` e.g.)
@ -128,6 +128,10 @@ class Query {
return qualify;
}
/*
* this is a utility function of a filter which helps
* in telling if a property qualifies according to this filter object
*/
@:keep
public function testProperty( property:String, value:String, ?exclude:Bool ):Bool{
var conds:Int = 0;
@ -148,32 +152,29 @@ class Query {
// conditional rules
for ( k in Reflect.fields(q) ){
var filter:Dynamic = Reflect.field(q,k);
if( filter.rules == null ) continue;
var rules:Array<Dynamic> = filter.rules;
var f:Dynamic = Reflect.field(q,k);
if( f.filter == null ) continue;
for( rule in rules ){
//if( Std.isOfType(value, String) ) contiggnue;
if( exclude ){
if( Reflect.field(rule,'!=') != null && testprop( Std.string(value) == Std.string(Reflect.field(rule,'!='))) && exclude ) qualify += 1;
}else{
if( Reflect.field(rule,'*') != null && testprop( Std.parseFloat(value) != null ) ) qualify += 1;
if( Reflect.field(rule,'>') != null && testprop( Std.parseFloat(value) > Std.parseFloat(Reflect.field(rule,'>' )) ) ) qualify += 1;
if( Reflect.field(rule,'<') != null && testprop( Std.parseFloat(value) < Std.parseFloat(Reflect.field(rule,'<' )) ) ) qualify += 1;
if( Reflect.field(rule,'>=') != null && testprop( Std.parseFloat(value) >= Std.parseFloat(Reflect.field(rule,'>=')) ) ) qualify += 1;
if( Reflect.field(rule,'<=') != null && testprop( Std.parseFloat(value) <= Std.parseFloat(Reflect.field(rule,'<=')) ) ) qualify += 1;
if( Reflect.field(rule,'=') != null && (
testprop( value == Reflect.field(rule,'=')) ||
testprop( Std.parseFloat(value) == Std.parseFloat(Reflect.field(rule,'=')))
)) qualify += 1;
}
//if( Std.isOfType(value, String) ) contiggnue;
if( exclude ){
if( Reflect.field(f.filter,'!=') != null && testprop( Std.string(value) == Std.string(Reflect.field(f.filter,'!='))) && exclude ) qualify += 1;
}else{
if( Reflect.field(f.filter,'*') != null && testprop( Std.parseFloat(value) != null ) ) qualify += 1;
if( Reflect.field(f.filter,'>') != null && testprop( Std.parseFloat(value) > Std.parseFloat(Reflect.field(f.filter,'>' )) ) ) qualify += 1;
if( Reflect.field(f.filter,'<') != null && testprop( Std.parseFloat(value) < Std.parseFloat(Reflect.field(f.filter,'<' )) ) ) qualify += 1;
if( Reflect.field(f.filter,'>=') != null && testprop( Std.parseFloat(value) >= Std.parseFloat(Reflect.field(f.filter,'>=')) ) ) qualify += 1;
if( Reflect.field(f.filter,'<=') != null && testprop( Std.parseFloat(value) <= Std.parseFloat(Reflect.field(f.filter,'<=')) ) ) qualify += 1;
if( Reflect.field(f.filter,'=') != null && (
testprop( value == Reflect.field(f.filter,'=')) ||
testprop( Std.parseFloat(value) == Std.parseFloat(Reflect.field(f.filter,'=')))
)) qualify += 1;
}
}
return qualify > 0;
}
}
/**
* > icanhazcode? yes, see [Parser.hx](https://github.com/coderofsalvation/xrfragment/blob/main/src/xrfragment/Query.hx)
* > icanhazcode? yes, see [Parser.hx](https://github.com/coderofsalvation/xrfragment/blob/main/src/xrfragment/Filter.hx)
*
* # Tests
*

View File

@ -7,8 +7,9 @@ import xrfragment.XRF;
@:expose // <- makes the class reachable from plain JavaScript
@:keep // <- avoids accidental removal by dead code elimination
class Parser {
public static var error:String = "";
public static var debug:Bool = false;
public static var error:String = "";
public static var debug:Bool = false;
public static var keyClean:EReg = ~/(\*$|^-)/g;
@:keep
public static function parse(key:String,value:String,store:haxe.DynamicAccess<Dynamic>):Bool {
@ -16,33 +17,17 @@ class Parser {
var Frag:Map<String, Int> = new Map<String, Int>();
Frag.set("#", XRF.ASSET | XRF.T_PREDEFINED_VIEW | XRF.PV_EXECUTE );
Frag.set("prio", XRF.ASSET | XRF.T_INT );
Frag.set("src", XRF.ASSET | XRF.T_URL );
// category: href navigation / portals / teleporting
Frag.set("href", XRF.ASSET | XRF.T_URL | XRF.T_PREDEFINED_VIEW );
Frag.set("tag", XRF.ASSET | XRF.T_STRING );
// category: query selector / object manipulation
// spatial category: query selector / object manipulation
Frag.set("pos", XRF.PV_OVERRIDE | XRF.T_VECTOR3 | XRF.T_STRING_OBJ | XRF.METADATA | XRF.NAVIGATOR );
Frag.set("q", XRF.PV_OVERRIDE | XRF.T_STRING | XRF.METADATA );
Frag.set("scale", XRF.QUERY_OPERATOR | XRF.PV_OVERRIDE | XRF.T_VECTOR3 | XRF.METADATA );
Frag.set("rot", XRF.QUERY_OPERATOR | XRF.PV_OVERRIDE | XRF.T_VECTOR3 | XRF.METADATA | XRF.NAVIGATOR );
Frag.set("mov", XRF.QUERY_OPERATOR | XRF.PV_OVERRIDE | XRF.T_VECTOR3 | XRF.METADATA );
Frag.set("show", XRF.QUERY_OPERATOR | XRF.PV_OVERRIDE | XRF.T_INT | XRF.METADATA );
Frag.set("env", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_STRING | XRF.METADATA );
// category: animation
Frag.set("t", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_FLOAT | XRF.T_VECTOR2 | XRF.T_STRING | XRF.NAVIGATOR | XRF.METADATA);
Frag.set("tv", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_FLOAT | XRF.T_VECTOR2 | XRF.T_VECTOR3 | XRF.NAVIGATOR | XRF.METADATA);
Frag.set("gravity", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_VECTOR3 | XRF.METADATA );
Frag.set("physics", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_VECTOR3 | XRF.METADATA );
// category: device / viewport settings
Frag.set("fov", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_INT | XRF.NAVIGATOR | XRF.METADATA );
Frag.set("clip", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_VECTOR2 | XRF.NAVIGATOR | XRF.METADATA );
Frag.set("fog", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_VECTOR2 | XRF.NAVIGATOR | XRF.METADATA );
Frag.set("bg", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_VECTOR3 | XRF.NAVIGATOR | XRF.METADATA );
// category: author / metadata
Frag.set("namespace", XRF.ASSET | XRF.T_STRING );
@ -64,12 +49,12 @@ class Parser {
// 1. requirement: receive arguments: key (string), value (string), store (writable associative array/object)
// dynamic fragments cases: predefined views & assign/binds
var isPVDynamic:Bool = value.length == 0 && key.length > 0 && !Frag.exists(key);
var isPVDynamic:Bool = key.length > 0 && !Frag.exists(key);
var isPVDefault:Bool = value.length == 0 && key.length > 0 && key == "#";
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 );
v.validate(key); // will fail but will parse multiple args for us (separated by |)
store.set(key, v );
v.validate(value); // will fail but will parse multiple args for us (separated by |)
store.set( keyClean.replace(key,''), v );
return true;
}
@ -85,9 +70,8 @@ class Parser {
}else{ // 1. expose (but mark) non-offical fragments too
if( Std.isOfType(value, String) ) v.guessType(v,value);
v.noXRF = true;
store.set(key,v);
store.set( keyClean.replace(key,'') ,v);
}
return true;
}

View File

@ -18,13 +18,6 @@ import xrfragment.XRF;
* sub-delims = "," / "="
* ```
*
* > Example: `://foo.com/my3d.asset#pos=1,0,0&prio=-5&t=0,100|100,200`
*
* | Explanation | |
* |-|-|
* | `pos=1,2,3` | vector/coordinate argument e.g. |
* | `pos=1,2,3&rot=0,90,0&q=.foo` | combinators |
*
* In case your programming language has no parser ([check here](https://github.com/coderofsalvation/xrfragment/tree/main/dist)) you can [crosscompile it](https://github.com/coderofsalvation/xrfragment/blob/main/build.hxml), or roll your own `Parser.parse(k,v,store)` using the spec:
*
*/
@ -38,7 +31,7 @@ class URI {
if( url == null || url.indexOf("#") == -1 ) return store;
var fragment:Array<String> = url.split("#"); // 1. fragment URI starts with `#`
var splitArray:Array<String> = fragment[1].split('&'); // 1. fragments are split by `&`
for (i in 0...splitArray.length) { // 1. loop thru each fragment
for (i in 0...splitArray.length) { // 1. loop thru each fragment
var splitByEqual = splitArray[i].split('='); // 1. for each fragment split on `=` to separate key/values
var regexPlus = ~/\+/g; // 1. fragment-values are urlencoded (space becomes `+` using `encodeUriComponent` e.g.)

View File

@ -15,7 +15,7 @@ class XRF {
// scope types (powers of 2)
public static var ASSET:Int = 1; // fragment is immutable
public static var PROP_BIND:Int = 2; // fragment binds/controls one property with another
public static var QUERY_OPERATOR:Int = 4; // fragment will be applied to result of queryselecto
public static var QUERY_OPERATOR:Int = 4; // fragment will be applied to result of filterselecto
public static var PROMPT:Int = 8; // ask user whether this fragment value can be changed
public static var ROUNDROBIN:Int = 16; // evaluation of this (multi) value can be roundrobined
public static var NAVIGATOR:Int = 32; // fragment can be overridden by (manual) browser URI change
@ -55,7 +55,7 @@ class XRF {
public var string:String; // |string| | | #q=-sun |
public var int:Int; // |int | | [-]x[xxxxx] | #price:>=100 |
public var float:Float; // |float | | [-]x[.xxxx] (ieee)| #prio=-20 |
public var query:Query;
public var filter:Filter;
public var noXRF:Bool;
//
public function new(_fragment:String,_flags:Int){
@ -78,8 +78,6 @@ class XRF {
public function validate(value:String) : Bool{
guessType(this, value); // 1. extract the type
// special case: query has its own DSL (*TODO* allow fragments to have custom validators)
if( fragment == "q" ) query = (new Query(value)).get();
// validate
var ok:Bool = true;
if( !is(T_FLOAT) && is(T_VECTOR2) && !(Std.isOfType(x,Float) && Std.isOfType(y,Float)) ) ok = false;
@ -90,23 +88,27 @@ class XRF {
@:keep
public function guessType(v:XRF, str:String):Void {
v.string = str;
if( str.split(",").length > 1){ // 1. `,` assumes 1D/2D/3D vector-values like x[,y[,z]]
var xyzw:Array<String> = str.split(","); // 1. parseFloat(..) and parseInt(..) is applied to vector/float and int values
if( xyzw.length > 0 ) v.x = Std.parseFloat(xyzw[0]); // 1. anything else will be treated as string-value
if( xyzw.length > 1 ) v.y = Std.parseFloat(xyzw[1]); // 1. incompatible value-types will be dropped / not used
if( xyzw.length > 2 ) v.z = Std.parseFloat(xyzw[2]); //
if( xyzw.length > 3 ) v.w = Std.parseFloat(xyzw[3]); //
} // > the xrfragment specification should stay simple enough
// > for anyone to write a parser using either regexes or grammar/lexers
if( isColor.match(str) ) v.color = str; // > therefore expressions/comprehensions are not supported (max wildcard/comparison operators for queries e.g.)
if( isFloat.match(str) ){
v.x = Std.parseFloat(str);
v.float = v.x;
}
if( isInt.match(str) ){
v.int = Std.parseInt(str);
v.x = cast(v.int);
}
if( !Std.isOfType(str,String) ) return;
if( str.length > 0 ){
if( str.split(",").length > 1){ // 1. `,` assumes 1D/2D/3D vector-values like x[,y[,z]]
var xyzw:Array<String> = str.split(","); // 1. parseFloat(..) and parseInt(..) is applied to vector/float and int values
if( xyzw.length > 0 ) v.x = Std.parseFloat(xyzw[0]); // 1. anything else will be treated as string-value
if( xyzw.length > 1 ) v.y = Std.parseFloat(xyzw[1]); // 1. incompatible value-types will be dropped / not used
if( xyzw.length > 2 ) v.z = Std.parseFloat(xyzw[2]); //
if( xyzw.length > 3 ) v.w = Std.parseFloat(xyzw[3]); //
} // > the xrfragment specification should stay simple enough
// > for anyone to write a parser using either regexes or grammar/lexers
if( isColor.match(str) ) v.color = str; // > therefore expressions/comprehensions are not supported (max wildcard/comparison operators for queries e.g.)
if( isFloat.match(str) ){
v.x = Std.parseFloat(str);
v.float = v.x;
}
if( isInt.match(str) ){
v.int = Std.parseInt(str);
v.x = cast(v.int);
}
filter = (new Filter(v.fragment+"="+v.string)).get();
}else filter = (new Filter(v.fragment)).get();
}
}