diff --git a/doc/RFC_XR_Fragments.md b/doc/RFC_XR_Fragments.md index 71f02a1..cf15481 100644 --- a/doc/RFC_XR_Fragments.md +++ b/doc/RFC_XR_Fragments.md @@ -259,6 +259,7 @@ These are automatic fragment-to-metadata mappings, which only trigger if the 3D | **MATERIALUPDATE** | `#[*]=` | string=string | `#car=metallic`| sets material of car to material with name `metallic` (`*`=including children)| | | | | `#soldout*=halfopacity`| set material of objects tagged with `product` to material with name `metallic` | | **VARIABLE UPDATE** | `#=` | string=string | `#foo=bar` | sets [URI Template](https://www.rfc-editor.org/rfc/rfc6570) variable `foo` to the value `#t=0` from **existing** object metadata (`bar`:`#t=0` e.g.), This allows for reactive [URI Template](https://www.rfc-editor.org/rfc/rfc6570) defined in object metadata elsewhere (`src`:`://m.com/cat.mp4#{foo}` e.g., to play media using [media fragment URI](https://www.w3.org/TR/media-frags/#valid-uri)). NOTE: metadata-key should not start with `#` | +| **ANIMATION** | `#=` | string=string | `#people=walk` `#people=noanim` | assign a different animation to object(s) | ## media fragments and datatypes @@ -364,7 +365,7 @@ For example, to render a portal with a preview-version of the scene, create an 3 1. the Y-coordinate of `pos` identifies the floorposition. This means that desktop-projections usually need to add 1.5m (average person height) on top (which is done automatically by VR/AR headsets). 1. set the position of the camera accordingly to the vector3 values of `#pos` 1. `rot` sets the rotation of the camera (only for non-VR/AR headsets) -1. `t` sets the playbackspeed and animation-range of the current scene animation(s) or `src`-mediacontent (video/audioframes e.g., use `t=0,7,7` to 'STOP' at frame 7 e.g.) +1. `t` in the top-URL sets the playbackspeed and animation-range of the global scene animation 1. after scene load: in case an `href` does not mention any `pos`-coordinate, `pos=0,0,0` will be assumed Here's an ascii representation of a 3D scene-graph which contains 3D objects `◻` and their metadata: @@ -488,6 +489,8 @@ navigation, portals & mutations 10. href-events should bubble upward the node-tree +11. the end-user navigator back/forward buttons should repeat a back/forward action until a `pos=...` primitive is found (the inbetween interaction URI's are only for UX research purposes) + [» example implementation](https://github.com/coderofsalvation/xrfragment/blob/main/src/3rd/js/three/xrf/href.js)
[» example 3D asset](https://github.com/coderofsalvation/xrfragment/blob/main/example/assets/href.gltf#L192)
[» discussion](https://github.com/coderofsalvation/xrfragment/issues/1)
diff --git a/example/aframe/sandbox/index.html b/example/aframe/sandbox/index.html index d29c367..beb9e9a 100644 --- a/example/aframe/sandbox/index.html +++ b/example/aframe/sandbox/index.html @@ -11,7 +11,7 @@ - + @@ -19,7 +19,7 @@ - + diff --git a/example/assets/index.glb b/example/assets/index.glb index e3dc0a9..a1b58b6 100644 Binary files a/example/assets/index.glb and b/example/assets/index.glb differ diff --git a/src/3rd/js/XRWG.js b/src/3rd/js/XRWG.js index 2973434..ca7f041 100644 --- a/src/3rd/js/XRWG.js +++ b/src/3rd/js/XRWG.js @@ -69,3 +69,17 @@ XRWG.generate = (opts) => { XRWG = XRWG.reverse() // the cleankey/get functions e.g. will persist xrf.emit('XRWG',XRWG) } + +XRWG.deepApplyMatch = function(match,v,cb){ + match.map( (m) => { + for( let i in m.types ){ + let type = m.types[i] + let node = m.nodes[i] + if (type == 'name' || type == 'tag'){ + cb(match,v,node,type) + if( v.filter.q.deep ) node.traverse( (c) => cb(match,v,c,t) ) + } + } + }) +} + diff --git a/src/3rd/js/three/navigator.js b/src/3rd/js/three/navigator.js index 29caebb..c9a2ead 100644 --- a/src/3rd/js/three/navigator.js +++ b/src/3rd/js/three/navigator.js @@ -96,9 +96,7 @@ xrf.navigator.init = () => { window.addEventListener('popstate', function (event){ if( !xrf.navigator.updateHash.active ){ // ignore programmatic hash updates (causes infinite recursion) - if( !document.location.hash.match(/pos=/) ){ - history.back() // go back until we find a position - }else xrf.navigator.to( document.location.search.substr(1) + document.location.hash ) + xrf.navigator.to( document.location.search.substr(1) + document.location.hash ) } }) @@ -155,9 +153,6 @@ xrf.navigator.updateHash = (hash,opts) => { xrf.navigator.pushState = (file,hash) => { if( file == document.location.search.substr(1) ) return // page is in its default state - if( !hash.match(/pos=/) ){ - history.forward() // go forward until we find a position - } window.history.pushState({},`${file}#${hash}`, document.location.pathname + `?${file}#${hash}` ) xrf.emit('pushState', {file, hash} ) } diff --git a/src/3rd/js/three/xrf/dynamic/URIvars.js b/src/3rd/js/three/xrf/dynamic/URIvars.js index 6777c96..7c842c3 100644 --- a/src/3rd/js/three/xrf/dynamic/URIvars.js +++ b/src/3rd/js/three/xrf/dynamic/URIvars.js @@ -39,21 +39,30 @@ xrf.addEventListener('dynamicKeyValue', (opts) => { if( !v.is( xrf.XRF.CUSTOMFRAG) ) return // only process custom frags from here if( v.string.match(/(<|>)/) ) return // ignore filter values - // check if fragment is an objectname if( match.length > 0 ){ - xrf.frag.dynamic.material(v,opts) + xrf.frag.dynamic.material(v,opts) // check if fragment is an objectname } - if( !xrf.URI.vars[ v.string ] ) return console.warn(`'${v.string}' metadata not found in scene`) // only assign to known values + if( !xrf.URI.vars[ v.string ] ) return console.error(`'${v.string}' metadata-key not found in scene`) + if( xrf.URI.vars[ id ] && !match.length ) return console.error(`'${id}' object/tag/metadata-key not found in scene`) - 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 && n.userData.srcTemplate.match(`{${id}}`) ){ - let srcNewFragments = xrf.frag.src.expandURI( n ).replace(/.*#/,'') - console.log(`URI.vars[${id}] => updating ${n.name} => ${srcNewFragments}`) - let frag = xrf.hashbus.pub( srcNewFragments, n ) - } - }) + if( xrf.URI.vars[id] ){ + xrf.URI.vars[ id ] = xrf.URI.vars[ v.string ] // update var + xrf.scene.traverse( (n) => { + // re-expand src-values which use the updated URI Template var + if( n.userData && n.userData.src && n.userData.srcTemplate && n.userData.srcTemplate.match(`{${id}}`) ){ + let srcNewFragments = xrf.frag.src.expandURI( n ).replace(/.*#/,'') + console.log(`URI.vars[${id}] => updating ${n.name} => ${srcNewFragments}`) + let frag = xrf.hashbus.pub( srcNewFragments, n ) + } + }) + }else{ + xrf.XRWG.deepApplyMatch(match, v, (match,v,node,type) => { + console.log(v.string) + if( node.geometry ) xrf.hashbus.pub( xrf.URI.vars[ v.string ](), node) // apply fragment mesh(es) + }) + } + }) diff --git a/src/3rd/js/three/xrf/dynamic/material.js b/src/3rd/js/three/xrf/dynamic/material.js index f603575..40dfc9f 100644 --- a/src/3rd/js/three/xrf/dynamic/material.js +++ b/src/3rd/js/three/xrf/dynamic/material.js @@ -6,10 +6,12 @@ xrf.frag.dynamic.material = function(v,opts){ xrf.scene.traverse( (n) => n.material && (n.material.name == v.string) && (material = n.material) ) if( !material && !v.reset ) return // nothing to do - xrf.frag.dynamic.material.setMatch(match,material,v) + xrf.XRWG.deepApplyMatch(match, v, (match,v,node,type) => { + if( node.material ) xrf.frag.dynamic.material.set( node, material, v.reset ) + }) } -xrf.frag.dynamic.material.setMaterial = function(mesh,material,reset){ +xrf.frag.dynamic.material.set = function(mesh,material,reset){ if( !mesh.materialOriginal ) mesh.materialOriginal = mesh.material let visible = mesh.material.visible //remember if( reset ){ @@ -18,27 +20,15 @@ xrf.frag.dynamic.material.setMaterial = function(mesh,material,reset){ mesh.material.visible = visible } -xrf.frag.dynamic.material.setMatch = function(match,material,v){ - const setMaterial = xrf.frag.dynamic.material.setMaterial - - match.map( (m) => { - for( let i in m.types ){ - let type = m.types[i] - let node = m.nodes[i] - if (type == 'name' || type == 'tag'){ - setMaterial( node, material, v.reset ) - if( v.filter.q.deep ) node.traverse( (c) => c.material && setMaterial( c, material, v.reset ) ) - } - } - }) -} - +// for reset calls like href: xrf://!myobject e.g. xrf.addEventListener('dynamicKey', (opts) => { let {v,match} = opts if( v.reset ){ - xrf.frag.dynamic.material.setMatch(match,null,v) + xrf.XRWG.deepApplyMatch(match,v, (match,v,node,type) => { + if( node.material ) xrf.frag.dynamic.material.set( node, null, v.reset ) + }) } }) diff --git a/src/3rd/js/three/xrf/href.js b/src/3rd/js/three/xrf/href.js index 3374646..a4b7c53 100644 --- a/src/3rd/js/three/xrf/href.js +++ b/src/3rd/js/three/xrf/href.js @@ -35,6 +35,8 @@ xrf.frag.href = function(v, opts){ let click = mesh.userData.XRF.href.exec = (e) => { + if( !mesh.material.visible ) return + // bubble up! mesh.traverseAncestors( (n) => n.userData && n.userData.href && n.dispatchEvent({type:e.type,data:{}}) ) @@ -48,8 +50,10 @@ xrf.frag.href = function(v, opts){ const hasPos = isLocal && v.string.match(/pos=/) const flags = isLocal ? xrf.XRF.PV_OVERRIDE : undefined - let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA ) - xrf.navigator.to(v.string) // let's surf + //let toFrag = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA ) + if( v.xrfScheme ){ + xrf.hashbus.pub(v.string) + } else xrf.navigator.to(v.string) // let's surf }) .catch( console.error ) } diff --git a/src/3rd/js/three/xrf/uv.js b/src/3rd/js/three/xrf/uv.js index 380362a..25eab18 100644 --- a/src/3rd/js/three/xrf/uv.js +++ b/src/3rd/js/three/xrf/uv.js @@ -2,70 +2,71 @@ xrf.frag.uv = function(v, opts){ let { frag, mesh, model, camera, scene, renderer, THREE} = opts if( !mesh.geometry ) return // nothing to do here - if( v.floats.length != 4 ) return + if( v.floats.length < 2 ) return console.warn('xrfragment.js: got less than 4 uv values ') xrf.frag.uv.init(mesh) mesh.uv.u = v.floats[0] mesh.uv.v = v.floats[1] - mesh.uv.uspeed = v.floats[2] - mesh.uv.vspeed = v.floats[3] - mesh.uv.uloop = v.shift[2] - mesh.uv.vloop = v.shift[3] + mesh.uv.uspeed = v.floats[2] || 1.0 + mesh.uv.vspeed = v.floats[3] || 1.0 + mesh.uv.ushift = v.shift[0] + mesh.uv.vshift = v.shift[1] + mesh.uv.uloop = v.shift[2] || false + mesh.uv.vloop = v.shift[3] || false + debugger mesh.onBeforeRender = xrf.frag.uv.scroll } xrf.frag.uv.init = function(mesh){ - if( !mesh.uv ) mesh.uv = {u:0, v:0, uspeed:1, vspeed:1, uloop:false, vloop:false, uv:false} + if( !mesh.uv ) mesh.uv = {u:0, v:0, uspeed:1, vspeed:1, uloop:false, vloop:false, uv:false, ushift:false,vshift:false} let uv = mesh.geometry.getAttribute("uv") if( !uv.old ) uv.old = mesh.geometry.getAttribute("uv").clone() } xrf.frag.uv.scroll = function(){ - if( this.uv.uspeed > 0.0 || this.uv.vspeed > 0.0 ){ - let diffU = 0.0 // distance to end-state (non-looping mode) - let diffV = 0.0 // distance to end-state (non-looping mode) - let uv = this.geometry.getAttribute("uv") + let diffU = 0.0 // distance to end-state (non-looping mode) + let diffV = 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.u - let vTarget = uv.old.getY(i) + this.uv.v + // translate! + for( let i = 0; i < uv.count; i++ ){ + let u = uv.getX(i) + let v = uv.getY(i) + let uTarget = (this.uv.ushift ? u : uv.old.getX(i) ) + this.uv.u + let vTarget = (this.uv.vshift ? v : uv.old.getY(i) ) + this.uv.v - // scroll U - if( this.uv.uloop ){ - u += this.uv.uspeed * 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) - u = u > uTarget ? u + (this.uv.uspeed * -xrf.clock.delta) - : u + (this.uv.uspeed * xrf.clock.delta) - diffU += Math.abs( u - uTarget ) // are we done yet? (non-looping mode) - } + // scroll U + if( this.uv.uloop ){ + u += this.uv.uspeed * 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) + u = u > uTarget ? u + (this.uv.uspeed * -uTarget ) // -xrf.clock.delta) + : u + (this.uv.uspeed * uTarget ) // xrf.clock.delta) + diffU += Math.abs( u - uTarget ) // are we done yet? (non-looping mode) + } - // scroll V - if( this.uv.vloop ){ - v += this.uv.vspeed * xrf.clock.delta - }else{ - // recover from super-high uv-values due to looped scrolling - if( Math.abs(v-vTarget) > 1.0 ) v = uv.old.getY(i) - v = v > vTarget ? v + (this.uv.vspeed * -xrf.clock.delta) - : v + (this.uv.vspeed * xrf.clock.delta) - diffV += Math.abs( v - vTarget ) + // scroll V + if( this.uv.vloop ){ + v += this.uv.vspeed * xrf.clock.delta + }else{ + // recover from super-high uv-values due to looped scrolling + if( Math.abs(v-vTarget) > 1.0 ) v = uv.old.getY(i) + v = v > vTarget ? v + (this.uv.vspeed * -vTarget ) // -xrf.clock.delta) + : v + (this.uv.vspeed * vTarget ) // xrf.clock.delta) + diffV += Math.abs( v - vTarget ) - } - uv.setXY(i,u,v) } - uv.needsUpdate = true + uv.setXY(i,u,v) + } + uv.needsUpdate = true - if( (!this.uv.uloop && diffU < 0.05) && - (!this.uv.vloop && diffV < 0.05) - ){ // stop animating if done - this.onBeforeRender = function(){} - } + if( (!this.uv.uloop && diffU < 0.05) && + (!this.uv.vloop && diffV < 0.05) + ){ // stop animating if done + this.onBeforeRender = function(){} } } diff --git a/src/xrfragment/URI.hx b/src/xrfragment/URI.hx index 6bab93b..f14d3e7 100644 --- a/src/xrfragment/URI.hx +++ b/src/xrfragment/URI.hx @@ -13,9 +13,10 @@ import xrfragment.XRF; * ### XR Fragment URI Grammar * * ``` - * reserved = gen-delims / sub-delims + * reserved = gen-delims / sub-delims / xrf-scheme * gen-delims = "#" / "&" * sub-delims = "," / "=" + * xrf-scheme = "xrf://" * ``` * * 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: @@ -66,6 +67,7 @@ class URI { parts[1] = frag; return parts.join("#"); } + } /** diff --git a/src/xrfragment/XRF.hx b/src/xrfragment/XRF.hx index eb425df..7494d6b 100644 --- a/src/xrfragment/XRF.hx +++ b/src/xrfragment/XRF.hx @@ -51,7 +51,8 @@ class XRF { public static var isNumber:EReg = ~/^[0-9\.]+$/; // 1. detect number values like `foo=1` (reference regex= `/^[0-9\.]+$/` ) public static var isMediaFrag:EReg = ~/^([0-9\.,\*+-]+)$/; // 1. detect (extended) media fragment public static var isReset:EReg = ~/^!/; // 1. detect reset operation - public static var isShift:EReg = ~/[+-]/; + public static var isShift:EReg = ~/^(\+|--)/; + public static var isXRFScheme = ~/^xrf:\/\//; // value holder(s) // |------|------|--------|----------------------------------| public var fragment:String; @@ -69,6 +70,7 @@ class XRF { public var filter:Filter; public var reset:Bool; public var loop:Bool; + public var xrfScheme:Bool; // public function new(_fragment:String,_flags:Int,?_index:Int){ fragment = _fragment; @@ -107,6 +109,12 @@ class XRF { if( str.length > 0 ){ + if( isXRFScheme.match(str) ){ + v.xrfScheme = true; + str = isXRFScheme.replace(str,""); + v.string = str; + } + 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 @@ -114,7 +122,7 @@ class XRF { if( xyzn.length > 2 ) v.z = Std.parseFloat(xyzn[2]); // for( i in 0...xyzn.length ){ v.shift.push( isShift.match(xyzn[i]) ); - v.floats.push( Std.parseFloat(xyzn[i]) ); + v.floats.push( Std.parseFloat( isShift.replace(xyzn[i],'') ) ); } } // > the xrfragment specification should stay simple enough // > for anyone to write a parser using either regexes or grammar/lexers