diff --git a/doc/RF6_XR_Fragments.png b/doc/RF6_XR_Fragments.png new file mode 100644 index 0000000..128e19e Binary files /dev/null and b/doc/RF6_XR_Fragments.png differ diff --git a/doc/RFC_XR_Fragments.jpg b/doc/RFC_XR_Fragments.jpg deleted file mode 100644 index 3c8ad94..0000000 Binary files a/doc/RFC_XR_Fragments.jpg and /dev/null differ diff --git a/doc/generate.sh b/doc/generate.sh index 90a6aae..65438d8 100755 --- a/doc/generate.sh +++ b/doc/generate.sh @@ -6,4 +6,5 @@ for topic in Fragments Macros; do mmark --html RFC_XR_$topic.md | grep -vE '()' > RFC_XR_$topic.html xml2rfc --v3 RFC_XR_$topic.xml # RFC_XR_$topic.txt sed -i 's/Expires: .*//g' RFC_XR_$topic.txt + convert -size 700x2400 xc:white -font "FreeMono" +antialias -pointsize 12 -fill black -annotate +15+15 "@RFC_XR_Fragments.txt" -colorspace gray +dither -posterize 6 RF6_XR_Fragments.png done diff --git a/doc/shell.nix b/doc/shell.nix index 621d1cf..72df7e8 100644 --- a/doc/shell.nix +++ b/doc/shell.nix @@ -7,6 +7,7 @@ mmark xml2rfc wkhtmltopdf-bin + imagemagick ]; } diff --git a/src/3rd/js/XRWG.js b/src/3rd/js/XRWG.js index 68d3635..425c9b9 100644 --- a/src/3rd/js/XRWG.js +++ b/src/3rd/js/XRWG.js @@ -20,6 +20,7 @@ XRWG.match = (str,types,level) => { return n }) str = str.toLowerCase() + .replace(/[-\*]/,'') // remove excludes and wildcards if( level <10 ) res = res.filter( (n) => n.key == str ) if( level >=10 ) res = res.filter( (n) => n.word == str || n.key == str ) if( level >30 ) res = res.filter( (n) => n.word.match(str) || n.key == str ) diff --git a/src/3rd/js/aframe/xrf-gaze.js b/src/3rd/js/aframe/xrf-gaze.js index 16b1efb..f602802 100644 --- a/src/3rd/js/aframe/xrf-gaze.js +++ b/src/3rd/js/aframe/xrf-gaze.js @@ -4,6 +4,13 @@ AFRAME.registerComponent('xrf-gaze',{ schema:{ spawn:{type:'boolean',default:false}, }, + events:{ + "fusing": function(e){ + if( e.detail.mouseEvent ) return // ignore click event + console.dir(e) + + } + }, setGazer: function(state){ let cam = document.querySelector("[camera]") if( state ){ @@ -15,7 +22,6 @@ AFRAME.registerComponent('xrf-gaze',{ raycaster="objects: .ray" visible="true" position="0 0 -1" - geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03" material="color: #BBBBBB; shader: flat"> ` }else{ @@ -25,9 +31,12 @@ AFRAME.registerComponent('xrf-gaze',{ } }, init:function(data){ - let enabled = () => AFRAME.utils.device.isMobile() let setVisible = (state) => { - if( enabled() ) this.setGazer(state) + if( AFRAME.utils.device.isMobile() ){ + this.setGazer(state) + if( state || xrf.debug ) this.el.setAttribute("geometry","primitive: ring; radiusInner: 0.02; radiusOuter: 0.03") + else this.el.removeAttribute("geometry") + } } setVisible(false); diff --git a/src/3rd/js/three/hashbus.js b/src/3rd/js/three/hashbus.js index fab1fa2..9ccab06 100644 --- a/src/3rd/js/three/hashbus.js +++ b/src/3rd/js/three/hashbus.js @@ -3,17 +3,29 @@ // it allows metadata-keys ('foo' e.g.) of 3D scene-nodes (.userData.foo e.g.) to // react by executing code -let pub = function( url, model, flags ){ // evaluate fragments in url +let pub = function( url, node_or_model, flags ){ // evaluate fragments in url if( !url ) return if( !url.match(/#/) ) url = `#${url}` - model = model || xrf.model let { THREE, camera } = xrf - let frag = xrf.URI.parse( url, flags ) - let opts = {frag, mesh:xrf.camera, model, camera: xrf.camera, scene: xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE, hashbus: xrf.hashbus } + let frag = xrf.URI.parse( url, flags ) + let fromNode = node_or_model != xrf.model + + let opts = { + frag, + mesh: fromNode ? node_or_model : xrf.camera, + model: xrf.model, + camera: xrf.camera, + scene: xrf.scene, + renderer: xrf.renderer, + THREE: xrf.THREE, + hashbus: xrf.hashbus + } xrf.emit('hashbus',opts) .then( () => { for ( let k in frag ){ - pub.fragment(k,opts) + let nodeAlias = fromNode && opts.mesh && opts.mesh.userData && opts.mesh.userData[k] && opts.mesh.userData[k][0] == '#' + if( nodeAlias ) pub(opts.mesh.userData[k], opts.mesh) // evaluate node alias + else pub.fragment(k,opts) } }) return frag @@ -37,7 +49,7 @@ pub.fragment = (k, opts ) => { // evaluate one fragment let frag = opts.frag[k]; let isPVorMediaFrag = frag.is( xrf.XRF.PV_EXECUTE ) || frag.is( xrf.XRF.T_MEDIAFRAG) - if( !opts.skipXRWG && isPVorMediaFrag ) pub.XRWG(opts) + if( !opts.skipXRWG && isPVorMediaFrag ) pub.XRWG(k,opts) // call native function (xrf/env.js e.g.), or pass it to user decorator xrf.emit(k,opts) @@ -48,32 +60,37 @@ pub.fragment = (k, opts ) => { // evaluate one fragment }) } -pub.XRWG = (opts) => { +pub.XRWG = (word,opts) => { let {frag,scene,model,renderer} = opts // if this query was triggered by an src-value, lets filter it const isSRC = opts.embedded && opts.embedded.fragment == 'src' if( !isSRC ){ // spec : https://xrfragment.org/#src - for ( let i in frag ) { - let v = frag[i] - let id = v.is( xrf.XRF.T_DYNAMIC ) ? v.fragment : v.string || v.fragment - if( id == '#' || !id ) return - let match = xrf.XRWG.match(id) - if( v.is( xrf.XRF.PV_EXECUTE ) && !v.is( xrf.XRF.T_DYNAMIC ) ){ - // evaluate aliases - match.map( (w) => { - if( w.key == `#${id}` ){ - if( w.value && w.value[0] == '#' ){ - // if value is alias, execute fragment value - xrf.hashbus.pub( w.value, xrf.model, xrf.XRF.METADATA | xrf.XRF.PV_OVERRIDE | xrf.XRF.NAVIGATOR ) - } + let triggeredByMesh = opts.model != opts.mesh + + let v = frag[word] + let id = v.is( xrf.XRF.T_DYNAMICKEY ) ? word : v.string || word + + if( id == '#' || !id ) return + let match = xrf.XRWG.match(id) + + if( !triggeredByMesh && (v.is( xrf.XRF.PV_EXECUTE ) || v.is( xrf.XRF.T_DYNAMIC)) && !v.is( xrf.XRF.T_DYNAMICKEYVALUE ) ){ + // evaluate global aliases or tag/objectnames + match.map( (w) => { + if( w.key == `#${id}` ){ + if( w.value && w.value[0] == '#' ){ + // if value is alias, execute fragment value + xrf.hashbus.pub( w.value, xrf.model, xrf.XRF.METADATA | xrf.XRF.PV_OVERRIDE | xrf.XRF.NAVIGATOR ) } - }) - xrf.emit('dynamicKey',{ ...opts,v,frag,id,match,scene }) - }else{ - xrf.emit('dynamicKeyValue',{ ...opts,v,frag,id,match,scene }) - } + } + }) + xrf.emit('dynamicKey',{ ...opts,v,frag,id,match,scene }) + }else if( v.string ){ + // evaluate global aliases + xrf.emit('dynamicKeyValue',{ ...opts,v,frag,id,match,scene }) + }else{ + xrf.emit('dynamicKey',{ ...opts,v,frag,id,match,scene }) } } } diff --git a/src/3rd/js/three/index.js b/src/3rd/js/three/index.js index 8ffda4a..895b491 100644 --- a/src/3rd/js/three/index.js +++ b/src/3rd/js/three/index.js @@ -1,4 +1,4 @@ -xrf.frag = {} +xrf.frag = {dynamic:{}} xrf.model = {} xrf.mixers = [] @@ -24,7 +24,7 @@ xrf.patchRenderer = function(opts){ xrf.clock = new xrf.THREE.Clock() renderer.render = ((render) => function(scene,camera){ // update clock - let time = xrf.clock.getDelta() + let time = xrf.clock.delta = xrf.clock.getDelta() xrf.emit('render',{scene,camera,time,render}) // allow fragments to do something at renderframe render(scene,camera) xrf.emit('renderPost',{scene,camera,time,render,renderer}) // allow fragments to do something after renderframe diff --git a/src/3rd/js/three/util/interactive.js b/src/3rd/js/three/util/interactive.js index ac51118..381ccd8 100644 --- a/src/3rd/js/three/util/interactive.js +++ b/src/3rd/js/three/util/interactive.js @@ -30,6 +30,12 @@ xrf.interactiveGroup = function(THREE,renderer,camera){ const raycaster = new Raycaster(); const tempMatrix = new Matrix4(); + let dispatchEvent = (object,_event) => { + object.dispatchEvent(_event) + // bubble up + object.traverseAncestors( (n) => n.userData && n.userData.href && n.dispatchEvent(_event) ) + } + // Pointer Events const element = renderer.domElement; @@ -56,12 +62,12 @@ xrf.interactiveGroup = function(THREE,renderer,camera){ _event.type = event.type; _event.data.set( uv.x, 1 - uv.y ); - object.dispatchEvent( _event ); + dispatchEvent( object, _event ); }else{ if( object.selected ) { _event.type = 'mouseleave' - object.dispatchEvent(_event) + dispatchEvent( object, _event) } } @@ -106,12 +112,12 @@ xrf.interactiveGroup = function(THREE,renderer,camera){ _event.type = events[ event.type ]; _event.data.set( uv.x, 1 - uv.y ); - object.dispatchEvent( _event ); + dispatchEvent( object, _event ); }else{ if( object.selected ) { _event.type = 'mouseleave' - object.dispatchEvent(_event) + dispatchEvent( object, _event) } } diff --git a/src/3rd/js/three/xrf/#.js b/src/3rd/js/three/xrf/#.js index 01bb811..a940b99 100644 --- a/src/3rd/js/three/xrf/#.js +++ b/src/3rd/js/three/xrf/#.js @@ -1,12 +1,14 @@ +// this is called by navigator.js rather than by a URL e.g. + xrf.frag.defaultPredefinedViews = (opts) => { let {scene,model} = opts; scene.traverse( (n) => { if( n.userData && n.userData['#'] ){ let frag = xrf.URI.parse( n.userData['#'] ) if( !n.parent && document.location.hash.length < 2){ - xrf.navigator.to( n.userData['#'] ) // evaluate static XR fragments + xrf.navigator.to( n.userData['#'] ) // evaluate default XR fragments (global-level) }else{ - xrf.hashbus.pub( n.userData['#'] ) // evaluate static XR fragments + xrf.hashbus.pub( n.userData['#'], n ) // evaluate default XR fragments (node-level) } } }) diff --git a/src/3rd/js/three/xrf/dynamic/URIvars.js b/src/3rd/js/three/xrf/dynamic/URIvars.js new file mode 100644 index 0000000..23d854f --- /dev/null +++ b/src/3rd/js/three/xrf/dynamic/URIvars.js @@ -0,0 +1,56 @@ + +// this holds all the URI Template variables (https://www.rfc-editor.org/rfc/rfc6570) + +xrf.addEventListener('parseModel', (opts) => { + let {model,url,file} = opts + if( model.isSRC || opts.isSRC ) return // ignore SRC models + xrf.URI.vars = new Proxy({},{ + set(me,k,v){ me[k] = v }, + get(me,k ){ + if( k == '__object' ){ + let obj = {} + for( let i in xrf.URI.vars ) obj[i] = xrf.URI.vars[i]() + return obj + } + return me[k] + }, + }) + + model.scene.traverse( (n) => { + if( n.userData ){ + for( let i in n.userData ){ + if( i[0] == '#' || i.match(/^(href|src|tag)$/) ) continue // ignore XR Fragment aliases + xrf.URI.vars[i] = () => n.userData[i] + } + } + }) + +}) + + +xrf.addEventListener('dynamicKeyValue', (opts) => { + // select active camera if any + let {id,match,v} = opts + + if( !v.is( xrf.XRF.CUSTOMFRAG) ) return // only process custom frags from here + + // check if fragment is an objectname + if( match.length > 0 ){ + xrf.frag.dynamic.material(v,opts) + }else{ + if( !xrf.URI.vars[ v.string ] ) return // only assign to known values + xrf.URI.vars[ id ] = xrf.URI.vars[ v.string ] // update var + if( xrf.debug ) console.log(`URI.vars[${id}]='${v.string}'`) + + xrf.scene.traverse( (n) => { // reflect new changes + if( n.userData && n.userData.src && n.userData.srcTemplate ){ + let srcOldFragments = n.userData.src.replace(/.*#/,'') + let srcNewFragments = xrf.frag.src.expandURI( n ).replace(/.*#/,'') + if( srcOldFragments != srcNewFragments ){ + console.log(`URI.vars[${id}] => updating ${n.name}`) + let frag = xrf.hashbus.pub( srcNewFragments, n ) + } + } + }) + } +}) diff --git a/src/3rd/js/three/xrf/dynamic/camera.js b/src/3rd/js/three/xrf/dynamic/camera.js new file mode 100644 index 0000000..8f3502f --- /dev/null +++ b/src/3rd/js/three/xrf/dynamic/camera.js @@ -0,0 +1,14 @@ +// switch camera when multiple cameras for url #mycameraname + +xrf.addEventListener('dynamicKey', (opts) => { + // select active camera if any + let {id,match,v} = opts + match.map( (w) => { + w.nodes.map( (node) => { + if( node.isCamera ){ + console.log("switching camera to cam: "+node.name) + xrf.model.camera = node + } + }) + }) +}) diff --git a/src/3rd/js/three/xrf/dynamic/filter.js b/src/3rd/js/three/xrf/dynamic/filter.js index b1794e9..87b06f8 100644 --- a/src/3rd/js/three/xrf/dynamic/filter.js +++ b/src/3rd/js/three/xrf/dynamic/filter.js @@ -1,7 +1,3 @@ -/* - * TODO: refactor/fix this (queries are being refactored to filters) - */ - xrf.addEventListener('dynamicKey', (opts) => { let {scene,id,match,v} = opts diff --git a/src/3rd/js/three/xrf/dynamic/material.js b/src/3rd/js/three/xrf/dynamic/material.js new file mode 100644 index 0000000..9561f66 --- /dev/null +++ b/src/3rd/js/three/xrf/dynamic/material.js @@ -0,0 +1,35 @@ +xrf.frag.dynamic.material = function(v,opts){ + let {match} = opts + + setMaterial = (mesh,material,reset) => { + if( !mesh.materialOriginal ) mesh.materialOriginal = mesh.material + if( reset ) mesh.material = mesh.materialOriginal + else mesh.material = material + } + + // update material in case of [*]= + let material + xrf.scene.traverse( (n) => n.material && (n.material.name == v.string) && (material = n.material) ) + if( !material && !v.reset ) return // nothing to do + + if( material ) xrf.frag.dynamic.material.setMatch(match,material,v) +} + +xrf.frag.dynamic.material.setMatch = function(match,material,v){ + match.map( (m) => { + m.nodes.map( (n) => { + n.material = setMaterial( n, material, v.reset ) + if( v.filter.q.deep ) n.traverse( (c) => c.material && setMaterial( c, material, v.reset ) ) + }) + }) +} + +xrf.addEventListener('dynamicKey', (opts) => { + + let {v,match} = opts + + if( v.reset ){ + xrf.frag.dynamic.material.setMatch(match,null,v) + } + +}) diff --git a/src/3rd/js/three/xrf/href.js b/src/3rd/js/three/xrf/href.js index 27c6f66..193b97a 100644 --- a/src/3rd/js/three/xrf/href.js +++ b/src/3rd/js/three/xrf/href.js @@ -62,7 +62,7 @@ xrf.frag.href = function(v, opts){ xrf.interactive.objects.map( (o) => { let newState = o.name == mesh.name ? state : false if( o.material ){ - if( o.material.uniforms ) o.material.uniforms.selected.value = newState + if( o.material.uniforms && o.material.uniforms.selected ) o.material.uniforms.selected.value = newState //if( o.material.emissive ) o.material.emissive.r = o.material.emissive.g = o.material.emissive.b = newState ? 2.0 : 1.0 if( o.material.emissive ){ if( !o.material.emissive.original ) o.material.emissive.original = o.material.emissive.clone() diff --git a/src/3rd/js/three/xrf/s.js b/src/3rd/js/three/xrf/s.js new file mode 100644 index 0000000..b1a6dc8 --- /dev/null +++ b/src/3rd/js/three/xrf/s.js @@ -0,0 +1,11 @@ +xrf.frag.suv = function(v, opts){ + let { frag, mesh, model, camera, scene, renderer, THREE} = opts + + if( !mesh.geometry ) return // nothing to do here + + xrf.frag.uv.init(mesh) + mesh.suv.x = v.x + mesh.suv.y = v.y !== undefined ? v.y : v.x + mesh.suv.loop = v.loop === true ? true : false + xrf.frag.uv.scroll(mesh) +} diff --git a/src/3rd/js/three/xrf/src.js b/src/3rd/js/three/xrf/src.js index 5bb999f..c1d8723 100644 --- a/src/3rd/js/three/xrf/src.js +++ b/src/3rd/js/three/xrf/src.js @@ -4,17 +4,25 @@ xrf.frag.src = function(v, opts){ opts.embedded = v // indicate embedded XR fragment let { mesh, model, camera, scene, renderer, THREE, hashbus, frag} = opts - let url = v.string - let srcFrag = opts.srcFrag = xrfragment.URI.parse(url) - opts.isLocal = v.string[0] == '#' + let url = xrf.frag.src.expandURI( mesh, v.string ) + let srcFrag = opts.srcFrag = xrfragment.URI.parse(url) + opts.isLocal = v.string[0] == '#' opts.isPortal = xrf.frag.src.renderAsPortal(mesh) - opts.isSRC = true + opts.isSRC = true if(xrf.debug) console.log(`src.js: instancing ${opts.isLocal?'local':'remote'} object ${url}`) if( opts.isLocal ){ xrf.frag.src.localSRC(url,srcFrag,opts) // local }else xrf.frag.src.externalSRC(url,srcFrag,opts) // external file + + xrf.hashbus.pub( url.replace(/.*#/,''), mesh) // eval src-url fragments +} + +xrf.frag.src.expandURI = function(mesh,uri){ + if( uri ) mesh.userData.srcTemplate = uri + mesh.userData.src = xrf.URI.template( mesh.userData.srcTemplate, xrf.URI.vars.__object ) + return mesh.userData.src } xrf.frag.src.addModel = (model,url,frag,opts) => { @@ -32,7 +40,7 @@ xrf.frag.src.addModel = (model,url,frag,opts) => { }else{ xrf.frag.src.scale( scene, opts, url ) // scale scene mesh.add(scene) - xrf.emit('parseModel', {...opts, scene, model}) + xrf.emit('parseModel', {...opts, isSRC:true, scene, model}) } // flag everything isSRC & isXRF mesh.traverse( (n) => { n.isSRC = n.isXRF = n[ opts.isLocal ? 'isSRCLocal' : 'isSRCExternal' ] = true }) @@ -66,7 +74,9 @@ xrf.frag.src.externalSRC = (url,frag,opts) => { fetch(url, { method: 'HEAD' }) .then( (res) => { let mimetype = res.headers.get('Content-type') - if( url.replace(/#.*/,'').match(/\.(gltf|glb)$/) ) mimetype = 'gltf' + if( url.replace(/#.*/,'').match(/\.(gltf|glb)$/) ) mimetype = 'gltf' + if( url.replace(/#.*/,'').match(/\.(frag|fs|glsl)$/) ) mimetype = 'x-shader/x-fragment' + if( url.replace(/#.*/,'').match(/\.(vert|vs)$/) ) mimetype = 'x-shader/x-fragment' //if( url.match(/\.(fbx|stl|obj)$/) ) mimetype = opts = { ...opts, frag, mimetype } return xrf.frag.src.type[ mimetype ] ? xrf.frag.src.type[ mimetype ](url,opts) : xrf.frag.src.type.unknown(url,opts) @@ -98,7 +108,6 @@ xrf.frag.src.scale = function(scene, opts, url){ // remove invisible objects (hidden by selectors) which might corrupt boundingbox size-detection let cleanScene = scene.clone() - if( !cleanScene ) debugger let remove = [] const notVisible = (n) => !n.visible || (n.material && !n.material.visible) cleanScene.traverse( (n) => notVisible(n) && n.children.length == 0 && (remove.push(n)) ) diff --git a/src/3rd/js/three/xrf/src/audio.js b/src/3rd/js/three/xrf/src/audio.js index ac2d36d..9c21c76 100644 --- a/src/3rd/js/three/xrf/src/audio.js +++ b/src/3rd/js/three/xrf/src/audio.js @@ -23,7 +23,8 @@ let loadAudio = (mimetype) => function(url,opts){ let sound = isPositionalAudio ? new THREE.PositionalAudio( camera.listener) : new THREE.Audio( camera.listener ) - mesh.audio = {} + mesh.media = mesh.media || {} + mesh.media.audio = { play: () => mesh.media.audio.autoplay = true } audioLoader.load( url.replace(/#.*/,''), function( buffer ) { @@ -36,9 +37,11 @@ let loadAudio = (mimetype) => function(url,opts){ //sound.setDirectionalCone( 360, 360, 0.01 ); } - sound.playXRF = (t) => { - mesh.add(sound) + mesh.add(sound) + + sound.pub = (t) => { try{ + sound.t = t if( sound.isPlaying && t.y != undefined ) sound.stop() if( sound.isPlaying && t.y == undefined ) sound.pause() @@ -65,11 +68,11 @@ let loadAudio = (mimetype) => function(url,opts){ }catch(e){ console.warn(e) } } - // autoplay if user already requested play - let autoplay = mesh.audio && mesh.audio.autoplay - mesh.audio = sound + // autoplay if user already requested play (before the sound was loaded) + let autoplay = mesh.media.audio && mesh.media.audio.autoplay + mesh.media.audio = sound if( autoplay ){ - xrf.hashbus.pub(mesh.audio.autoplay) + xrf.hashbus.pub(mesh.media.audio.autoplay) } }); } diff --git a/src/3rd/js/three/xrf/src/glsl.js b/src/3rd/js/three/xrf/src/glsl.js new file mode 100644 index 0000000..885d755 --- /dev/null +++ b/src/3rd/js/three/xrf/src/glsl.js @@ -0,0 +1,66 @@ +/* + * extensions: .frag/.fs/.vs/.vert + */ + +xrf.frag.src.type['x-shader/x-fragment'] = function(url,opts){ + let {mesh,THREE} = opts + + let isFragmentShader = /\.(fs|frag|glsl)$/ + let isVertexShader = /\.(vs|vert)$/ + + let shaderReqs = [] + let shaderCode = {} + let shader = { + fragment: { code: '', url: url.match( isFragmentShader ) ? url : '' }, + vertex: { code: '', url: url.match( isVertexShader ) ? url : '' } + } + + var onShaderLoaded = ((args) => (type, status, code) => { + shader[type].status = status + shader[type].code = code + if( shader.fragment.code && shader.vertex.code ){ + + let oldMaterial = mesh.material + mesh.material = new THREE.RawShaderMaterial({ + uniforms: { + time: { value: 1.0 }, + resolution: { value: new THREE.Vector2(1.0,1.0) } + }, + // basic shaders include following common vars/funcs: https://github.com/mrdoob/three.js/blob/master/src/renderers/shaders/ShaderChunk/common.glsl.js + fragmentShader: shader.fragment.status == 200 ? shader.fragment.code : THREE.ShaderChunk.meshbasic_frag, + vertexShader: shader.vertex.status == 200 ? shader.vertex.code : THREE.ShaderChunk.meshbasic_vert, + + }); + + mesh.material.needsUpdate = true + mesh.needsUpdate = true + + mesh.onBeforeRender = () => { + if( !mesh.material || !mesh.material.uniforms ) return mesh.onBeforeRender = function(){} + mesh.material.uniforms.time.value = xrf.clock.elapsedTime + } + + } + + })({}) + + // sidecar-load vertex shader file + if( shader.fragment.url && !shader.vertex.url ){ + shader.vertex.url = shader.fragment.url.replace(/\.fs$/, '.vs') + .replace(/\.frag$/, '.vert') + } + + if( shader.fragment.url ){ + fetch(shader.fragment.url) + .then( (res) => res.text().then( (code) => onShaderLoaded('fragment',res.status,code) ) ) + } + + if( shader.vertex.url ){ + fetch(shader.vertex.url) + .then( (res) => res.text().then( (code) => onShaderLoaded('vertex',res.status,code) ) ) + } + +} + +xrf.frag.src.type['x-shader/x-vertex'] = xrf.frag.src.type['x-shader/x-fragment'] + diff --git a/src/3rd/js/three/xrf/src/non-euclidian.js b/src/3rd/js/three/xrf/src/non-euclidian.js index 930c291..b52383b 100644 --- a/src/3rd/js/three/xrf/src/non-euclidian.js +++ b/src/3rd/js/three/xrf/src/non-euclidian.js @@ -74,6 +74,9 @@ xrf.portalNonEuclidian = function(opts){ this.setupListeners = () => { + // below is a somewhat weird tapdance to render the portals **after** the scene + // is rendered (otherwise it messes up occlusion) + mesh.onAfterRender = function(renderer, scene, camera, geometry, material, group ){ mesh.portal.needUpdate = true } diff --git a/src/3rd/js/three/xrf/src/video.js b/src/3rd/js/three/xrf/src/video.js index 228d722..88fb52f 100644 --- a/src/3rd/js/three/xrf/src/video.js +++ b/src/3rd/js/three/xrf/src/video.js @@ -5,7 +5,9 @@ let loadVideo = (mimetype) => function(url,opts){ const THREE = xrf.THREE let frag = xrf.URI.parse( url ) - let video = mesh.video = document.createElement('video') + mesh.media = mesh.media || {} + + let video = mesh.media.video = document.createElement('video') video.setAttribute("crossOrigin","anonymous") video.setAttribute("playsinline",'') video.addEventListener('loadedmetadata', function(){ @@ -17,21 +19,23 @@ let loadVideo = (mimetype) => function(url,opts){ // set range video.addEventListener('timeupdate', function timeupdate() { if (video.t && video.t.y !== undefined && video.t.y > video.t.x && Math.abs(video.currentTime) >= video.t.y ){ - if( video.t.speed.length ) video.currentTime = video.t.x // speed means loop + if( video.looping ) video.currentTime = video.t.x // speed means loop else video.pause() } },false) }) video.src = url - video.playXRF = (t) => { + video.speed = 1.0 + video.looping = false + video.pub = (t) => { video.t = t video.pause() if( t.x !== undefined && t.x == t.y ) return // stop paused else{ video.currentTime = t.x video.time = t.x - video.playbackRate = Math.abs( t.speed.length ? t.speed[0] : 1.0 ) // html5 video does not support reverseplay :/ + video.playbackRate = Math.abs( video.speed ) // html5 video does not support reverseplay :/ video.play() } } diff --git a/src/3rd/js/three/xrf/suv.js b/src/3rd/js/three/xrf/suv.js new file mode 100644 index 0000000..b974bbc --- /dev/null +++ b/src/3rd/js/three/xrf/suv.js @@ -0,0 +1,11 @@ +xrf.frag.suv = function(v, opts){ + let { frag, mesh, model, camera, scene, renderer, THREE} = opts + + if( !mesh.geometry ) return // nothing to do here + + xrf.frag.uv.init(mesh) + mesh.suv.x = v.x + mesh.suv.y = v.y !== undefined ? v.y : v.x + mesh.suv.loop = v.loop === true ? true : false + mesh.onBeforeRender = xrf.frag.uv.scroll +} diff --git a/src/3rd/js/three/xrf/t.js b/src/3rd/js/three/xrf/t.js index 64f25b4..670c0e3 100644 --- a/src/3rd/js/three/xrf/t.js +++ b/src/3rd/js/three/xrf/t.js @@ -1,5 +1,15 @@ +// this is the global #t mediafragment handler (which affects the 3D animation) + xrf.frag.t = function(v, opts){ let { frag, mesh, model, camera, scene, renderer, THREE} = opts + + // handle object media players + if( mesh && mesh.media ){ + for( let i in mesh.media ) mesh.media[i].pub(v) + return + } + + // otherwise handle global 3D animations if( !model.mixer ) return if( !model.animations || model.animations[0] == undefined ){ console.warn('no animations found in model') @@ -11,10 +21,10 @@ xrf.frag.t = function(v, opts){ mixer.t = v // update speed - mixer.timeScale = mixer.loop.speed = v.x - mixer.loop.speedAbs = Math.abs(v.x) + mixer.timeScale = mixer.loop.speed || 1.0 + mixer.loop.speedAbs = Math.abs( mixer.timeScale ) - if( v.y != undefined || v.z != undefined ) mixer.updateLoop( v ) + mixer.updateLoop( v ) // play animations mixer.play( v ) @@ -31,7 +41,7 @@ xrf.addEventListener('parseModel', (opts) => { let {model} = opts let mixer = model.mixer = new xrf.THREE.AnimationMixer(model.scene) mixer.model = model - mixer.loop = {timeStart:0,timeStop:0} + mixer.loop = {timeStart:0,timeStop:0,speed:1.0} mixer.i = xrf.mixers.length mixer.actions = [] @@ -41,7 +51,6 @@ xrf.addEventListener('parseModel', (opts) => { mixer.actions.push( mixer.clipAction( anim, model.scene ) ) }) - mixer.checkZombies = (animations) => { if( mixer.zombieCheck ) return // fire only once animations.map( (anim) => { @@ -62,7 +71,7 @@ xrf.addEventListener('parseModel', (opts) => { } mixer.play = (t) => { - mixer.isPlaying = t.x != 0 + mixer.isPlaying = t.x !== undefined && t.x != t.y mixer.updateLoop(t) xrf.emit( mixer.isPlaying === false ? 'stop' : 'play',{isPlaying: mixer.isPlaying}) } @@ -72,17 +81,15 @@ xrf.addEventListener('parseModel', (opts) => { } mixer.updateLoop = (t) => { - mixer.loop.timeStart = t.y != undefined ? t.y : mixer.loop.timeStart - mixer.loop.timeStop = t.z != undefined ? t.z : mixer.loop.timeStop + mixer.loop.timeStart = t.x != undefined ? t.x : mixer.loop.timeStart + mixer.loop.timeStop = t.y != undefined ? t.y : mixer.loop.timeStop mixer.actions.map( (action) => { if( mixer.loop.timeStart != undefined ){ action.time = mixer.loop.timeStart action.setLoop( xrf.THREE.LoopOnce, ) action.timeScale = mixer.timeScale action.enabled = true - if( t.x != 0 ){ - action.play() - } + if( t.x === 0 ) action.play() } }) mixer.setTime(mixer.loop.timeStart) @@ -152,19 +159,6 @@ xrf.addEventListener('render', (opts) => { } }) -xrf.addEventListener('dynamicKey', (opts) => { - // select active camera if any - let {id,match,v} = opts - match.map( (w) => { - w.nodes.map( (node) => { - if( node.isCamera ){ - console.log("switching camera to cam: "+node.name) - xrf.model.camera = node - } - }) - }) -}) - // remove mixers and stop mixers when loading another scene xrf.addEventListener('reset', (opts) => { xrf.mixers.map( (m) => m.stop()) diff --git a/src/3rd/js/three/xrf/uv.js b/src/3rd/js/three/xrf/uv.js new file mode 100644 index 0000000..34bbcc5 --- /dev/null +++ b/src/3rd/js/three/xrf/uv.js @@ -0,0 +1,57 @@ +xrf.frag.uv = function(v, opts){ + let { frag, mesh, model, camera, scene, renderer, THREE} = opts + + if( !mesh.geometry ) return // nothing to do here + + xrf.frag.uv.init(mesh) + mesh.uv.x = v.x + mesh.uv.y = v.y !== undefined ? v.y : v.x + mesh.onBeforeRender = xrf.frag.uv.scroll +} + +xrf.frag.uv.init = function(mesh){ + if( !mesh.uv ) mesh.uv = {x:0, y:0, w:1, h:1, uv:false} + if( !mesh.suv ) mesh.suv = {x:1, y:1, loop:false } + let uv = mesh.geometry.getAttribute("uv") + if( !uv.old ) uv.old = uv.clone() +} + +xrf.frag.uv.scroll = function(){ + if( this.suv.x > 0.0 || this.suv.y > 0.0 ){ + + let diff = 0.0 // distance to end-state (non-looping mode) + let uv = this.geometry.getAttribute("uv") + + // translate! + for( let i = 0; i < uv.count; i++ ){ + let u = uv.getX(i) + let v = uv.getY(i) + let uTarget = uv.old.getX(i) + this.uv.x + let vTarget = uv.old.getY(i) + this.uv.y + + if( this.suv.loop ){ + u += this.suv.x * xrf.clock.delta + v += this.suv.y * xrf.clock.delta + }else{ + + // recover from super-high uv-values due to looped scrolling + if( Math.abs(u-uTarget) > 1.0 ) u = uv.old.getX(i) + if( Math.abs(v-vTarget) > 1.0 ) v = uv.old.getY(i) + + u = u > uTarget ? u + (this.suv.x * -xrf.clock.delta) + : u + (this.suv.x * xrf.clock.delta) + v = v > vTarget ? v + (this.suv.y * -xrf.clock.delta) + : v + (this.suv.y * xrf.clock.delta) + diff += Math.abs( u - uTarget ) // are we done yet? (non-looping mode) + diff += Math.abs( v - vTarget ) + + } + uv.setXY(i,u,v) + } + uv.needsUpdate = true + + if( !this.suv.loop && diff < 0.05 ){ // stop animating if done + this.onBeforeRender = function(){} + } + } +} diff --git a/src/3rd/js/three/xrf/xywh.js b/src/3rd/js/three/xrf/xywh.js new file mode 100644 index 0000000..8eb6635 --- /dev/null +++ b/src/3rd/js/three/xrf/xywh.js @@ -0,0 +1,9 @@ +xrf.frag.xywh = function(v, opts){ + let { frag, mesh, model, camera, scene, renderer, THREE} = opts + xrf.mediafragment.init(mesh) + mesh.xywh.x = v.floats[0] + mesh.xywh.y = v.floats[1] !== undefined ? v.floats[1] : mesh.xywh.x + mesh.xywh.w = v.floats[2] !== undefined ? v.floats[2] : mesh.xywh.y + mesh.xywh.h = v.floats[3] !== undefined ? v.floats[3] : mesh.xywh.w + // TODO: nondestructive cropping of texture (not superimportant now) +} diff --git a/src/Test.hx b/src/Test.hx index 82c2019..517466d 100644 --- a/src/Test.hx +++ b/src/Test.hx @@ -18,16 +18,17 @@ class Test { 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( "xywh.json", Spec.load("src/spec/xywh.json") ); + test( "xywh.json", Spec.load("src/spec/xywh.json") ); test( "s.json", Spec.load("src/spec/s.json") ); - test( "sxy.json", Spec.load("src/spec/sxy.json") ); + test( "suv.json", Spec.load("src/spec/suv.json") ); + test( "suv.json", Spec.load("src/spec/uv.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):Void { - trace("\n[.] running "+topic); + trace("\n[ . ] running "+topic); var Filter = xrfragment.Filter; for( i in 0...spec.length ){ var f:Filter = null; @@ -52,7 +53,7 @@ class Test { if( item.expect.fn == "equal.mediafragmentT" ) valid = equalMediaFragment(res,item,"t"); if( item.expect.fn == "equal.mediafragmentXYWH") valid = equalMediaFragment(res,item,"xywh"); if( item.expect.fn == "equal.mediafragmentS") valid = equalMediaFragment(res,item,"s"); - if( item.expect.fn == "equal.mediafragmentSXY") valid = equalMediaFragment(res,item,"sxy"); + if( item.expect.fn == "equal.mediafragmentSUV") valid = equalMediaFragment(res,item,"suv"); 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 ? "[ ✔ ] " : "[ ❌] "; diff --git a/src/spec/filter.selectors.json b/src/spec/filter.selectors.json index 7d6a253..baec658 100644 --- a/src/spec/filter.selectors.json +++ b/src/spec/filter.selectors.json @@ -10,6 +10,5 @@ {"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":"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"} + {"fn":"url","data":"#foo*", "expect":{ "fn":"testFilterDeep","input":["foo"],"out":1},"label":"foo should be deep"} ] diff --git a/src/spec/s.json b/src/spec/s.json index 31197f3..9ffa9b7 100644 --- a/src/spec/s.json +++ b/src/spec/s.json @@ -1,5 +1,5 @@ [ - {"fn":"url","data":"http://foo.com?foo=1#s=1", "expect":{ "fn":"equal.mediafragmentSpeed", "input":"0","out":"1"},"label":"playback speed"}, - {"fn":"url","data":"http://foo.com?foo=1#s=0.5", "expect":{ "fn":"equal.mediafragmentSpeed", "input":"0","out":"0.5"},"label":"playback speed"}, - {"fn":"url","data":"http://foo.com?foo=1#s=-0.5", "expect":{ "fn":"equal.mediafragmentSpeed", "input":"0","out":"-0.5"},"label":"playback speed"} + {"fn":"url","data":"http://foo.com?foo=1#s=1", "expect":{ "fn":"equal.x", "input":"s","out":"1"},"label":"playback speed"}, + {"fn":"url","data":"http://foo.com?foo=1#s=0.5", "expect":{ "fn":"equal.x", "input":"s","out":"0.5"},"label":"playback speed"}, + {"fn":"url","data":"http://foo.com?foo=1#s=-0.5", "expect":{ "fn":"equal.x", "input":"s","out":"-0.5"},"label":"playback speed"} ] diff --git a/src/spec/suv.json b/src/spec/suv.json index b9e4345..b99b461 100644 --- a/src/spec/suv.json +++ b/src/spec/suv.json @@ -1,4 +1,4 @@ [ - {"fn":"url","data":"http://foo.com?foo=1#sxy=l:0,0.1", "expect":{ "fn":"equal.mediafragmentSXY", "input":"1","out":"0.2"},"label":"sxy"}, - {"fn":"url","data":"http://foo.com?foo=1#sxy=0,0.1", "expect":{ "fn":"equal.mediafragmentSXY", "input":"1","out":"0.2"},"label":"sxy looped"} + {"fn":"url","data":"http://foo.com?foo=1#suv=l:0,0.1", "expect":{ "fn":"equal.mediafragmentSUV", "input":"1","out":"0.1"},"label":"suv"}, + {"fn":"url","data":"http://foo.com?foo=1#suv=0.2,0.1", "expect":{ "fn":"equal.mediafragmentSUV", "input":"0","out":"0.2"},"label":"suv looped"} ] diff --git a/src/spec/t.json b/src/spec/t.json index 02bdc7a..5569074 100644 --- a/src/spec/t.json +++ b/src/spec/t.json @@ -8,8 +8,5 @@ {"fn":"url","data":"http://foo.com?foo=1#t=1,100", "expect":{ "fn":"equal.xy", "input":"t","out":"1,100"},"label":"a equal.xy"}, {"fn":"url","data":"http://foo.com?foo=1#t=2,500", "expect":{ "fn":"testBrowserOverride", "input":"t","out":true},"label":"browser URI can override t (defined in asset)"}, {"fn":"url","data":"http://foo.com?foo=1#t=1,100,400,500", "expect":{ "fn":"equal.mediafragmentT", "input":"3","out":"500"},"label":"a equal.mediafragment"}, - {"fn":"url","data":"http://foo.com?foo=1#t=l:1,100,400,500", "expect":{ "fn":"equal.mediafragmentT", "input":"3","out":"500"},"label":"a equal.mediafragment loop"}, - {"fn":"url","data":"http://foo.com?foo=1#t=v:l:1,100,400,500", "expect":{ "fn":"equal.mediafragmentT", "input":"3","out":"500"},"label":"a equal.mediafragment uv loop "}, - {"fn":"url","data":"http://foo.com?foo=1#t=v:1,100,400,500", "expect":{ "fn":"equal.mediafragmentT", "input":"3","out":"500"},"label":"a equal.mediafragment uv"}, - {"fn":"url","data":"http://foo.com?foo=1#t=v:1,2", "expect":{ "fn":"testParsed", "input":"mycustom","out":true},"label":"test uv is set"} + {"fn":"url","data":"http://foo.com?foo=1#t=l:1,100,400,500", "expect":{ "fn":"equal.mediafragmentT", "input":"3","out":"500"},"label":"a equal.mediafragment loop"} ] diff --git a/src/spec/uv.json b/src/spec/uv.json new file mode 100644 index 0000000..838f5c0 --- /dev/null +++ b/src/spec/uv.json @@ -0,0 +1,3 @@ +[ + {"fn":"url","data":"http://foo.com?foo=1#uv=1.2,2.2", "expect":{ "fn":"equal.xy", "input":"uv","out":"1.2,2.2"},"label":"equal.string uv"} +] diff --git a/src/xrfragment/Parser.hx b/src/xrfragment/Parser.hx index 965470f..482b098 100644 --- a/src/xrfragment/Parser.hx +++ b/src/xrfragment/Parser.hx @@ -15,27 +15,27 @@ class Parser { // here we define allowed characteristics & datatypes for each fragment (stored as bitmasked int for performance purposes) var Frag:Map = new Map(); - Frag.set("#", XRF.ASSET | XRF.T_PREDEFINED_VIEW | XRF.PV_EXECUTE ); - Frag.set("src", XRF.ASSET | XRF.T_URL ); - Frag.set("href", XRF.ASSET | XRF.T_URL | XRF.T_PREDEFINED_VIEW ); - Frag.set("tag", XRF.ASSET | XRF.T_STRING ); + Frag.set("#", XRF.IMMUTABLE | XRF.T_PREDEFINED_VIEW | XRF.PV_EXECUTE ); + Frag.set("src", XRF.T_URL ); + Frag.set("href", XRF.T_URL | XRF.T_PREDEFINED_VIEW ); + Frag.set("tag", XRF.IMMUTABLE | XRF.T_STRING ); // spatial category: query selector / object manipulation Frag.set("pos", XRF.PV_OVERRIDE | XRF.T_VECTOR3 | XRF.T_STRING | XRF.METADATA | XRF.NAVIGATOR ); Frag.set("rot", XRF.QUERY_OPERATOR | XRF.PV_OVERRIDE | XRF.T_VECTOR3 | XRF.METADATA | XRF.NAVIGATOR ); // category: media fragments - Frag.set("t", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_FLOAT | XRF.T_VECTOR2 | XRF.T_MEDIAFRAG | XRF.NAVIGATOR | XRF.METADATA); - Frag.set("xywh", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_FLOAT | XRF.T_VECTOR2 | XRF.T_MEDIAFRAG | XRF.NAVIGATOR | XRF.METADATA); + Frag.set("t", XRF.PV_OVERRIDE | XRF.T_FLOAT | XRF.T_VECTOR2 | XRF.T_MEDIAFRAG | XRF.NAVIGATOR | XRF.METADATA); + Frag.set("xywh", XRF.T_FLOAT | XRF.T_VECTOR2 | XRF.T_MEDIAFRAG | XRF.NAVIGATOR | XRF.METADATA); + Frag.set("s", XRF.PV_OVERRIDE | XRF.T_MEDIAFRAG | XRF.T_FLOAT ); + Frag.set("uv", XRF.T_VECTOR2 | XRF.T_MEDIAFRAG ); + Frag.set("suv", XRF.T_VECTOR2 | XRF.T_MEDIAFRAG ); // category: author / metadata - Frag.set("namespace", XRF.ASSET | XRF.T_STRING ); - Frag.set("SPDX", XRF.ASSET | XRF.T_STRING ); - Frag.set("unit", XRF.ASSET | XRF.T_STRING ); - Frag.set("description", XRF.ASSET | XRF.T_STRING ); - - // category: multiparty - Frag.set("session", XRF.ASSET | XRF.T_URL | XRF.PV_OVERRIDE | XRF.NAVIGATOR | XRF.METADATA | XRF.PROMPT ); + Frag.set("namespace", XRF.IMMUTABLE | XRF.T_STRING ); + Frag.set("SPDX", XRF.IMMUTABLE | XRF.T_STRING ); + Frag.set("unit", XRF.IMMUTABLE | XRF.T_STRING ); + Frag.set("description", XRF.IMMUTABLE | XRF.T_STRING ); /** * # Spec @@ -51,11 +51,12 @@ class Parser { // 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) + if( isPVDynamic ){ // 1. add key(values) to store as [predefined view](predefined_view) or dynamic assignment var v:XRF = new XRF(key, XRF.PV_EXECUTE | XRF.NAVIGATOR, index ); v.validate(value); // ignore failures (empty values are allowed) - v.flags = XRF.set( XRF.T_DYNAMIC, v.flags ); + v.flags = XRF.set( XRF.T_DYNAMICKEY, v.flags ); + if( !Frag.exists(key) ) v.flags = XRF.set( XRF.CUSTOMFRAG, v.flags ); + if( value.length == 0 ) v.flags = XRF.set( XRF.T_DYNAMICKEYVALUE, v.flags ); store.set( keyStripped, v ); return true; } @@ -67,11 +68,11 @@ class Parser { trace("⚠ fragment '"+key+"' has incompatible value ("+value+")");// 1. don't add to store if value-type is incorrect return false; } - store.set( keyStripped, 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; + v.flags = XRF.set( XRF.CUSTOMFRAG, v.flags ); store.set( keyStripped ,v); } return true; diff --git a/src/xrfragment/URI.hx b/src/xrfragment/URI.hx index 78aedfe..f763925 100644 --- a/src/xrfragment/URI.hx +++ b/src/xrfragment/URI.hx @@ -52,6 +52,19 @@ class URI { } return store; } + + @keep + public static function template(uri:String, vars:Dynamic):String { + var parts = uri.split("#"); + if( parts.length == 1 ) return uri; // we only do level1 fragment expansion + var frag = parts[1]; + frag = StringTools.replace(frag,"{","::"); + frag = StringTools.replace(frag,"}","::"); + frag = new haxe.Template(frag).execute(vars); + frag = StringTools.replace(frag,"null",""); // *TODO* needs regex to check for [#&]null[&] + parts[1] = frag; + return parts.join("#"); + } } /** diff --git a/src/xrfragment/XRF.hx b/src/xrfragment/XRF.hx index 07b16f3..cd76e8b 100644 --- a/src/xrfragment/XRF.hx +++ b/src/xrfragment/XRF.hx @@ -10,14 +10,14 @@ class XRF { * this class represents a fragment (value) */ - // public static inline readonly ASSET + // public static inline readonly IMMUTABLE // scope types (powers of 2) - public static var ASSET:Int = 1; // fragment is immutable + public static var IMMUTABLE: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 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 CUSTOMFRAG: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 public static var METADATA:Int = 64; // fragment can be overridden by an embedded URL public static var PV_OVERRIDE:Int = 128; // embedded fragment can be overridden when specified in predefined view value @@ -33,7 +33,8 @@ class XRF { public static var T_PREDEFINED_VIEW:Int = 524288; public static var T_STRING:Int = 1048576; public static var T_MEDIAFRAG:Int = 2097152; - public static var T_DYNAMIC:Int = 4194304; + public static var T_DYNAMICKEY:Int = 4194304; + public static var T_DYNAMICKEYVALUE:Int = 8388608; // 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})$/` @@ -43,12 +44,13 @@ class XRF { 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 operators:EReg = ~/(^-|[\*]+)/; // 1. detect operators so you can easily strip keys (reference regex= `~/(^-)?(\/)?(\*)?/` ) + public static var operators:EReg = ~/(^[-]|^[!]|[\*]$)/g; // 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\.]+$/` ) - public static var isMediaFrag:EReg = ~/^(uv:)?(l:)?([0-9\.,\*]+)$/; // 1. detect (extended) media fragment + public static var isMediaFrag:EReg = ~/^(l:)?([0-9\.,\*]+)$/; // 1. detect (extended) media fragment + public static var isReset:EReg = ~/^!/; // 1. detect reset operation // value holder(s) // |------|------|--------|----------------------------------| public var fragment:String; @@ -58,15 +60,13 @@ class XRF { public var y:Float; public var z:Float; public var floats:Array = new Array(); - public var speed:Array = new Array(); public var color:String; // |string| color| FFFFFF (hex) | #fog=5m,FFAACC | 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 filter:Filter; - public var noXRF:Bool; + public var reset:Bool; public var loop:Bool; - public var uv:Bool; // public function new(_fragment:String,_flags:Int,?_index:Int){ fragment = _fragment; @@ -99,16 +99,13 @@ class XRF { @:keep public function guessType(v:XRF, str:String):Void { v.string = str; + if( isReset.match(v.fragment) ) v.reset = true; if( !Std.isOfType(str,String) ) return; if( str.length > 0 ){ if( str.split("l:").length > 1 ){ str = str.split("l:")[1]; v.loop = true; } - if( str.split("uv:").length > 1 ){ - str = str.split("uv:")[1]; - v.uv = true; - } if( str.split(",").length > 1){ // 1. `,` assumes 1D/2D/3D vector-values like x[,y[,z]] var xyzn:Array = str.split(","); // 1. parseFloat(..) and parseInt(..) is applied to vector/float and int values if( xyzn.length > 0 ) v.x = Std.parseFloat(xyzn[0]); // 1. anything else will be treated as string-value @@ -127,6 +124,7 @@ class XRF { if( isInt.match(str) ){ v.int = Std.parseInt(str); v.x = cast(v.int); + v.floats.push( cast(v.x) ); } v.filter = new Filter(v.fragment+"="+v.string); }else v.filter = new Filter(v.fragment);