/* * v0.5.1 generated at Wed Jan 15 10:52:05 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:51:41 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:51:31 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:51:15 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:50:01 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:49:22 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:48:58 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:47:50 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:43:55 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:39:36 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:38:56 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:38:18 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:37:32 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:37:04 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:35:19 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:30:49 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:30:17 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:27:52 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:27:22 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:25:57 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:22:36 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:19:46 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:19:21 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:15:58 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:15:30 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Wed Jan 15 10:15:28 AM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 08:15:04 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 08:14:22 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 08:13:57 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 08:13:42 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 08:13:18 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 08:12:32 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 08:11:05 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 08:10:11 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 08:07:36 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 08:07:12 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 08:06:48 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 08:06:27 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 08:06:11 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 08:05:47 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:35:01 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:33:48 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:33:30 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:33:24 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:32:59 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:26:30 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:26:19 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:26:05 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:24:14 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:23:45 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:23:27 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:23:12 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:22:13 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:21:51 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:21:36 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:20:54 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:20:39 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:20:13 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:19:55 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:18:34 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:01:28 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 03:00:58 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:55:26 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:55:01 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:54:21 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:54:04 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:53:31 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:52:11 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:51:59 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:51:44 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:51:01 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:50:51 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:50:39 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:49:27 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:48:13 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:42:07 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:41:43 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:41:25 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:40:51 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:39:41 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:38:52 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:38:19 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:37:01 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:36:19 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:35:59 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:35:44 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:35:28 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:34:46 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:32:50 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:31:07 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:30:27 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Tue Jan 14 02:24:55 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ /* * v0.5.1 generated at Fri Jan 10 07:51:18 PM CET 2025 * https://xrfragment.org * SPDX-License-Identifier: AGPL-3.0-or-later */ // Generated by Haxe 4.3.6 var $hx_exports = typeof exports != "undefined" ? exports : typeof window != "undefined" ? window : typeof self != "undefined" ? self : this; (function ($global) { "use strict"; $hx_exports["xrfragment"] = $hx_exports["xrfragment"] || {}; var $estr = function() { return js_Boot.__string_rec(this,''); },$hxEnums = $hxEnums || {},$_; function $extend(from, fields) { var proto = Object.create(from); for (var name in fields) proto[name] = fields[name]; if( fields.toString !== Object.prototype.toString ) proto.toString = fields.toString; return proto; } var EReg = function(r,opt) { this.r = new RegExp(r,opt.split("u").join("")); }; EReg.__name__ = true; EReg.prototype = { match: function(s) { if(this.r.global) { this.r.lastIndex = 0; } this.r.m = this.r.exec(s); this.r.s = s; return this.r.m != null; } ,matched: function(n) { if(this.r.m != null && n >= 0 && n < this.r.m.length) { return this.r.m[n]; } else { throw haxe_Exception.thrown("EReg::matched"); } } ,matchedRight: function() { if(this.r.m == null) { throw haxe_Exception.thrown("No string matched"); } var sz = this.r.m.index + this.r.m[0].length; return HxOverrides.substr(this.r.s,sz,this.r.s.length - sz); } ,matchedPos: function() { if(this.r.m == null) { throw haxe_Exception.thrown("No string matched"); } return { pos : this.r.m.index, len : this.r.m[0].length}; } ,split: function(s) { var d = "#__delim__#"; return s.replace(this.r,d).split(d); } }; var HxOverrides = function() { }; HxOverrides.__name__ = true; HxOverrides.cca = function(s,index) { var x = s.charCodeAt(index); if(x != x) { return undefined; } return x; }; HxOverrides.substr = function(s,pos,len) { if(len == null) { len = s.length; } else if(len < 0) { if(pos == 0) { len = s.length + len; } else { return ""; } } return s.substr(pos,len); }; HxOverrides.now = function() { return Date.now(); }; Math.__name__ = true; var Reflect = function() { }; Reflect.__name__ = true; Reflect.field = function(o,field) { try { return o[field]; } catch( _g ) { return null; } }; Reflect.getProperty = function(o,field) { var tmp; if(o == null) { return null; } else { var tmp1; if(o.__properties__) { tmp = o.__properties__["get_" + field]; tmp1 = tmp; } else { tmp1 = false; } if(tmp1) { return o[tmp](); } else { return o[field]; } } }; Reflect.fields = function(o) { var a = []; if(o != null) { var hasOwnProperty = Object.prototype.hasOwnProperty; for( var f in o ) { if(f != "__id__" && f != "hx__closures__" && hasOwnProperty.call(o,f)) { a.push(f); } } } return a; }; Reflect.isObject = function(v) { if(v == null) { return false; } var t = typeof(v); if(!(t == "string" || t == "object" && v.__enum__ == null)) { if(t == "function") { return (v.__name__ || v.__ename__) != null; } else { return false; } } else { return true; } }; Reflect.deleteField = function(o,field) { if(!Object.prototype.hasOwnProperty.call(o,field)) { return false; } delete(o[field]); return true; }; Reflect.copy = function(o) { if(o == null) { return null; } var o2 = { }; var _g = 0; var _g1 = Reflect.fields(o); while(_g < _g1.length) { var f = _g1[_g]; ++_g; o2[f] = Reflect.field(o,f); } return o2; }; var Std = function() { }; Std.__name__ = true; Std.string = function(s) { return js_Boot.__string_rec(s,""); }; Std.parseInt = function(x) { var v = parseInt(x); if(isNaN(v)) { return null; } return v; }; var StringBuf = function() { this.b = ""; }; StringBuf.__name__ = true; var StringTools = function() { }; StringTools.__name__ = true; StringTools.isSpace = function(s,pos) { var c = HxOverrides.cca(s,pos); if(!(c > 8 && c < 14)) { return c == 32; } else { return true; } }; StringTools.ltrim = function(s) { var l = s.length; var r = 0; while(r < l && StringTools.isSpace(s,r)) ++r; if(r > 0) { return HxOverrides.substr(s,r,l - r); } else { return s; } }; StringTools.rtrim = function(s) { var l = s.length; var r = 0; while(r < l && StringTools.isSpace(s,l - r - 1)) ++r; if(r > 0) { return HxOverrides.substr(s,0,l - r); } else { return s; } }; StringTools.trim = function(s) { return StringTools.ltrim(StringTools.rtrim(s)); }; StringTools.replace = function(s,sub,by) { return s.split(sub).join(by); }; var haxe_Exception = function(message,previous,native) { Error.call(this,message); this.message = message; this.__previousException = previous; this.__nativeException = native != null ? native : this; }; haxe_Exception.__name__ = true; haxe_Exception.caught = function(value) { if(((value) instanceof haxe_Exception)) { return value; } else if(((value) instanceof Error)) { return new haxe_Exception(value.message,null,value); } else { return new haxe_ValueException(value,null,value); } }; haxe_Exception.thrown = function(value) { if(((value) instanceof haxe_Exception)) { return value.get_native(); } else if(((value) instanceof Error)) { return value; } else { var e = new haxe_ValueException(value); return e; } }; haxe_Exception.__super__ = Error; haxe_Exception.prototype = $extend(Error.prototype,{ unwrap: function() { return this.__nativeException; } ,get_native: function() { return this.__nativeException; } ,__properties__: {get_native:"get_native"} }); var haxe__$Template_TemplateExpr = $hxEnums["haxe._Template.TemplateExpr"] = { __ename__:true,__constructs__:null ,OpVar: ($_=function(v) { return {_hx_index:0,v:v,__enum__:"haxe._Template.TemplateExpr",toString:$estr}; },$_._hx_name="OpVar",$_.__params__ = ["v"],$_) ,OpExpr: ($_=function(expr) { return {_hx_index:1,expr:expr,__enum__:"haxe._Template.TemplateExpr",toString:$estr}; },$_._hx_name="OpExpr",$_.__params__ = ["expr"],$_) ,OpIf: ($_=function(expr,eif,eelse) { return {_hx_index:2,expr:expr,eif:eif,eelse:eelse,__enum__:"haxe._Template.TemplateExpr",toString:$estr}; },$_._hx_name="OpIf",$_.__params__ = ["expr","eif","eelse"],$_) ,OpStr: ($_=function(str) { return {_hx_index:3,str:str,__enum__:"haxe._Template.TemplateExpr",toString:$estr}; },$_._hx_name="OpStr",$_.__params__ = ["str"],$_) ,OpBlock: ($_=function(l) { return {_hx_index:4,l:l,__enum__:"haxe._Template.TemplateExpr",toString:$estr}; },$_._hx_name="OpBlock",$_.__params__ = ["l"],$_) ,OpForeach: ($_=function(expr,loop) { return {_hx_index:5,expr:expr,loop:loop,__enum__:"haxe._Template.TemplateExpr",toString:$estr}; },$_._hx_name="OpForeach",$_.__params__ = ["expr","loop"],$_) ,OpMacro: ($_=function(name,params) { return {_hx_index:6,name:name,params:params,__enum__:"haxe._Template.TemplateExpr",toString:$estr}; },$_._hx_name="OpMacro",$_.__params__ = ["name","params"],$_) }; haxe__$Template_TemplateExpr.__constructs__ = [haxe__$Template_TemplateExpr.OpVar,haxe__$Template_TemplateExpr.OpExpr,haxe__$Template_TemplateExpr.OpIf,haxe__$Template_TemplateExpr.OpStr,haxe__$Template_TemplateExpr.OpBlock,haxe__$Template_TemplateExpr.OpForeach,haxe__$Template_TemplateExpr.OpMacro]; var haxe_iterators_ArrayIterator = function(array) { this.current = 0; this.array = array; }; haxe_iterators_ArrayIterator.__name__ = true; haxe_iterators_ArrayIterator.prototype = { hasNext: function() { return this.current < this.array.length; } ,next: function() { return this.array[this.current++]; } }; var haxe_Template = function(str) { var tokens = this.parseTokens(str); this.expr = this.parseBlock(tokens); if(!tokens.isEmpty()) { throw haxe_Exception.thrown("Unexpected '" + Std.string(tokens.first().s) + "'"); } }; haxe_Template.__name__ = true; haxe_Template.prototype = { execute: function(context,macros) { this.macros = macros == null ? { } : macros; this.context = context; this.stack = new haxe_ds_List(); this.buf = new StringBuf(); this.run(this.expr); return this.buf.b; } ,resolve: function(v) { if(v == "__current__") { return this.context; } if(Reflect.isObject(this.context)) { var value = Reflect.getProperty(this.context,v); if(value != null || Object.prototype.hasOwnProperty.call(this.context,v)) { return value; } } var _g_head = this.stack.h; while(_g_head != null) { var val = _g_head.item; _g_head = _g_head.next; var ctx = val; var value = Reflect.getProperty(ctx,v); if(value != null || Object.prototype.hasOwnProperty.call(ctx,v)) { return value; } } return Reflect.field(haxe_Template.globals,v); } ,parseTokens: function(data) { var tokens = new haxe_ds_List(); while(haxe_Template.splitter.match(data)) { var p = haxe_Template.splitter.matchedPos(); if(p.pos > 0) { tokens.add({ p : HxOverrides.substr(data,0,p.pos), s : true, l : null}); } if(HxOverrides.cca(data,p.pos) == 58) { tokens.add({ p : HxOverrides.substr(data,p.pos + 2,p.len - 4), s : false, l : null}); data = haxe_Template.splitter.matchedRight(); continue; } var parp = p.pos + p.len; var npar = 1; var params = []; var part = ""; while(true) { var c = HxOverrides.cca(data,parp); ++parp; if(c == 40) { ++npar; } else if(c == 41) { --npar; if(npar <= 0) { break; } } else if(c == null) { throw haxe_Exception.thrown("Unclosed macro parenthesis"); } if(c == 44 && npar == 1) { params.push(part); part = ""; } else { part += String.fromCodePoint(c); } } params.push(part); tokens.add({ p : haxe_Template.splitter.matched(2), s : false, l : params}); data = HxOverrides.substr(data,parp,data.length - parp); } if(data.length > 0) { tokens.add({ p : data, s : true, l : null}); } return tokens; } ,parseBlock: function(tokens) { var l = new haxe_ds_List(); while(true) { var t = tokens.first(); if(t == null) { break; } if(!t.s && (t.p == "end" || t.p == "else" || HxOverrides.substr(t.p,0,7) == "elseif ")) { break; } l.add(this.parse(tokens)); } if(l.length == 1) { return l.first(); } return haxe__$Template_TemplateExpr.OpBlock(l); } ,parse: function(tokens) { var t = tokens.pop(); var p = t.p; if(t.s) { return haxe__$Template_TemplateExpr.OpStr(p); } if(t.l != null) { var pe = new haxe_ds_List(); var _g = 0; var _g1 = t.l; while(_g < _g1.length) { var p1 = _g1[_g]; ++_g; pe.add(this.parseBlock(this.parseTokens(p1))); } return haxe__$Template_TemplateExpr.OpMacro(p,pe); } var kwdEnd = function(kwd) { var pos = -1; var length = kwd.length; if(HxOverrides.substr(p,0,length) == kwd) { pos = length; var _g_offset = 0; var _g_s = HxOverrides.substr(p,length,null); while(_g_offset < _g_s.length) { var c = _g_s.charCodeAt(_g_offset++); if(c == 32) { ++pos; } else { break; } } } return pos; }; var pos = kwdEnd("if"); if(pos > 0) { p = HxOverrides.substr(p,pos,p.length - pos); var e = this.parseExpr(p); var eif = this.parseBlock(tokens); var t = tokens.first(); var eelse; if(t == null) { throw haxe_Exception.thrown("Unclosed 'if'"); } if(t.p == "end") { tokens.pop(); eelse = null; } else if(t.p == "else") { tokens.pop(); eelse = this.parseBlock(tokens); t = tokens.pop(); if(t == null || t.p != "end") { throw haxe_Exception.thrown("Unclosed 'else'"); } } else { t.p = HxOverrides.substr(t.p,4,t.p.length - 4); eelse = this.parse(tokens); } return haxe__$Template_TemplateExpr.OpIf(e,eif,eelse); } var pos = kwdEnd("foreach"); if(pos >= 0) { p = HxOverrides.substr(p,pos,p.length - pos); var e = this.parseExpr(p); var efor = this.parseBlock(tokens); var t = tokens.pop(); if(t == null || t.p != "end") { throw haxe_Exception.thrown("Unclosed 'foreach'"); } return haxe__$Template_TemplateExpr.OpForeach(e,efor); } if(haxe_Template.expr_splitter.match(p)) { return haxe__$Template_TemplateExpr.OpExpr(this.parseExpr(p)); } return haxe__$Template_TemplateExpr.OpVar(p); } ,parseExpr: function(data) { var l = new haxe_ds_List(); var expr = data; while(haxe_Template.expr_splitter.match(data)) { var p = haxe_Template.expr_splitter.matchedPos(); var k = p.pos + p.len; if(p.pos != 0) { l.add({ p : HxOverrides.substr(data,0,p.pos), s : true}); } var p1 = haxe_Template.expr_splitter.matched(0); l.add({ p : p1, s : p1.indexOf("\"") >= 0}); data = haxe_Template.expr_splitter.matchedRight(); } if(data.length != 0) { var _g_offset = 0; var _g_s = data; while(_g_offset < _g_s.length) { var _g_key = _g_offset; var _g_value = _g_s.charCodeAt(_g_offset++); var i = _g_key; var c = _g_value; if(c != 32) { l.add({ p : HxOverrides.substr(data,i,null), s : true}); break; } } } var e; try { e = this.makeExpr(l); if(!l.isEmpty()) { throw haxe_Exception.thrown(l.first().p); } } catch( _g ) { var _g1 = haxe_Exception.caught(_g).unwrap(); if(typeof(_g1) == "string") { var s = _g1; throw haxe_Exception.thrown("Unexpected '" + s + "' in " + expr); } else { throw _g; } } return function() { try { return e(); } catch( _g ) { var exc = haxe_Exception.caught(_g).unwrap(); throw haxe_Exception.thrown("Error : " + Std.string(exc) + " in " + expr); } }; } ,makeConst: function(v) { haxe_Template.expr_trim.match(v); v = haxe_Template.expr_trim.matched(1); if(HxOverrides.cca(v,0) == 34) { var str = HxOverrides.substr(v,1,v.length - 2); return function() { return str; }; } if(haxe_Template.expr_int.match(v)) { var i = Std.parseInt(v); return function() { return i; }; } if(haxe_Template.expr_float.match(v)) { var f = parseFloat(v); return function() { return f; }; } var me = this; return function() { return me.resolve(v); }; } ,makePath: function(e,l) { var p = l.first(); if(p == null || p.p != ".") { return e; } l.pop(); var field = l.pop(); if(field == null || !field.s) { throw haxe_Exception.thrown(field.p); } var f = field.p; haxe_Template.expr_trim.match(f); f = haxe_Template.expr_trim.matched(1); return this.makePath(function() { return Reflect.field(e(),f); },l); } ,makeExpr: function(l) { return this.makePath(this.makeExpr2(l),l); } ,skipSpaces: function(l) { var p = l.first(); while(p != null) { var _g_offset = 0; var _g_s = p.p; while(_g_offset < _g_s.length) { var c = _g_s.charCodeAt(_g_offset++); if(c != 32) { return; } } l.pop(); p = l.first(); } } ,makeExpr2: function(l) { this.skipSpaces(l); var p = l.pop(); this.skipSpaces(l); if(p == null) { throw haxe_Exception.thrown(""); } if(p.s) { return this.makeConst(p.p); } switch(p.p) { case "!": var e = this.makeExpr(l); return function() { var v = e(); if(v != null) { return v == false; } else { return true; } }; case "(": this.skipSpaces(l); var e1 = this.makeExpr(l); this.skipSpaces(l); var p1 = l.pop(); if(p1 == null || p1.s) { throw haxe_Exception.thrown(p1); } if(p1.p == ")") { return e1; } this.skipSpaces(l); var e2 = this.makeExpr(l); this.skipSpaces(l); var p2 = l.pop(); this.skipSpaces(l); if(p2 == null || p2.p != ")") { throw haxe_Exception.thrown(p2); } switch(p1.p) { case "!=": return function() { return e1() != e2(); }; case "&&": return function() { return e1() && e2(); }; case "*": return function() { return e1() * e2(); }; case "+": return function() { return e1() + e2(); }; case "-": return function() { return e1() - e2(); }; case "/": return function() { return e1() / e2(); }; case "<": return function() { return e1() < e2(); }; case "<=": return function() { return e1() <= e2(); }; case "==": return function() { return e1() == e2(); }; case ">": return function() { return e1() > e2(); }; case ">=": return function() { return e1() >= e2(); }; case "||": return function() { return e1() || e2(); }; default: throw haxe_Exception.thrown("Unknown operation " + p1.p); } break; case "-": var e3 = this.makeExpr(l); return function() { return -e3(); }; } throw haxe_Exception.thrown(p.p); } ,run: function(e) { switch(e._hx_index) { case 0: var v = e.v; var _this = this.buf; var x = Std.string(this.resolve(v)); _this.b += Std.string(x); break; case 1: var e1 = e.expr; var _this = this.buf; var x = Std.string(e1()); _this.b += Std.string(x); break; case 2: var e1 = e.expr; var eif = e.eif; var eelse = e.eelse; var v = e1(); if(v == null || v == false) { if(eelse != null) { this.run(eelse); } } else { this.run(eif); } break; case 3: var str = e.str; this.buf.b += str == null ? "null" : "" + str; break; case 4: var l = e.l; var _g_head = l.h; while(_g_head != null) { var val = _g_head.item; _g_head = _g_head.next; var e1 = val; this.run(e1); } break; case 5: var e1 = e.expr; var loop = e.loop; var v = e1(); try { var x = $getIterator(v); if(x.hasNext == null) { throw haxe_Exception.thrown(null); } v = x; } catch( _g ) { try { if(v.hasNext == null) { throw haxe_Exception.thrown(null); } } catch( _g1 ) { throw haxe_Exception.thrown("Cannot iter on " + Std.string(v)); } } this.stack.push(this.context); var v1 = v; var ctx = v1; while(ctx.hasNext()) { var ctx1 = ctx.next(); this.context = ctx1; this.run(loop); } this.context = this.stack.pop(); break; case 6: var m = e.name; var params = e.params; var v = Reflect.field(this.macros,m); var pl = []; var old = this.buf; pl.push($bind(this,this.resolve)); var _g_head = params.h; while(_g_head != null) { var val = _g_head.item; _g_head = _g_head.next; var p = val; if(p._hx_index == 0) { var v1 = p.v; pl.push(this.resolve(v1)); } else { this.buf = new StringBuf(); this.run(p); pl.push(this.buf.b); } } this.buf = old; try { var _this = this.buf; var x = Std.string(v.apply(this.macros,pl)); _this.b += Std.string(x); } catch( _g ) { var e = haxe_Exception.caught(_g).unwrap(); var plstr; try { plstr = pl.join(","); } catch( _g1 ) { plstr = "???"; } var msg = "Macro call " + m + "(" + plstr + ") failed (" + Std.string(e) + ")"; throw haxe_Exception.thrown(msg); } break; } } }; var haxe_ValueException = function(value,previous,native) { haxe_Exception.call(this,String(value),previous,native); this.value = value; }; haxe_ValueException.__name__ = true; haxe_ValueException.__super__ = haxe_Exception; haxe_ValueException.prototype = $extend(haxe_Exception.prototype,{ unwrap: function() { return this.value; } }); var haxe_ds_List = function() { this.length = 0; }; haxe_ds_List.__name__ = true; haxe_ds_List.prototype = { add: function(item) { var x = new haxe_ds__$List_ListNode(item,null); if(this.h == null) { this.h = x; } else { this.q.next = x; } this.q = x; this.length++; } ,push: function(item) { var x = new haxe_ds__$List_ListNode(item,this.h); this.h = x; if(this.q == null) { this.q = x; } this.length++; } ,first: function() { if(this.h == null) { return null; } else { return this.h.item; } } ,pop: function() { if(this.h == null) { return null; } var x = this.h.item; this.h = this.h.next; if(this.h == null) { this.q = null; } this.length--; return x; } ,isEmpty: function() { return this.h == null; } ,toString: function() { var s_b = ""; var first = true; var l = this.h; s_b += "{"; while(l != null) { if(first) { first = false; } else { s_b += ", "; } s_b += Std.string(Std.string(l.item)); l = l.next; } s_b += "}"; return s_b; } }; var haxe_ds__$List_ListNode = function(item,next) { this.item = item; this.next = next; }; haxe_ds__$List_ListNode.__name__ = true; var js_Boot = function() { }; js_Boot.__name__ = true; js_Boot.__string_rec = function(o,s) { if(o == null) { return "null"; } if(s.length >= 5) { return "<...>"; } var t = typeof(o); if(t == "function" && (o.__name__ || o.__ename__)) { t = "object"; } switch(t) { case "function": return ""; case "object": if(o.__enum__) { var e = $hxEnums[o.__enum__]; var con = e.__constructs__[o._hx_index]; var n = con._hx_name; if(con.__params__) { s = s + "\t"; return n + "(" + ((function($this) { var $r; var _g = []; { var _g1 = 0; var _g2 = con.__params__; while(true) { if(!(_g1 < _g2.length)) { break; } var p = _g2[_g1]; _g1 = _g1 + 1; _g.push(js_Boot.__string_rec(o[p],s)); } } $r = _g; return $r; }(this))).join(",") + ")"; } else { return n; } } if(((o) instanceof Array)) { var str = "["; s += "\t"; var _g = 0; var _g1 = o.length; while(_g < _g1) { var i = _g++; str += (i > 0 ? "," : "") + js_Boot.__string_rec(o[i],s); } str += "]"; return str; } var tostr; try { tostr = o.toString; } catch( _g ) { return "???"; } if(tostr != null && tostr != Object.toString && typeof(tostr) == "function") { var s2 = o.toString(); if(s2 != "[object Object]") { return s2; } } var str = "{\n"; s += "\t"; var hasp = o.hasOwnProperty != null; var k = null; for( k in o ) { if(hasp && !o.hasOwnProperty(k)) { continue; } if(k == "prototype" || k == "__class__" || k == "__super__" || k == "__interfaces__" || k == "__properties__") { continue; } if(str.length != 2) { str += ", \n"; } str += s + k + " : " + js_Boot.__string_rec(o[k],s); } s = s.substring(1); str += "\n" + s + "}"; return str; case "string": return o; default: return String(o); } }; var xrfragment_Filter = $hx_exports["xrfragment"]["Filter"] = function(str) { this.q = { }; this.str = ""; if(str != null) { this.parse(str); } }; xrfragment_Filter.__name__ = true; xrfragment_Filter.prototype = { toObject: function() { return Reflect.copy(this.q); } ,get: function() { return Reflect.copy(this.q); } ,parse: function(str) { var token = str.split(" "); var q = { }; var process = function(str,prefix) { if(prefix == null) { prefix = ""; } str = StringTools.trim(str); var k = str.split("=")[0]; var v = str.split("=")[1]; var filter = { }; if(q[prefix + k]) { filter = q[prefix + k]; } if(xrfragment_XRF.isProp.match(str)) { var oper = ""; if(str.indexOf(">") != -1) { oper = ">"; } if(str.indexOf("<") != -1) { oper = "<"; } if(xrfragment_XRF.isExclude.match(k)) { k = HxOverrides.substr(k,1,null); } v = HxOverrides.substr(v,oper.length,null); if(oper.length == 0) { oper = "="; } var rule = { }; if(xrfragment_XRF.isNumber.match(v)) { rule[oper] = parseFloat(v); } else { rule[oper] = v; } q["expr"] = rule; } var value = xrfragment_XRF.isDeep.match(str) ? k.split("*").length - 1 : 0; q["deep"] = value; var value = xrfragment_XRF.isExclude.match(str) ? false : true; q["show"] = value; var value = k.replace(xrfragment_XRF.operators.r,""); q["key"] = value; q["value"] = v; }; var _g = 0; var _g1 = token.length; while(_g < _g1) { var i = _g++; process(token[i]); } return this.q = q; } ,test: function(obj) { var qualify = false; var _g = 0; var _g1 = Reflect.fields(obj); while(_g < _g1.length) { var k = _g1[_g]; ++_g; var v = Std.string(Reflect.field(obj,k)); if(this.testProperty(k,v)) { qualify = true; } } var _g = 0; var _g1 = Reflect.fields(obj); while(_g < _g1.length) { var k = _g1[_g]; ++_g; var v = Std.string(Reflect.field(obj,k)); if(this.testProperty(k,v,true)) { qualify = false; } } return qualify; } ,testProperty: function(property,value,exclude) { var conds = 0; var fails = 0; var qualify = 0; var testprop = function(expr) { conds += 1; fails += expr ? 0 : 1; return expr; }; if(this.q[value] != null) { var v = this.q[value]; if(v[property] != null) { return v[property]; } } if(Reflect.field(this.q,"expr")) { var f = Reflect.field(this.q,"expr"); if(!Reflect.field(this.q,"show")) { if(Reflect.field(f,"!=") != null && testprop((value == null ? "null" : "" + value) == Std.string(Reflect.field(f,"!="))) && exclude) { ++qualify; } } else { if(Reflect.field(f,"*") != null && testprop(parseFloat(value) != null)) { ++qualify; } if(Reflect.field(f,">") != null && testprop(parseFloat(value) >= parseFloat(Reflect.field(f,">")))) { ++qualify; } if(Reflect.field(f,"<") != null && testprop(parseFloat(value) <= parseFloat(Reflect.field(f,"<")))) { ++qualify; } if(Reflect.field(f,"=") != null && (testprop(value == Reflect.field(f,"=")) || testprop(parseFloat(value) == parseFloat(Reflect.field(f,"="))))) { ++qualify; } } } return qualify > 0; } }; var xrfragment_Parser = $hx_exports["xrfragment"]["Parser"] = function() { }; xrfragment_Parser.__name__ = true; xrfragment_Parser.parse = function(key,value,store,index) { var Frag_h = Object.create(null); Frag_h["#"] = xrfragment_XRF.IMMUTABLE | xrfragment_XRF.T_PREDEFINED_VIEW | xrfragment_XRF.PV_EXECUTE; Frag_h["src"] = xrfragment_XRF.T_URL; Frag_h["href"] = xrfragment_XRF.T_URL | xrfragment_XRF.T_PREDEFINED_VIEW; Frag_h["tag"] = xrfragment_XRF.IMMUTABLE | xrfragment_XRF.T_STRING; Frag_h["pos"] = xrfragment_XRF.PV_OVERRIDE | xrfragment_XRF.T_VECTOR3 | xrfragment_XRF.T_STRING | xrfragment_XRF.METADATA | xrfragment_XRF.NAVIGATOR; Frag_h["rot"] = xrfragment_XRF.QUERY_OPERATOR | xrfragment_XRF.PV_OVERRIDE | xrfragment_XRF.T_VECTOR3 | xrfragment_XRF.METADATA | xrfragment_XRF.NAVIGATOR; Frag_h["t"] = xrfragment_XRF.PV_OVERRIDE | xrfragment_XRF.T_FLOAT | xrfragment_XRF.T_VECTOR2 | xrfragment_XRF.NAVIGATOR | xrfragment_XRF.METADATA; Frag_h["s"] = xrfragment_XRF.PV_OVERRIDE | xrfragment_XRF.T_MEDIAFRAG; Frag_h["loop"] = xrfragment_XRF.PV_OVERRIDE | xrfragment_XRF.T_PREDEFINED_VIEW; Frag_h["uv"] = xrfragment_XRF.T_VECTOR2 | xrfragment_XRF.T_MEDIAFRAG; Frag_h["namespace"] = xrfragment_XRF.IMMUTABLE | xrfragment_XRF.T_STRING; Frag_h["SPDX"] = xrfragment_XRF.IMMUTABLE | xrfragment_XRF.T_STRING; Frag_h["unit"] = xrfragment_XRF.IMMUTABLE | xrfragment_XRF.T_STRING; Frag_h["description"] = xrfragment_XRF.IMMUTABLE | xrfragment_XRF.T_STRING; var keyStripped = key.replace(xrfragment_XRF.operators.r,""); var isPVDynamic = key.length > 0 && !Object.prototype.hasOwnProperty.call(Frag_h,key); if(isPVDynamic) { var v = new xrfragment_XRF(key,xrfragment_XRF.PV_EXECUTE | xrfragment_XRF.NAVIGATOR,index); v.validate(value); v.flags = xrfragment_XRF.set(xrfragment_XRF.T_DYNAMICKEY,v.flags); if(!Object.prototype.hasOwnProperty.call(Frag_h,key)) { v.flags = xrfragment_XRF.set(xrfragment_XRF.CUSTOMFRAG,v.flags); } if(value.length == 0) { v.flags = xrfragment_XRF.set(xrfragment_XRF.T_DYNAMICKEYVALUE,v.flags); } store[keyStripped] = v; return true; } var v = new xrfragment_XRF(key,Frag_h[key],index); if(Object.prototype.hasOwnProperty.call(Frag_h,key)) { if(!v.validate(value)) { console.log("src/xrfragment/Parser.hx:67:","⚠ fragment '" + key + "' has incompatible value (" + value + ")"); return false; } store[keyStripped] = v; if(xrfragment_Parser.debug) { console.log("src/xrfragment/Parser.hx:71:","✔ " + key + ": " + v.string); } } else { if(typeof(value) == "string") { v.guessType(v,value); } v.flags = xrfragment_XRF.set(xrfragment_XRF.CUSTOMFRAG,v.flags); store[keyStripped] = v; } return true; }; xrfragment_Parser.getMetaData = function() { var meta = { title : ["title","og:title","dc.title"], description : ["aria-description","og:description","dc.description"], author : ["author","dc.creator"], publisher : ["publisher","dc.publisher"], website : ["og:site_name","og:url","dc.publisher"], license : ["SPDX","dc.rights"]}; return meta; }; var xrfragment_URI = $hx_exports["xrfragment"]["URI"] = function() { this.XRF = { }; this.hash = { }; this.fragment = ""; }; xrfragment_URI.__name__ = true; xrfragment_URI.parseFragment = function(url,filter) { var store = { }; if(url == null || url.indexOf("#") == -1) { return store; } var fragment = url.split("#"); var splitArray = fragment[1].split("&"); var _g = 0; var _g1 = splitArray.length; while(_g < _g1) { var i = _g++; var splitByEqual = splitArray[i].split("="); var regexPlus = new EReg("\\+","g"); var key = splitByEqual[0]; var value = ""; if(splitByEqual.length > 1) { if(xrfragment_XRF.isVector.match(splitByEqual[1])) { value = splitByEqual[1]; } else { var s = regexPlus.split(splitByEqual[1]).join(" "); value = decodeURIComponent(s.split("+").join(" ")); } } var ok = xrfragment_Parser.parse(key,value,store,i); } if(filter != null && filter != 0) { var _g = 0; var _g1 = Reflect.fields(store); while(_g < _g1.length) { var key = _g1[_g]; ++_g; var xrf = store[key]; if(!xrf.is(filter)) { Reflect.deleteField(store,key); } } } return store; }; xrfragment_URI.template = function(uri,vars) { var parts = uri.split("#"); if(parts.length == 1) { return uri; } var frag = parts[1]; frag = StringTools.replace(frag,"{","::"); frag = StringTools.replace(frag,"}","::"); frag = new haxe_Template(frag).execute(vars); frag = StringTools.replace(frag,"null",""); parts[1] = frag; return parts.join("#"); }; xrfragment_URI.parse = function(stringUrl,flags) { var r = new EReg("^(?:(?![^:@]+:[^:@/]*@)([^:/?#.]+):)?(?://)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:/?#]*)(?::(\\d*))?)(((/(?:[^?#](?![^?#/]*\\.[^?#/.]+(?:[?#]|$)))*/?)?([^?#/]*))(?:\\?([^#]*))?(?:#(.*))?)",""); if(stringUrl.indexOf("://") == -1 && stringUrl.charAt(0) != "/" && stringUrl.charAt(0) != "#") { stringUrl = "/" + stringUrl; } r.match(stringUrl); var url = new xrfragment_URI(); var _g = 0; var _g1 = xrfragment_URI._parts.length; while(_g < _g1) { var i = _g++; url[xrfragment_URI._parts[i]] = r.matched(i); } if(xrfragment_URI.isRelative(url) == true) { if(url.directory == null && url.host != null) { url.file = url.host; } url.host = ""; } url.hash = { }; if(url.fragment != null && url.fragment.length > 0) { url.XRF = xrfragment_URI.parseFragment("#" + url.fragment,flags); var key; var _g = 0; var _g1 = Reflect.fields(url.XRF); while(_g < _g1.length) { var key = _g1[_g]; ++_g; var v = url.XRF[key]; url.hash[key] = v["string"]; } } xrfragment_URI.computeVars(url); return url; }; xrfragment_URI.computeVars = function(url) { var r_r = new RegExp("//","g".split("u").join("")); if(url.directory != null && url.directory.indexOf("//") != -1) { url.directory = url.directory.replace(r_r,"/"); } if(url.path != null && url.path.indexOf("//") != -1) { url.path = url.path.replace(r_r,"/"); } if(url.file != null && url.file.indexOf("//") != -1) { url.file = url.file.replace(r_r,"/"); } url.URN = url.scheme + "://" + url.host; if(url.port != null) { url.URN += ":" + url.port; } url.URN += url.directory; if(url.file != null) { var parts = url.file.split("."); if(parts.length > 1) { url.fileExt = parts.pop(); } } }; xrfragment_URI.toString = function(url) { var result = ""; if(url.scheme != null) { result += url.scheme + "://"; } if(url.user != null) { result += url.user + ":"; } if(url.password != null) { result += url.password + "@"; } if(url.host != null) { result += url.host; } if(url.port != null) { result += ":" + url.port; } if(url.directory != null) { result += url.directory; } if(url.file != null) { result += url.file; } if(url.query != null) { result += "?" + url.query; } if(url.fragment != null) { result += "#" + url.fragment; } return result; }; xrfragment_URI.appendURI = function(url,appendedURI) { if(xrfragment_URI.isRelative(url) == true) { return xrfragment_URI.appendToRelativeURI(url,appendedURI); } else { return xrfragment_URI.appendToAbsoluteURI(url,appendedURI); } }; xrfragment_URI.isRelative = function(url) { return url.scheme == null; }; xrfragment_URI.appendToRelativeURI = function(url,appendedURI) { if(url.directory == null || url.host == null) { return xrfragment_URI.cloneURI(appendedURI); } var resultURI = new xrfragment_URI(); resultURI.host = url.host; resultURI.directory = url.directory; if(appendedURI.host != null) { resultURI.directory += appendedURI.host; } if(appendedURI.directory != null) { var directory = appendedURI.directory; if(appendedURI.host == null) { resultURI.directory += HxOverrides.substr(directory,1,null); } else { resultURI.directory += directory; } } if(appendedURI.file != null) { resultURI.file = appendedURI.file; } resultURI.path = resultURI.directory + resultURI.file; if(appendedURI.query != null) { resultURI.query = appendedURI.query; } if(appendedURI.fragment != null) { resultURI.fragment = appendedURI.fragment; } return resultURI; }; xrfragment_URI.appendToAbsoluteURI = function(url,appendedURI) { var resultURI = new xrfragment_URI(); if(url.scheme != null) { resultURI.scheme = url.scheme; } if(url.host != null) { resultURI.host = url.host; } var directory = ""; if(url.directory != null) { directory = url.directory; } if(appendedURI.host != null) { appendedURI.directory += appendedURI.host; } if(appendedURI.directory != null) { directory += appendedURI.directory; } resultURI.directory = directory; if(appendedURI.file != null) { resultURI.file = appendedURI.file; } resultURI.path = resultURI.directory + resultURI.file; if(appendedURI.query != null) { resultURI.query = appendedURI.query; } if(appendedURI.fragment != null) { resultURI.fragment = appendedURI.fragment; } return resultURI; }; xrfragment_URI.toAbsolute = function(url,newUrl) { var newURI = xrfragment_URI.parse(newUrl,0); var resultURI = new xrfragment_URI(); resultURI.port = url.port; resultURI.source = newUrl; if(newURI.scheme != null) { resultURI.scheme = newURI.scheme; } else { resultURI.scheme = url.scheme; } if(newURI.host != null && newURI.host.length > 0) { resultURI.host = newURI.host; resultURI.port = null; resultURI.fragment = null; resultURI.hash = { }; resultURI.XRF = { }; if(newURI.port != null) { resultURI.port = newURI.port; } } else { resultURI.host = url.host; } var directory = ""; if(url.directory != null) { directory = url.directory; } if(newURI.directory != null && newURI.source.charAt(0) != "#" && newURI.directory.length > 0) { if(newUrl.charAt(0) != "/" && newUrl.indexOf("://") == -1) { var stripRelative_r = new RegExp("\\./.*","".split("u").join("")); directory = directory.replace(stripRelative_r,""); directory += newURI.directory; } else { directory = newURI.directory; } } resultURI.directory = directory; if(newURI.file != null && newURI.file.length > 0) { resultURI.file = newURI.file; } else { resultURI.file = url.file; } resultURI.path = resultURI.directory + resultURI.file; if(newURI.query != null) { resultURI.query = newURI.query; } if(newURI.fragment != null) { resultURI.fragment = newURI.fragment; } resultURI.hash = newURI.hash; resultURI.XRF = newURI.XRF; xrfragment_URI.computeVars(resultURI); return resultURI; }; xrfragment_URI.cloneURI = function(url) { var clonedURI = new xrfragment_URI(); clonedURI.url = url.url; clonedURI.source = url.source; clonedURI.scheme = url.scheme; clonedURI.authority = url.authority; clonedURI.userInfo = url.userInfo; clonedURI.password = url.password; clonedURI.host = url.host; clonedURI.port = url.port; clonedURI.relative = url.relative; clonedURI.path = url.path; clonedURI.directory = url.directory; clonedURI.file = url.file; clonedURI.query = url.query; clonedURI.fragment = url.fragment; return clonedURI; }; var xrfragment_XRF = $hx_exports["xrfragment"]["XRF"] = function(_fragment,_flags,_index) { this.floats = []; this.shift = []; this.fragment = _fragment; this.flags = _flags; this.index = _index; }; xrfragment_XRF.__name__ = true; xrfragment_XRF.set = function(flag,flags) { return flags | flag; }; xrfragment_XRF.unset = function(flag,flags) { return flags & ~flag; }; xrfragment_XRF.prototype = { is: function(flag) { var v = this.flags; if(!(typeof(v) == "number" && ((v | 0) === v))) { this.flags = 0; } return (this.flags & flag) != 0; } ,validate: function(value) { this.guessType(this,value); var ok = true; if(value.length == 0 && !this.is(xrfragment_XRF.T_PREDEFINED_VIEW)) { ok = false; } if(!this.is(xrfragment_XRF.T_FLOAT) && this.is(xrfragment_XRF.T_VECTOR2) && !(typeof(this.x) == "number" && typeof(this.y) == "number")) { ok = false; } if(!(this.is(xrfragment_XRF.T_VECTOR2) || this.is(xrfragment_XRF.T_STRING)) && this.is(xrfragment_XRF.T_VECTOR3) && !(typeof(this.x) == "number" && typeof(this.y) == "number" && typeof(this.z) == "number")) { ok = false; } return ok; } ,guessType: function(v,str) { v.string = str; if(xrfragment_XRF.isReset.match(v.fragment)) { v.reset = true; } if(v.fragment == "loop") { v.loop = true; } if(typeof(str) != "string") { return; } if(str.length > 0) { if(xrfragment_XRF.isXRFScheme.match(str)) { v.xrfScheme = true; str = str.replace(xrfragment_XRF.isXRFScheme.r,""); v.string = str; } if(str.split(",").length > 1) { var xyzn = str.split(","); if(xyzn.length > 0) { v.x = parseFloat(xyzn[0]); } if(xyzn.length > 1) { v.y = parseFloat(xyzn[1]); } if(xyzn.length > 2) { v.z = parseFloat(xyzn[2]); } var _g = 0; var _g1 = xyzn.length; while(_g < _g1) { var i = _g++; v.shift.push(xrfragment_XRF.isShift.match(xyzn[i])); v.floats.push(parseFloat(xyzn[i].replace(xrfragment_XRF.isShift.r,""))); } } if(xrfragment_XRF.isColor.match(str)) { v.color = str; } if(xrfragment_XRF.isFloat.match(str)) { v.x = parseFloat(str); v.float = v.x; } if(xrfragment_XRF.isInt.match(str)) { v.int = Std.parseInt(str); v.x = v.int; v.floats.push(v.x); } v.filter = new xrfragment_Filter(v.fragment + "=" + v.string); } else { v.filter = new xrfragment_Filter(v.fragment); } } }; function $getIterator(o) { if( o instanceof Array ) return new haxe_iterators_ArrayIterator(o); else return o.iterator(); } function $bind(o,m) { if( m == null ) return null; if( m.__id__ == null ) m.__id__ = $global.$haxeUID++; var f; if( o.hx__closures__ == null ) o.hx__closures__ = {}; else f = o.hx__closures__[m.__id__]; if( f == null ) { f = m.bind(o); o.hx__closures__[m.__id__] = f; } return f; } $global.$haxeUID |= 0; if(typeof(performance) != "undefined" ? typeof(performance.now) == "function" : false) { HxOverrides.now = performance.now.bind(performance); } if( String.fromCodePoint == null ) String.fromCodePoint = function(c) { return c < 0x10000 ? String.fromCharCode(c) : String.fromCharCode((c>>10)+0xD7C0)+String.fromCharCode((c&0x3FF)+0xDC00); } String.__name__ = true; Array.__name__ = true; js_Boot.__toStr = ({ }).toString; haxe_Template.splitter = new EReg("(::[A-Za-z0-9_ ()&|!+=/><*.\"-]+::|\\$\\$([A-Za-z0-9_-]+)\\()",""); haxe_Template.expr_splitter = new EReg("(\\(|\\)|[ \r\n\t]*\"[^\"]*\"[ \r\n\t]*|[!+=/><*.&|-]+)",""); haxe_Template.expr_trim = new EReg("^[ ]*([^ ]+)[ ]*$",""); haxe_Template.expr_int = new EReg("^[0-9]+$",""); haxe_Template.expr_float = new EReg("^([+-]?)(?=\\d|,\\d)\\d*(,\\d*)?([Ee]([+-]?\\d+))?$",""); haxe_Template.globals = { }; haxe_Template.hxKeepArrayIterator = new haxe_iterators_ArrayIterator([]); xrfragment_Parser.error = ""; xrfragment_Parser.debug = false; xrfragment_URI.__meta__ = { statics : { template : { keep : null}}}; xrfragment_URI._parts = ["source","scheme","authority","userInfo","user","password","host","port","relative","path","directory","file","query","fragment"]; xrfragment_XRF.IMMUTABLE = 1; xrfragment_XRF.PROP_BIND = 2; xrfragment_XRF.QUERY_OPERATOR = 4; xrfragment_XRF.PROMPT = 8; xrfragment_XRF.CUSTOMFRAG = 16; xrfragment_XRF.NAVIGATOR = 32; xrfragment_XRF.METADATA = 64; xrfragment_XRF.PV_OVERRIDE = 128; xrfragment_XRF.PV_EXECUTE = 256; xrfragment_XRF.T_COLOR = 8192; xrfragment_XRF.T_INT = 16384; xrfragment_XRF.T_FLOAT = 32768; xrfragment_XRF.T_VECTOR2 = 65536; xrfragment_XRF.T_VECTOR3 = 131072; xrfragment_XRF.T_URL = 262144; xrfragment_XRF.T_PREDEFINED_VIEW = 524288; xrfragment_XRF.T_STRING = 1048576; xrfragment_XRF.T_MEDIAFRAG = 2097152; xrfragment_XRF.T_DYNAMICKEY = 4194304; xrfragment_XRF.T_DYNAMICKEYVALUE = 8388608; xrfragment_XRF.isColor = new EReg("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$",""); xrfragment_XRF.isInt = new EReg("^[-0-9]+$",""); xrfragment_XRF.isFloat = new EReg("^[-0-9]+\\.[0-9]+$",""); xrfragment_XRF.isVector = new EReg("([,]+|\\w)",""); xrfragment_XRF.isUrl = new EReg("(://)?\\..*",""); xrfragment_XRF.isUrlOrPretypedView = new EReg("(^#|://)?\\..*",""); xrfragment_XRF.isString = new EReg(".*",""); xrfragment_XRF.operators = new EReg("(^[-]|^[!]|[\\*]$)","g"); xrfragment_XRF.isProp = new EReg("^.*=[><=]?",""); xrfragment_XRF.isExclude = new EReg("^-",""); xrfragment_XRF.isDeep = new EReg("\\*",""); xrfragment_XRF.isNumber = new EReg("^[0-9\\.]+$",""); xrfragment_XRF.isMediaFrag = new EReg("^([0-9\\.,\\*+-]+)$",""); xrfragment_XRF.isReset = new EReg("^!",""); xrfragment_XRF.isShift = new EReg("^(\\+|--)",""); xrfragment_XRF.isXRFScheme = new EReg("^xrf://",""); })(typeof window != "undefined" ? window : typeof global != "undefined" ? global : typeof self != "undefined" ? self : this); var xrfragment = $hx_exports["xrfragment"]; // the core project uses #vanillajs #proxies #clean #noframework var $ = typeof $ != 'undefined' ? $ : (s) => document.querySelector(s) // respect jquery var $$ = typeof $$ != 'undefined' ? $$ : (s) => [...document.querySelectorAll(s)] // zepto etc. var $el = (html,tag) => { let el = document.createElement('div') el.innerHTML = html return el.children[0] } // SPDX-License-Identifier: MPL-2.0 // Copyright (c) 2023 Leon van Kammen/NLNET var xrf = {} xrf.init = function(opts){ opts = opts || {} xrf.debug = document.location.hostname.match(/^(localhost|[0-9]\.[0-9])/) || document.location.port == '8080' ? 0 : false if( document.location.hash.match(/debug=([0-9])/) ){ xrf.debug = parseInt( ( document.location.hash.match(/debug=([0-9])/) || [0,'0'] )[1] ) } if( xrf.debug === false ) console.log("add #debug=[0-9] to URL to see XR Fragment debuglog") xrf.Parser.debug = xrf.debug xrf.detectCameraRig(opts) for ( let i in opts ) xrf[i] = opts[i] xrf.emit('init',opts) return xrf } xrf.query = function(){ // framework implementations can override this function, see src/3rd/js/three/index.sj alert("queries are not implemented (yet) for this particular framework") } xrf.detectCameraRig = function(opts){ if( opts.camera ){ // detect rig (if any) const getCam = ((cam) => () => cam)(opts.camera) let offsetY = 0 while( opts.camera.parent.type != "Scene" ){ offsetY += opts.camera.position.y opts.camera = opts.camera.parent opts.camera.getCam = getCam opts.camera.updateProjectionMatrix = () => opts.camera.getCam().updateProjectionMatrix() } if( !opts.camera.getCam ) opts.camera.getCam = getCam // always attach function opts.camera.offsetY = offsetY } } xrf.hasTag = (tag,tags) => String(tags).match( new RegExp(`(^| )${tag}( |$)`,`g`) ) // map library functions to xrf for ( let i in xrfragment ) xrf[i] = xrfragment[i] /* * (promise-able) EVENTS (optionally continue after listeners are finished using .then) * * example: * * xrf.addEventListener('foo',(e) => { * // let promise = e.promise() * console.log("navigating to: "+e.detail.destination.url) * // promise.resolve() * // promise.reject("not going to happen") * }) * * xrf.emit('foo',123) * xrf.emit('foo',123).then(...).catch(...).finally(...) */ xrf.addEventListener = function(eventName, callback, opts) { if( !this._listeners ) this._listeners = [] callback.opts = opts || {weight: this._listeners.length} if (!this._listeners[eventName]) { // create a new array for this event name if it doesn't exist yet this._listeners[eventName] = []; } // add the callback to the listeners array for this event name this._listeners[eventName].push(callback); // sort this._listeners[eventName] = this._listeners[eventName].sort( (a,b) => a.opts.weight > b.opts.weight ) callback.unlisten = () => { this._listeners[eventName] = this._listeners[eventName].filter( (c) => c != callback ) } return callback.unlisten }; xrf.emit = function(eventName, data){ if( typeof data != 'object' ) throw 'emit() requires passing objects' if( xrf.debug && xrf.debug > 1 && ( !eventName.match(/^render/) || xrf.debug == eventName ) ){ let label = String(`xrf.emit('${eventName}')`).padEnd(35," "); label += data.mesh && data.mesh.name ? '#'+data.mesh.name : '' console.groupCollapsed(label) console.info(data) console.groupEnd(label) if( eventName == 'reset' ) debugger if( xrf.debug > 2 ) debugger } return xrf.emit.promise(eventName,data) } xrf.emit.normal = function(eventName, opts) { if( !xrf._listeners ) xrf._listeners = [] var callbacks = xrf._listeners[eventName] if (callbacks) { for (var i = 0; i < callbacks.length && !opts.halt; i++) { try{ callbacks[i](opts); }catch(e){ console.error(e) } } } }; xrf.emit.promise = function(e, opts){ return new Promise( (resolve, reject) => { opts.promise = () => { opts.promises = opts.promises || [] opts.promises.push(0) return { resolve: ((index) => () => { opts.promises[index] = 1 let succesful = opts.promises.reduce( (a,b) => a+b ) if( succesful == opts.promises.length ) resolve(opts) })(opts.promises.length-1), reject: (reason) => { opts.halt = true console.warn(`'${e}' event rejected: ${reason}`) } } } xrf.emit.normal(e, opts) if( !opts.promises ) resolve(opts) delete opts.promise }) } xrf.addEventListener('reset', () => { let events = ['renderPost'] events.map( (e) => { if( xrf._listeners[e] ) xrf._listeners[e].map( (r) => r.unlisten && r.unlisten() ) }) }) /*! rasterizeHTML.js - v1.3.1 - 2023-07-06 * http://www.github.com/cburgmer/rasterizeHTML.js * Copyright (c) 2023 Christoph Burgmer; Licensed MIT */ !function(o,i){void 0===o&&void 0!==window&&(o=window),"function"==typeof define&&define.amd?define(["url","xmlserializer","sane-domparser-error","inlineresources"],function(e,t,n,r){return o.rasterizeHTML=i(e,t,n,r)}):"object"==typeof module&&module.exports?module.exports=i(require("url"),require("xmlserializer"),require("sane-domparser-error"),require("inlineresources")):o.rasterizeHTML=i(o.url,o.xmlserializer,o.sanedomparsererror,o.inlineresources)}(this,function(e,t,n,r){var o=function(n){"use strict";var o={},t=[];o.joinUrl=function(e,t){return e?n.resolve(e,t):t},o.getConstantUniqueIdFor=function(e){return t.indexOf(e)<0&&t.push(e),t.indexOf(e)},o.clone=function(e){var t,n={};for(t in e)e.hasOwnProperty(t)&&(n[t]=e[t]);return n};return o.parseOptionalParameters=function(e){var t,n,r={canvas:null,options:{}};return null==e[0]||(t=e[0],"object"==typeof(n=t)&&null!==n&&Object.prototype.toString.apply(t).match(/\[object (Canvas|HTMLCanvasElement)\]/i))?(r.canvas=e[0]||null,r.options=o.clone(e[1])):r.options=o.clone(e[0]),r},o}(e),i=function(i){"use strict";function u(e,t,n){var r=e[t];return e[t]=function(){var e=Array.prototype.slice.call(arguments);return n.apply(this,[e,r])},r}var e={};return e.baseUrlRespectingXhr=function(t,o){return function(){var e=new t;return u(e,"open",function(e,t){var n=e.shift(),r=e.shift(),r=i.joinUrl(o,r);return t.apply(this,[n,r].concat(e))}),e}},e.finishNotifyingXhr=function(t){function e(){var e=new t;return u(e,"send",function(e,t){return r+=1,t.apply(this,arguments)}),e.addEventListener("load",function(){o+=1,n()}),e}var n,r=0,o=0,i=!1,c=new Promise(function(e){n=function(){r-o<=0&&i&&e({totalCount:r})}});return e.waitForRequestsToFinish=function(){return i=!0,n(),c},e},e}(o),e=function(i){"use strict";function r(e){return Array.prototype.slice.call(e)}var e={},c={active:!0,hover:!0,focus:!1,target:!1};return e.fakeUserAction=function(e,t,n){var r=e.querySelector(t),o=":"+n,t="rasterizehtml"+n;r&&(c[n]?i.addClassNameRecursively(r,t):i.addClassName(r,t),i.rewriteCssSelectorWith(e,o,"."+t))},e.persistInputValues=function(e){function t(e){return"checkbox"===e.type||"radio"===e.type}var n=e.querySelectorAll("input"),e=e.querySelectorAll("textarea");r(n).filter(t).forEach(function(e){e.checked?e.setAttribute("checked",""):e.removeAttribute("checked")}),r(n).filter(function(e){return!t(e)}).forEach(function(e){e.setAttribute("value",e.value)}),r(e).forEach(function(e){e.textContent=e.value})},e.rewriteTagNameSelectorsToLowerCase=function(e){i.lowercaseCssTypeSelectors(e,i.findHtmlOnlyNodeNames(e))},e}(function(){"use strict";function c(e){return Array.prototype.slice.call(e)}var n={};n.addClassName=function(e,t){e.className+=" "+t},n.addClassNameRecursively=function(e,t){n.addClassName(e,t),e.parentNode!==e.ownerDocument&&n.addClassNameRecursively(e.parentNode,t)};function r(e,t,o){var i="((?:^|[^.#:\\w])|(?=\\W))("+t.join("|")+")(?=\\W|$)";c(e.querySelectorAll("style")).forEach(function(e){var t,n;void 0===e.sheet&&(t=e,n=document.implementation.createHTMLDocument(""),(r=document.createElement("style")).textContent=t.textContent,n.body.appendChild(r),t.sheet=r.sheet);var r=c(e.sheet.cssRules).filter(function(e){return e.selectorText&&new RegExp(i,"i").test(e.selectorText)});r.length&&(r.forEach(function(e){var t,n=e.selectorText.replace(new RegExp(i,"gi"),function(e,t,n){return t+o(n)});n!==e.selectorText&&(t=n,e=(n=e).cssText.replace(/^[^\{]+/,""),u(n,t+" "+e))}),e.textContent=a(e.sheet.cssRules))})}var u=function(e,t){var n=e.parentStyleSheet,e=c(n.cssRules).indexOf(e);n.insertRule(t,e+1),n.deleteRule(e)},a=function(e){return c(e).reduce(function(e,t){return e+t.cssText},"")};return n.rewriteCssSelectorWith=function(e,t,n){r(e,[t],function(){return n})},n.lowercaseCssTypeSelectors=function(e,t){r(e,t,function(e){return e.toLowerCase()})},n.findHtmlOnlyNodeNames=function(e){for(var t,n=e.ownerDocument.createTreeWalker(e,NodeFilter.SHOW_ELEMENT),r={},o={};t=n.currentNode.tagName.toLowerCase(),"http://www.w3.org/1999/xhtml"===n.currentNode.namespaceURI?r[t]=!0:o[t]=!0,n.nextNode(););return Object.keys(r).filter(function(e){return!o[e]})},n}()),i=function(a,f,t,m){"use strict";var e={};e.executeJavascript=function(s,l){return new Promise(function(t){function n(){m.document.getElementsByTagName("body")[0].removeChild(r)}function e(){var e=r.contentDocument;t({document:e,errors:i,cleanUp:n})}var r=function(e,t,n,r){t=e.createElement(t);return t.style.visibility="hidden",t.style.width=n+"px",t.style.height=r+"px",t.style.position="absolute",t.style.top=-1e4-r+"px",t.style.left=-1e4-n+"px",e.getElementsByTagName("body")[0].appendChild(t),t}(m.document,"iframe",l.width,l.height),o=s.outerHTML,i=[],c=l.executeJsTimeout||0,u=r.contentWindow.XMLHttpRequest,a=f.finishNotifyingXhr(u),u=f.baseUrlRespectingXhr(a,l.baseUrl);r.onload=function(){var t;(0<(t=c)?new Promise(function(e){setTimeout(e,t)}):Promise.resolve()).then(a.waitForRequestsToFinish).then(e)},r.contentDocument.open(),r.contentWindow.XMLHttpRequest=u,r.contentWindow.onerror=function(e){i.push({resourceType:"scriptExecution",msg:e})},r.contentDocument.write(""),r.contentDocument.write(o),r.contentDocument.close()})};function s(e,t,n,r,o){var i,c,u,a=Math.max(e.scrollWidth,e.clientWidth),s=Math.max(e.scrollHeight,e.clientHeight),l=t?(i=(l=function(e,t){var n=e.querySelector(t);if(n)return n;if(e.ownerDocument.querySelector(t)===e)return e;throw{message:"Clipping selector not found"}}(e,t).getBoundingClientRect()).top,c=l.left,u=l.width,l.height):(c=i=0,u=a,s);return l={width:u,height:l},r=r,o=o,r={width:Math.max(l.width*o,n),height:Math.max(l.height*o,r)},e=m.getComputedStyle(e.ownerDocument.documentElement).fontSize,{left:c,top:i,width:r.width,height:r.height,viewportWidth:a,viewportHeight:s,rootFontSize:e}}e.calculateDocumentContentSize=function(c,u){return new Promise(function(n,r){var e,t,o=u.zoom||1,i=function(e,t,n){e=Math.floor(e/n),n=Math.floor(t/n);return function(e,t,n){e=e.createElement("iframe");return e.style.width=t+"px",e.style.height=n+"px",e.style.visibility="hidden",e.style.position="absolute",e.style.top=-1e4-n+"px",e.style.left=-1e4-t+"px",e.style.borderWidth=0,e.sandbox="allow-same-origin",e.scrolling="no",e}(m.document,e,n)}(u.width,u.height,o);m.document.getElementsByTagName("body")[0].appendChild(i),i.onload=function(){var e,t=i.contentDocument;try{e=s(function(e,t){e=e.tagName;return t.querySelector(e)}(c,t),u.clip,u.width,u.height,o),n(e)}catch(e){r(e)}finally{m.document.getElementsByTagName("body")[0].removeChild(i)}},i.contentDocument.open(),i.contentDocument.write(""),i.contentDocument.write("html"===(t=(e=c).tagName.toLowerCase())||"body"===t?e.outerHTML:''+e.outerHTML+""),i.contentDocument.close()})},e.parseHtmlFragment=function(e){var t=m.document.implementation.createHTMLDocument("");t.documentElement.innerHTML=e;t=t.querySelector("body").firstChild;if(!t)throw"Invalid source";return t};e.parseHTML=function(e){var t=m.document.implementation.createHTMLDocument("");return t.documentElement.innerHTML=e,function(e,t){var n,r,o,i=/]*)?)>/im.exec(t),t=m.document.implementation.createHTMLDocument("");if(i)for(i="",t.documentElement.innerHTML=i,r=t.querySelector("div"),n=0;n',n=function(){return new Promise(function(t,e){var n;(function(){if(r.Blob)try{return new Blob([""],{type:"text/xml"}),!0}catch(e){}return!1})()&&r.URL?(n=o(a,!0),i(n).then(function(e){return c(n),!e&&i(o(a,!1)).then(function(e){return e})},function(){return!1}).then(function(e){t(!e)},function(){e()})):t(!1)})};return t.renderSvg=function(i){return new Promise(function(e,t){function n(){r&&c(r)}var r,o=new Image;o.onload=function(){o.onload=null,o.onerror=null,n(),e(o)},o.onerror=function(){n(),t()},u(i).then(function(e){r=e,o.src=r},t)})},t}(window);return function(o,i,c){"use strict";var u={};u.drawDocument=function(){var e=arguments[0],t=Array.prototype.slice.call(arguments,1),n=o.parseOptionalParameters(t),r=e.documentElement||e;return c.rasterize(r,n.canvas,(e=(t=n).canvas,r=t.options,n=e?e.width:300,e=e?e.height:200,e={width:void 0!==r.width?r.width:n,height:void 0!==r.height?r.height:e},(t=o.clone(t.options)).width=e.width,t.height=e.height,t))};u.drawHTML=function(){var e=arguments[0],t=Array.prototype.slice.call(arguments,1),t=o.parseOptionalParameters(t);return function(e,t,n){e=i.parseHTML(e);return u.drawDocument(e,t,n)}(e,t.canvas,t.options)};function n(t,n,r){return i.loadDocument(t,r).then(function(e){e=function(e,t,n){var r=document.implementation.createHTMLDocument("");r.replaceChild(e.documentElement,r.documentElement);e=n?o.clone(n):{};return n.baseUrl||(e.baseUrl=t),{document:r,options:e}}(e,t,r);return u.drawDocument(e.document,n,e.options)})}return u.drawURL=function(){var e=arguments[0],t=Array.prototype.slice.call(arguments,1),t=o.parseOptionalParameters(t);return n(e,t.canvas,t.options)},u}(o,i,function(o,i,c,r,e,u){"use strict";function a(t){return e.renderSvg(t).then(function(e){return{image:e,svg:t}},function(e){throw l(e)})}function s(e,t,n){return r.drawDocumentAsSvg(e,n).then(a).then(function(e){return t&&function(e,t){try{t.getContext("2d").drawImage(e,0,0)}catch(e){throw l(e)}}(e.image,t),e})}var t={},l=function(e){return{message:"Error rendering page",originalError:e}};return t.rasterize=function(e,n,r){var t=o.clone(r);return t.inlineScripts=!0===r.executeJs,u.inlineReferences(e,t).then(function(t){return r.executeJs?i.executeJavascript(e,r).then(function(e){var t=e.document;return c.persistInputValues(t),{document:t,errors:e.errors,cleanUp:e.cleanUp}}).then(function(e){return{element:e.document.documentElement,errors:t.concat(e.errors),cleanUp:e.cleanUp}}):{element:e,errors:t,cleanUp:function(){}}}).then(function(t){return s(t.element,n,r).then(function(e){return t.cleanUp(),{image:e.image,svg:e.svg,errors:t.errors}})})},t}(o,i,e,function(c,r,u){"use strict";function a(t){var e=Object.keys(t);return e.length?" "+e.map(function(e){return e+'="'+t[e]+'"'}).join(" "):""}function o(e,t,n){var r,o,i=u.serializeToString(e);return c.validateXHTML(i),(e=(r=t,o=Math.round(r.viewportWidth),e=Math.round(r.viewportHeight),{x:-r.left,y:-r.top,width:o,height:e})).style=(e.style||"")+"float: left;",e.externalResourcesRequired=!0,'"+i+""}var i={};return i.getSvgForDocument=function(e,t,n){return r.rewriteTagNameSelectorsToLowerCase(e),o(e,t,n)},i.drawDocumentAsSvg=function(t,n){return["hover","active","focus","target"].forEach(function(e){n[e]&&r.fakeUserAction(t,n[e],e)}),c.calculateDocumentContentSize(t,n).then(function(e){return i.getSvgForDocument(t,e,n.zoom)})},i}(i,e,t),n,r))}); // the XRWG (XR WordGraph)is mentioned in the spec // // it collects metadata-keys ('foo' e.g.), names and tags across 3D scene-nodes (.userData.foo e.g.) let XRWG = xrf.XRWG = [] XRWG.word = (key) => XRWG.find( (w) => w.word == word ) XRWG.cleankey = (word) => String(word).replace(/[^0-9\.a-zA-Z_]/g,'') .toLowerCase() .replace(/.*:\/\//,'') XRWG.get = (v,k) => XRWG.find( (x) => x[ k || 'word'] == v ) XRWG.match = (str,types,level) => { if( XRWG.length == 0 ) XRWG.generate(xrf) level = level == undefined ? 1000 : level types = types || [] let res = XRWG.filter( (n) => { types.map( (type) => n[type] ? n = false : false ) return n }) str = str.toLowerCase() .replace(/[!-\*]/g,'') // 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 ) if( level >40 ) res = res.filter( (n) => n.word.match(str) || n.key == str || String(n.value||'').match(str) ) if( level >999 ) res = res.filter( (n) => n.word.match(str) != null || n.key.match(str) != null || String(n.value||'').match(str) != null) return res } XRWG.generate = (opts) => { let {scene,model} = opts if( !scene ) return console.warn('xrfragment: XRWG cannot be generated from empty (nonloaded?) scene') XRWG.slice(0,0) // empty // collect words from 3d nodes let add = (key, spatialNode, type) => { if( !key || key.match(/(^#$|name)/) ) return let node = XRWG.get( XRWG.cleankey(key) ) if( node ){ node.types.push(type) node.nodes.push(spatialNode) }else{ node = { word: XRWG.cleankey(key), key, nodes:[spatialNode], types:[] } if( spatialNode.userData[key] ) node.value = spatialNode.userData[key] node.types.push(type) xrf.emit('XRWGnode',node) XRWG.push( node ) } } scene.traverse( (o) => { add( `#${o.name}`, o, 'name') for( let k in o.userData ){ if( k == 'tag' ){ let tagArr = o.userData.tag.split(" ") .map( (t) => t.trim() ) .filter( (t) => t ) .map( (w) => add( w, o, 'tag') ) }else if( k.match(/^(href|src)$/) ) add( o.userData[k], o, k) else if( k[0] == '#' ) add( k, o , 'pv') else add( k, o , 'query') } }) // sort by n XRWG.sort( (a,b) => a.nodes.length - b.nodes.length ) 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) ) } } }) } // the hashbus (QueryString eventBus) is mentioned in the spec // // it allows metadata-keys ('foo' e.g.) of 3D scene-nodes (.userData.foo e.g.) to // react by executing code let pub = function( url, node_or_model, flags ){ // evaluate fragments in url if( !url ) return if( !url.match(/#/) ) url = `#${url}` let { THREE, camera } = xrf let frag = xrf.URI.parse( url, flags ).XRF let fromNode = node_or_model != xrf.model let isNode = node_or_model && node_or_model.children let opts = { frag, mesh: fromNode ? node_or_model : xrf.camera, model: xrf.model, camera: xrf.camera, scene: isNode ? node_or_model : xrf.scene, renderer: xrf.renderer, THREE: xrf.THREE, hashbus: xrf.hashbus } xrf.emit('hashbus',opts) .then( () => { for ( let k in frag ){ 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 } 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(k,opts) // call native function (xrf/env.js e.g.), or pass it to user decorator if( xrf.frag[k] ){ xrf.emit(k,opts) .then( () => { let func = xrf.frag[k] || function(){} func( frag, 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 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 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 }) } } } xrf.hashbus = { pub } xrf.frag = {dynamic:{}} xrf.model = {} xrf.mixers = [] xrf.init = ((init) => function(opts){ // operate in own subscene let scene = new opts.THREE.Group() xrf.clock = new opts.THREE.Clock() // don't mess with original scene object // but with our own sub-scene opts.scene.add(scene) opts.sceneRoot = opts.scene opts.scene = scene init(opts) //if( opts.loaders ) Object.values(opts.loaders).map( xrf.patchLoader ) xrf.patchRenderer(opts) xrf.navigator.init() xrf.interactive = xrf.interactiveGroup( xrf.THREE, xrf.renderer, xrf.camera) // return xrfragment lib as 'xrf' query functor (like jquery) for ( let i in xrf ) xrf.query[i] = xrf[i] return xrf.query })(xrf.init) xrf.patchRenderer = function(opts){ let {renderer,camera} = opts renderer.xr.addEventListener( 'sessionstart', () => xrf.baseReferenceSpace = renderer.xr.getReferenceSpace() ); renderer.xr.enabled = true; renderer.render = ((render) => function(scene,camera){ // update clock 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 })(renderer.render.bind(renderer)) } xrf.getFile = (url) => url.split("/").pop().replace(/#.*/,'') // parseModel event is essential for src.js to hook into embedded loaded models xrf.parseModel = function(model,url){ let file = xrf.getFile(url) model.file = file model.isXRF = true model.scene.isXRFRoot = true model.scene.traverse( (n) => { n.isXRF = true }) // mark for deletion during reset() xrf.emit('parseModel',{model,url,file}) } xrf.loadModel = function(model,url,noadd){ let URI = xrfragment.URI.toAbsolute( xrf.navigator.URI, url ) let {directory,file,fragment,fileExt} = URI; model.file = URI.file xrf.model = model if( !model.isXRF ) xrf.parseModel(model,url.replace(directory,"")) // this marks the model as an XRF model if(xrf.debug ) model.animations.map( (a) => console.log("anim: "+a.name) ) // spec: 1. generate the XRWG xrf.XRWG.generate({model,scene:model.scene}) // spec: 2. init metadata inside model for non-SRC data if( !model.isSRC ){ model.scene.traverse( (mesh) => xrf.parseModel.metadataInMesh(mesh,model) ) } // spec: 1. execute the default predefined view '#' (if exist) (https://xrfragment.org/#predefined_view) const defaultFragment = xrf.frag.defaultPredefinedViews({model,scene:model.scene}) // spec: predefined view(s) & objects-of-interest-in-XRWG from URI (https://xrfragment.org/#predefined_view) let frag = xrf.hashbus.pub( url, model) // and eval URI XR fragments if( !noadd ) xrf.add( model.scene ) // only change url when loading *another* file fragment = fragment || defaultFragment || '' xrf.navigator.pushState( URI.external ? URI.URN + URI.file : URI.file, fragment.replace(/^#/,'') ) //if( fragment ) xrf.navigator.updateHash(fragment) xrf.emit('navigateLoaded',{url,model}) } xrf.parseModel.metadataInMesh = (mesh,model) => { if( mesh.userData ){ let frag = {} for( let k in mesh.userData ) xrf.Parser.parse( k, mesh.userData[k], frag ) for( let k in frag ){ let opts = {frag, mesh, model, camera: xrf.camera, scene: model.scene, renderer: xrf.renderer, THREE: xrf.THREE, hashbus: xrf.hashbus } mesh.userData.XRF = frag // allow fragment impl to access XRF obj already xrf.emit('frag2mesh',opts) .then( () => { xrf.hashbus.pub.fragment(k, {...opts, skipXRWG:true}) }) } } } xrf.getLastModel = () => xrf.model.last xrf.reset = () => { // allow others to reset certain events xrf.emit('reset',{}) // reattach camera to root scene xrf.scene.attach(xrf.camera) xrf.camera.position.set(0,0,0) xrf.camera.updateMatrixWorld() xrf.camera.getCam().updateMatrixWorld() const disposeObject = (obj) => { if (obj.children.length > 0){ obj.children.forEach((child) => disposeObject(child)); } if (obj.geometry) obj.geometry.dispose(); if (obj.material) { if (obj.material.map) obj.material.map.dispose(); obj.material.dispose(); } obj.clear() obj.removeFromParent() return true }; // also remove XRF objects from global scene let nodes = [] xrf.scene.traverse( (child) => child.isXRF && (nodes.push(child)) ) nodes.map( disposeObject ) xrf.interactive.clear() xrf.layers = 0 } xrf.add = (object) => { object.isXRF = true // mark for easy deletion when replacing scene xrf.scene.add(object) } xrf.hasNoMaterial = (mesh) => { const hasTexture = mesh.material && mesh.material.map const hasMaterialName = mesh.material && mesh.material.name.length > 0 return mesh.geometry && !hasMaterialName && !hasTexture } xrf.navigator = { opts: { openInNewTab: true }, URI: xrf.URI.parse(document.location.href) // scheme: document.location.protocol.replace(/:$/,''), // directory: document.location.pathname, // host: document.location.hostname, // port: document.location.port, // file: 'index.glb' // } } xrf.navigator.to = (url,flags,loader,data) => { if( !url ) throw 'xrf.navigator.to(..) no url given' let hashbus = xrf.hashbus let URI if( typeof url == 'string' ){ URI = xrfragment.URI.toAbsolute( xrf.navigator.URI, url ) URI.hash = xrf.navigator.reactifyHash(URI.hash) // automatically reflect hash-changes to navigator.to(...) // decorate with extra state URI.fileChange = URI.file && URI.URN + URI.file != xrf.navigator.URI.URN + xrf.navigator.URI.file console.log( URI.URN + URI.file ) console.log( xrf.navigator.URI.URN + xrf.navigator.URI.file ) URI.external = URI.file && URI.URN != document.location.origin + document.location.pathname URI.hasPos = URI.hash.pos ? true : false URI.duplicatePos = URI.source == xrf.navigator.URI.source && URI.hasPos URI.hashChange = String(xrf.navigator.URI.fragment||"") != String(URI.fragment||"") }else{ URI = url url = URI.source } URI.last = xrf.navigator.URI xrf.navigator.URI = URI let {directory,file,fragment,fileExt} = URI; const evalFragment = () => { if( URI.fragment ){ hashbus.pub( URI.fragment, xrf.model, flags ) // eval local URI XR fragments xrf.navigator.updateHash(fragment) // which don't require } } return new Promise( (resolve,reject) => { xrf .emit('navigate', {url,loader,data}) .then( () => { const Loader = xrf.loaders[fileExt] if( fileExt && !loader ){ if( !Loader ) return resolve() loader = loader || new Loader().setPath( URI.URN ) } if( URI.duplicatePos || (!Object.values(URI.XRF).length && !URI.file && !URI.fileExt) ){ return resolve(xrf.model) // nothing we can do here } if( xrf.model && !URI.fileChange && URI.hashChange && !URI.hasPos ){ evalFragment() return resolve(xrf.model) // eval non-positional fragments (no loader needed) } xrf .emit('navigateLoading', {url,loader,data}) .then( () => { if( (!URI.fileChange || !file) && URI.hashChange && URI.hasPos ){ // we're already loaded evalFragment() xrf.emit('navigateLoaded',{url}) return resolve(xrf.model) } // clear xrf objects from scene if( xrf.model && xrf.model.scene ) xrf.model.scene.visible = false xrf.reset() // force relative path for files which dont include protocol or relative path if( directory ) directory = directory[0] == '.' || directory.match("://") ? directory : `.${directory}` if( loader || Loader ){ const onLoad = (model) => { xrf.loadModel(model,url) resolve(model) } loader = loader || new Loader().setPath( URI.URN ) if( data ){ // file upload loader.parse(data, "", onLoad ) }else{ try{ loader.load(file, onLoad ) }catch(e){ console.error(e) xrf.emit('navigateError',{url}) } } }else xrf.emit('navigateError',{url,URI}) }) }) }) } xrf.navigator.init = () => { if( xrf.navigator.init.inited ) return xrf.navigator.URI = xrfragment.URI.parse(document.location.href) window.addEventListener('popstate', function (event){ if( xrf.navigator.updateHash.active ){ // ignore programmatic hash updates (causes infinite recursion) xrf.navigator.to( xrf.navigator.URI.last ) } }) window.addEventListener('hashchange', function (e){ xrf.emit('hash', {hash: document.location.hash }) }) xrf.navigator.setupNavigateFallbacks() // this allows selectionlines to be updated according to the camera (renderloop) xrf.focusLine = new xrf.THREE.Group() xrf.focusLine.material = new xrf.THREE.LineDashedMaterial({color:0xFF00FF,linewidth:3, scale: 1, dashSize: 0.2, gapSize: 0.1,opacity:0.3, transparent:true}) xrf.focusLine.isXRF = true xrf.focusLine.position.set(0,0,-0.5); xrf.focusLine.points = [] xrf.focusLine.lines = [] xrf.camera.add(xrf.focusLine) xrf.navigator.init.inited = true } xrf.navigator.setupNavigateFallbacks = () => { xrf.addEventListener('navigate', (opts) => { let {url} = opts let {fileExt} = xrfragment.URI.parse(url) // handle http links if( url.match(/^http/) && url != xrf.navigator.URI.URN && !xrf.loaders[fileExt] ){ let inIframe try { inIframe = window.self !== window.top; } catch (e) { inIframe = true; } if( inIframe ){ window.parent.postMessage({ url }, '*') }else{ if( xrf.navigator.opts.openInNewTab ) window.open( url, '_blank') else{ setTimeout( () => document.location.href = url, 1000) } } // in case you're running in an iframe, then use this in the parent page: // // window.addEventListener("message", (e) => { // if (e.data && e.data.url){ // window.open( e.data.url, '_blank') // } // }, // false, // ); } }) } xrf.navigator.updateHash = (hash,opts) => { if( hash.replace(/^#/,'') == document.location.hash.substr(1) || hash.match(/\|/) ) return // skip unnecesary pushState triggers console.log(`URI: ${document.location.search.substr(1)}#${hash}`) xrf.navigator.updateHash.active = false // important to prevent recursion document.location.hash = hash xrf.navigator.updateHash.active = true } xrf.navigator.updateHash.active = true xrf.navigator.pushState = (file,hash) => { if( file == document.location.search.substr(1) ) return // page is in its default state window.history.pushState({}, '', document.location.pathname + `?${xrf.navigator.URI.source.replace(/#.*/,'')}#${hash}` ) xrf.emit('pushState', {file, hash} ) } xrf.navigator.reactifyHash = ( obj ) => { return new Proxy(obj,{ get(me,k) { return me[k] }, set(me,k,v){ me[k] = v if( xrf.navigator.updateHash.active ){ xrf.navigator.to( "#" + this.toString(me) ) } xrf.navigator.URI.fragment = this.toString(me) }, toString(me){ let parts = [] Object.keys(me).map( (k) => { parts.push( me[k] ? `${k}=${me[k]}` : k ) }) return parts.join('&') } }) } /** * * navigation, portals & mutations * * | fragment | type | scope | example value | * |`href`| string (uri or predefined view) | 🔒 |`#pos=1,1,0`
`#pos=1,1,0&rot=90,0,0`
`#pos=pyramid`
`#pos=lastvisit|pyramid`
`://somefile.gltf#pos=1,1,0`
| * * [[» example implementation|https://github.com/coderofsalvation/xrfragment/blob/main/src/3rd/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]]
* * [img[xrfragment.jpg]] * * * !!!spec 1.0 * * 1. an ''external''- or ''file URI'' fully replaces the current scene and assumes `pos=0,0,0&rot=0,0,0` by default (unless specified) * * 2. navigation should not happen when queries (`q=`) are present in local url: queries will apply (`pos=`, `rot=` e.g.) to the targeted object(s) instead. * * 3. navigation should not happen ''immediately'' when user is more than 2 meter away from the portal/object containing the href (to prevent accidental navigation e.g.) * * 4. URL navigation should always be reflected in the client (in case of javascript: see [[here|https://github.com/coderofsalvation/xrfragment/blob/dev/src/3rd/three/navigator.js]] for an example navigator). * * 5. In XR mode, the navigator back/forward-buttons should be always visible (using a wearable e.g., see [[here|https://github.com/coderofsalvation/xrfragment/blob/dev/example/aframe/sandbox/index.html#L26-L29]] for an example wearable) * * [img[navigation.png]] * */ xrf.frag.href = function(v, opts){ let { frag, mesh, model, camera, scene, renderer, THREE} = opts if( mesh.userData.XRF.href.exec ) return // mesh already initialized // correct for relative urls if( v.string.charAt(0) != '#' && xrf.URI.isRelative( xrf.URI.parse( v.string ) ) ){ v.string = xrf.navigator.URI.URN + v.string } let click = mesh.userData.XRF.href.exec = (e) => { if( !mesh.material || !(mesh.material && mesh.material.visible) ) return // ignore invisible nodes // update our values to the latest value (might be edited) let URI = xrf.URI.template( mesh.userData.href, xrf.URI.vars.__object ) xrf.Parser.parse( "href", URI, frag ) const v = frag.href // bubble up! mesh.traverseAncestors( (n) => n.userData && n.userData.href && n.dispatchEvent({type:e.type,data:{}}) ) let fragValue = xrf.URI.parse( v.string, xrf.XRF.NAVIGATOR | xrf.XRF.PV_OVERRIDE | xrf.XRF.METADATA ) let lastPos = `pos=${camera.position.x.toFixed(2)},${camera.position.y.toFixed(2)},${camera.position.z.toFixed(2)}` xrf .emit('href',{click:true,mesh,xrf:v,value: fragValue}) // let all listeners agree .then( () => { const isLocal = v.string[0] == '#' const hasPos = isLocal && v.string.match(/pos=/) const flags = isLocal ? xrf.XRF.PV_OVERRIDE : undefined if( v.xrfScheme ){ xrf.hashbus.pub(v.string) } else xrf.navigator.to(v.string) // let's surf }) .catch( console.error ) } let selected = mesh.userData.XRF.href.selected = (state) => () => { if( (!mesh.material && !mesh.material.visible) && !mesh.isSRC ) return // ignore invisible nodes if( mesh.selected == state ) return // nothing changed xrf.interactive.objects.map( (o) => { let newState = o.name == mesh.name ? state : false if( o.material ){ 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() o.material.emissive.r = o.material.emissive.g = o.material.emissive.b = newState ? o.material.emissive.original.r + 0.5 : o.material.emissive.original.r } } }) // update mouse cursor if( !renderer.domElement.lastCursor ) renderer.domElement.lastCursor = renderer.domElement.style.cursor renderer.domElement.style.cursor = state ? 'pointer' : renderer.domElement.lastCursor xrf .emit('href',{selected:state,mesh,xrf:v}) // let all listeners agree .then( () => mesh.selected = state ) } mesh.addEventListener('click', click ) mesh.addEventListener('mousemove', selected(true) ) mesh.addEventListener('mouseenter', selected(true) ) mesh.addEventListener('mouseleave', selected(false) ) if( mesh.material ) mesh.material = mesh.material.clone() // clone, so we can individually highlight meshes // lazy add mesh (because we're inside a recursive traverse) setTimeout( (mesh) => { xrf.interactive.add(mesh) xrf.emit('interactionReady', {mesh,xrf:v,clickHandler: mesh.userData.XRF.href.exec }) }, 0, mesh ) } xrf.addEventListener('audioInited', function(opts){ let {THREE,listener} = opts opts.audio = opts.audio || {} opts.audio.click = opts.audio.click || '/dist/audio/click.wav' opts.audio.hover = opts.audio.hover || '/dist/audio/hover.wav' opts.audio.teleport = opts.audio.teleport || '/dist/audio/teleport.wav' let audio = xrf.frag.href.audio = {} const actions = ['click','hover','teleport'] actions.map( (action) => { const audioLoader = new THREE.AudioLoader(); audio[action] = new THREE.Audio( xrf.camera.listener ) audioLoader.load( opts.audio[action], function( buffer ) { audio[action].setBuffer( buffer ); }) }); xrf.addEventListener('href', (opts) => { let v = opts.xrf if( opts.selected ){ xrf.frag.href.audio.hover.stop() xrf.frag.href.audio.hover.play() return } if( opts.click ){ xrf.frag.href.audio.click.stop() xrf.frag.href.audio.click.play() return } }) xrf.addEventListener('navigateLoading', (e) => { xrf.frag.href.audio.click.stop() xrf.frag.href.audio.teleport.stop() xrf.frag.href.audio.teleport.play() }) }) /** * > above solutions were abducted from [[this|https://i.imgur.com/E3En0gJ.png]] and [[this|https://i.imgur.com/lpnTz3A.png]] survey result * * !!!Demo * * <$videojs controls="controls" aspectratio="16:9" preload="auto" poster="" fluid="fluid" class="vjs-big-play-centered"> * * * * > capture of aframe/sandbox */ // this is called by navigator.js rather than by a URL e.g. xrf.frag['#'] = xrf.frag.defaultPredefinedViews = (opts) => { let {scene,model} = opts; if( !scene ) return let defaultFragment; scene.traverse( (n) => { if( n.userData && n.userData['#'] ){ if( n.isXRFRoot ){ defaultFragment = n.userData['#'] } xrf.hashbus.pub( n.userData['#'], n ) // evaluate default XR fragments without affecting URL } }) return defaultFragment } xrf.frag.loop = 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].set("loop",v) return } // otherwise handle global 3D animations xrf.mixers.map ( (mixer) => { // update loop mixer.loop.enabled = v.loop }) } xrf.frag.pos = function(v, opts){ let { frag, mesh, model, camera, scene, renderer, THREE} = opts if( !scene )return let pos = v // spec: indirect coordinate using objectname: https://xrfragment.org/#navigating%203D if( pos.x == undefined ){ let obj = scene.getObjectByName(v.string) if( !obj ) return console.warn("#pos="+v.string+" not found") obj.add(camera) // follow animation of targeted position camera.position.set(0,0,0) // set playerheight //let c = camera.rotation //c.set( c.x, obj.rotation.y, c.z ) }else{ // spec: direct coordinate: https://xrfragment.org/#navigating%203D camera.position.x = pos.x camera.position.y = pos.y camera.position.z = pos.z } if( xrf.debug ) console.log(`#pos.js: setting camera to position ${pos.x},${pos.y},${pos.z}`) xrf.frag.pos.last = v.string // remember xrf.frag.pos.lastVector3 = camera.position.clone() camera.updateMatrixWorld() camera.getCam().updateMatrixWorld() } xrf.frag.pos.get = function(precision,randomize){ if( !precision ) precision = 2; if( typeof THREE == 'undefined' ) THREE = xrf.THREE let radToDeg = THREE.MathUtils.radToDeg let toDeg = (x) => x / (Math.PI / 180) let camera = xrf.camera if( randomize ){ camera.position.x += Math.random()/10 camera.position.z += Math.random()/10 } // *TODO* add camera direction let direction = new xrf.THREE.Vector3() camera.getWorldDirection(direction) const pitch = Math.asin(direction.y); const yaw = Math.atan2(direction.x, direction.z); const pitchInDegrees = pitch * 180 / Math.PI; const yawInDegrees = yaw * 180 / Math.PI; return { x: String(camera.position.x.toFixed(2)), y: String(camera.position.y.toFixed(2)), z: String(camera.position.z.toFixed(2)), } } xrf.addEventListener('reset', (opts) => { // set the player to position 0,0,0 xrf.camera.position.set(0,0,0) }) xrf.frag.rot = function(v, opts){ let { frag, mesh, model, camera, scene, renderer, THREE} = opts if( xrf.debug ) console.log("#rot.js: setting camera rotation to "+v.string) if( !camera || !scene ) return if( !model.isSRC ){ camera.rotation.set( v.x * Math.PI / 180, v.y * Math.PI / 180, v.z * Math.PI / 180 ) camera.rotation.offset = camera.rotation.clone() // remember //camera.updateProjectionMatrix() }else{ obj = model.scene.isReparented ? model.scene.children[0] : model.scene obj.rotation.set( v.x * Math.PI / 180, v.y * Math.PI / 180, v.z * Math.PI / 180 ) } } xrf.frag.s = 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].set("s",v) return } // otherwise handle global 3D animations xrf.mixers.map ( (mixer) => { mixer.s = v // update speed mixer.timeScale = v.x || 1.0 mixer.loop.speed = v.x || 1.0 mixer.loop.speedAbs = Math.abs( v.x ) }) } // *TODO* use webgl instancing xrf.frag.src = function(v, opts){ opts.embedded = v // indicate embedded XR fragment let { mesh, model, camera, scene, renderer, THREE, hashbus, frag} = opts if( mesh.isSRC ) return // only embed src once opts.isLocal = v.string[0] == '#' opts.isPortal = xrf.frag.src.renderAsPortal(mesh) opts.isSRC = mesh.isSRC = true // correct for relative urls let url = v.string if( v.string.charAt(0) != '#' && xrf.URI.isRelative( xrf.URI.parse( v.string ) ) ){ url = xrf.navigator.URI.URN + v.string } url = xrf.frag.src.expandURI( mesh, url ) let srcFrag = opts.srcFrag = xrfragment.URI.parse(url).XRF 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) => { let {mesh} = opts let scene = model.scene scene = xrf.frag.src.filterScene(scene,{...opts,frag}) // get filtered scene if( mesh.material && mesh.userData.src ) mesh.material.visible = false // hide placeholder object if( opts.isPortal ){ xrf.portalNonEuclidian({...opts,model,scene:model.scene}) // only add external objects, because // local scene-objects are already added to scene if( !opts.isLocal ) xrf.add(scene) }else{ xrf.frag.src.scale( scene, opts, url ) // scale scene mesh.add(scene) } xrf.frag.src.enableSourcePortation({...opts, scene,mesh,url,model}) // flag everything isSRC & isXRF mesh.isXRF = scene.isXRF = true mesh.traverse( (n) => { n.isSRC = n.isXRF = n[ opts.isLocal ? 'isSRCLocal' : 'isSRCExternal' ] = true }) xrf.emit('parseModel', {...opts, isSRC:true, mesh, model}) // this will execute all embedded metadata/fragments e.g. } xrf.frag.src.renderAsPortal = (mesh) => { // *TODO* should support better isFlat(mesh) check const isPlane = mesh.geometry && mesh.geometry.attributes.uv && mesh.geometry.attributes.uv.count == 4 return xrf.hasNoMaterial(mesh) && isPlane } xrf.frag.src.enableSourcePortation = (opts) => { let {scene,mesh,url,model,THREE} = opts if( url[0] == '#' ) return url = url.replace(/(&)?[-][\w-+\.]+(&)?/g,'&') // remove negative selectors to refer to original scene if( !mesh.userData.href ){ // show sourceportation clickable sphere for non-portals let scale = new THREE.Vector3() let size = new THREE.Vector3() scene.getWorldScale(scale) new THREE.Box3().setFromObject(scene).getSize(size) const geo = new THREE.SphereGeometry( Math.max(size.x, size.y, size.z) * scale.x * 0.33, 10, 10 ) const mat = new THREE.MeshBasicMaterial() mat.visible = false // we just use this for collisions const sphere = new THREE.Mesh( geo, mat ) sphere.isXRF = true // reparent scene to sphere let children = mesh.children mesh.children = [] mesh.add(sphere) children.map( (c) => sphere.add(c) ) // make sphere clickable/hoverable let frag = {} xrf.Parser.parse("href", url, frag) sphere.userData = scene.userData // allow rich href notifications/hovers sphere.userData.href = url.replace(/#.*/,'') // remove fragments to refer to original scene sphere.userData.XRF = frag xrf.hashbus.pub.fragment("href", {...opts, mesh:sphere, frag, skipXRWG:true, renderer:xrf.renderer, camera:xrf.camera }) } for ( let i in scene.userData ) { if( !mesh.userData[i] ) mesh.userData[i] = scene.userData[i] // allow rich href notifications/hovers } } xrf.frag.src.externalSRC = (url,frag,opts) => { fetch(url, { method: 'HEAD' }) .then( (res) => { let mimetype = res.headers.get('Content-type') if(xrf.debug != undefined ) console.log("HEAD "+url+" => "+mimetype) 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) }) .then( (model) => { if( model && model.scene ) xrf.frag.src.addModel(model, url, frag, opts ) }) .finally( () => { }) .catch( console.error ) return xrf.frag.src } xrf.frag.src.localSRC = (url,frag,opts) => { let {model,mesh,scene} = opts //setTimeout( (mesh,scene) => { if( mesh.material ) mesh.material = mesh.material.clone() // clone, so we can individually highlight meshes let _model = { animations: model.animations, scene: scene.clone() } _model.scene.isClone = true _model.scene.traverse( (n) => n.isXRF = true ) // make sure they respond to xrf.reset() _model.scenes = [_model.scene] xrf.frag.src.addModel(_model,url,frag, opts) // current file //},1000,mesh,scene ) } // scale embedded XR fragments https://xrfragment.org/#scaling%20of%20instanced%20objects xrf.frag.src.scale = function(scene, opts, url){ let { mesh, model, camera, renderer, THREE} = opts // remove invisible objects (hidden by selectors) which might corrupt boundingbox size-detection let cleanScene = scene.clone() let remove = [] const notVisible = (n) => !n.visible || (n.material && !n.material.visible) cleanScene.traverse( (n) => notVisible(n) && n.children.length == 0 && (remove.push(n)) ) remove.map( (n) => n.removeFromParent() ) let restrictTo3DBoundingBox = mesh.geometry if( restrictTo3DBoundingBox ){ // spec 3 of https://xrfragment.org/#src // spec 1 of https://xrfragment.org/#scaling%20of%20instanced%20objects // normalize instanced objectsize to boundingbox let sizeFrom = new THREE.Vector3() let sizeTo = new THREE.Vector3() let empty = new THREE.Object3D() new THREE.Box3().setFromObject(mesh).getSize(sizeTo) new THREE.Box3().setFromObject(cleanScene).getSize(sizeFrom) let ratio = sizeFrom.divide(sizeTo) scene.scale.multiplyScalar( 1.0 / Math.max(ratio.x, ratio.y, ratio.z)); }else{ // spec 4 of https://xrfragment.org/#src // spec 2 of https://xrfragment.org/#scaling%20of%20instanced%20objects scene.scale.multiply( mesh.scale ) } } xrf.frag.src.filterScene = (scene,opts) => { let { mesh, model, camera, renderer, THREE, hashbus, frag} = opts scene = xrf.filter.scene({scene,frag,reparent:true}) //,copyScene: opts.isPortal}) if( !opts.isLocal ){ scene.traverse( (m) => { if( m.userData && (m.userData.src || m.userData.href) ) return ; // prevent infinite recursion xrf.parseModel.metadataInMesh(m,{scene,recursive:true}) }) } return scene } /* * replace the src-mesh with the contents of the src */ xrf.frag.src.type = {} /* * mimetype: unknown */ xrf.frag.src.type['unknown'] = function( url, opts ){ return new Promise( (resolve,reject) => { reject(`${url} mimetype '${opts.mimetype}' not found or supported (yet)`) }) } // this ns 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].set("t",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') return xrf.emit( v.x == 0 ? 'stop' : 'play',{isPlaying: v.x != 0 }) } xrf.mixers.map ( (mixer) => { mixer.t = v // update speed mixer.timeScale = mixer.loop.speed mixer.loop.speedAbs = Math.abs( mixer.timeScale ) mixer.updateLoop( v ) // play animations mixer.play( v ) }) } xrf.frag.t.default = { x:0, // (play from) offset (in seconds) y:0 // optional: (stop at) offset (in seconds) } // setup animation mixer for global scene & src scenes 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,speed:1.0,enabled:false} mixer.i = xrf.mixers.length mixer.actions = [] // calculate total duration/frame based on longest animation mixer.duration = 0 if( model.animations.length ){ model.animations.map( (a) => mixer.duration = ( a.duration > mixer.duration ) ? a.duration : mixer.duration ) } model.animations.map( (anim) => { console.log("animation action: "+anim.name) mixer.actions.push( mixer.clipAction( anim, model.scene ) ) }) mixer.play = (t) => { let msg = `media fragment: ${t.x}-${t.y} seconds` if( t.x > 49 ) msg += ", not frames (!)" console.log(msg) mixer.isPlaying = t.x !== undefined && t.x != t.y mixer.updateLoop(t) xrf.emit( mixer.isPlaying === false ? 'stop' : 'play',{isPlaying: mixer.isPlaying}) } mixer.stop = () => { mixer.play(false) } mixer.updateLoop = (t) => { if( t ){ 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 && t.x != undefined ) action.play() } }) mixer.setTime(mixer.loop.timeStart) mixer.time = Math.abs( mixer.loop.timeStart ) mixer.update(0) } // monkeypatch: update loop when needed if( !mixer.update.patched ){ let update = mixer.update mixer.update = function(time){ mixer.time = Math.abs(mixer.time) if( time == 0 ) return update.call(this,time) // loop jump if( mixer.loop.timeStop > 0 && mixer.time > mixer.loop.timeStop ){ if( mixer.loop.enabled ){ setTimeout( () => mixer.updateLoop(), 0 ) // prevent recursion }else{ mixer.setTime( mixer.time = mixer.loop.timeStop ) //mixer.update(mixer.time ) mixer.timeScale = 0 } } return update.call( this, time ) } mixer.update.patched = true } xrf.mixers.push(mixer) }) if( document.location.hash.match(/t=/) ){ let url = document.location.href let playAfterUserGesture = () => { xrf.hashbus.pub(url) // re-post t fragment on the hashbus again window.removeEventListener('click',playAfterUserGesture) window.removeEventListener('touchstart',playAfterUserGesture) } window.addEventListener('click', playAfterUserGesture ) window.addEventListener('touchstart', playAfterUserGesture ) } xrf.addEventListener('render', (opts) => { let model = xrf.model let {time} = opts if( !model ) return if( xrf.mixers.length ){ xrf.mixers.map( (m) => m.isPlaying && (m.update( time )) ) // update active camera in case selected by dynamicKey in URI if( xrf.model.camera && xrf.model.camera.length && model.mixer.isPlaying ){ let cam = xrf.camera.getCam() // cam.fov = model.cameras[0].fov (why is blender not exporting radians?) cam.far = model.cameras[0].far cam.near = model.cameras[0].near let rig = xrf.camera rig.position.copy( model.cameras[0].position ) rig.position.y -= rig.offsetY // VR/AR compensate camera rig //rig.rotation.copy( model.cameras[0].rotation ) rig.updateProjectionMatrix() } } }) // remove mixers and stop mixers when loading another scene xrf.addEventListener('reset', (opts) => { xrf.mixers.map( (m) => m.stop()) xrf.mixers = [] }) 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 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] || 1.0 mesh.uv.vspeed = v.floats[3] || 1.0 mesh.uv.ushift = v.shift[0] || v.floats[0] < 0 // negative u is always relative mesh.uv.vshift = v.shift[1] || v.floats[1] < 0 // negative v is always relative mesh.uv.uloop = v.shift[2] || false mesh.uv.vloop = v.shift[3] || false 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} let uv = mesh.geometry.getAttribute("uv") if( !uv.old ) uv.old = mesh.geometry.getAttribute("uv").clone() } xrf.frag.uv.scroll = function(){ 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++ ){ if( this.uv.uspeed == 1.0 ) uv.setX(i, this.uv.ushift ? uv.getX(i) + this.uv.u : uv.old.getX(i) + this.uv.u ) if( this.uv.vspeed == 1.0 ) uv.setY(i, this.uv.vshift ? uv.getY(i) + this.uv.v : uv.old.getY(i) + this.uv.v ) if( this.uv.uspeed != 1.0 || this.uv.vspeed != 1.0 ){ let u = uv.getX(i) let v = uv.getY(i) let uTarget = this.uv.ushift ? uv.getX(i) + this.uv.u : uv.old.getX(i) + this.uv.u let vTarget = this.uv.vshift ? uv.getY(i) + this.uv.v : uv.old.getY(i) + this.uv.v // scroll U if( this.uv.uloop ){ u += this.uv.uspeed * xrf.clock.delta }else{ // *TODO* tween to offset //// recover from super-high uv-values due to looped scrolling //if( Math.abs(u-uTarget) > 10.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 V if( this.uv.vloop ){ v += this.uv.vspeed * xrf.clock.delta }else{ // *TODO* tween to offset //// recover from super-high uv-values due to looped scrolling //// recover from super-high uv-values due to looped scrolling //if( Math.abs(v-vTarget) > 10.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 ) } 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(){} } } xrf.getCollisionMeshes = () => { let meshes = [] xrf.scene.traverse( (n) => { if( n.type == 'Mesh' && !n.userData.href && !n.userData.src && xrf.hasNoMaterial(n) ){ meshes.push(n) } }) return meshes } // wrapper to collect interactive raycastable objects xrf.interactiveGroup = function(THREE,renderer,camera){ let { Group, Matrix4, Raycaster, Vector2 } = THREE const _pointer = new Vector2(); const _event = { type: '', data: _pointer }; let object = {selected:false} class interactive extends Group { constructor( renderer, camera ) { super(); if( !renderer || !camera ) return // extract camera when camera-rig is passed camera.traverse( (n) => String(n.type).match(/Camera/) ? camera = n : null ) const scope = this; scope.objects = [] scope.raycastAll = false const raycaster = this.raycaster = new Raycaster(); const tempMatrix = new Matrix4(); // Pointer Events const element = renderer.domElement; const getAllMeshes = (scene) => { let objects = [] xrf.scene.traverse( (n) => { if( !n.material || n.type != 'Mesh' ) return objects.push(n) }) return objects } function onPointerEvent( event ) { //event.stopPropagation(); const rect = renderer.domElement.getBoundingClientRect(); _pointer.x = ( event.clientX - rect.left ) / rect.width * 2 - 1; _pointer.y = - ( event.clientY - rect.top ) / rect.height * 2 + 1; raycaster.setFromCamera( _pointer, camera ); let objects = scope.raycastAll ? getAllMeshes(xrf.scene) : scope.objects const intersects = raycaster.intersectObjects( objects, false ) if ( intersects.length > 0 ) { const intersection = intersects[ 0 ]; object = intersection.object; const uv = intersection.uv; _event.type = event.type; if( uv ) _event.data.set( uv.x, 1 - uv.y ); object.dispatchEvent( _event ); }else{ if( object.selected ) { _event.type = 'mouseleave' object.dispatchEvent( _event) } } } element.addEventListener( 'pointerdown', onPointerEvent ); element.addEventListener( 'pointerup', onPointerEvent ); element.addEventListener( 'pointermove', onPointerEvent ); element.addEventListener( 'mousedown', onPointerEvent ); element.addEventListener( 'mousemove', onPointerEvent ); element.addEventListener( 'click', onPointerEvent ); element.addEventListener( 'mouseup', onPointerEvent ); element.addEventListener( 'touchstart', onPointerEvent ); // WebXR Controller Events // TODO: Dispatch pointerevents too const eventsMapper = { 'move': 'mousemove', 'select': 'click', 'touchstart': 'click', 'selectstart': 'mousedown', 'selectend': 'mouseup' }; function onXRControllerEvent( event ) { const controller = event.target; tempMatrix.identity().extractRotation( controller.matrixWorld ); raycaster.ray.origin.setFromMatrixPosition( controller.matrixWorld ); raycaster.ray.direction.set( 0, 0, - 1 ).applyMatrix4( tempMatrix ); raycaster.far = Infinity let objects = scope.raycastAll ? getAllMeshes(xrf.scene) : scope.objects const intersects = raycaster.intersectObjects( objects, false ) if ( intersects.length > 0 ) { const intersection = intersects[ 0 ]; object = intersection.object; const uv = intersection.uv; _event.type = eventsMapper[ event.type ]; console.log( (new Date()).getTime()+" "+event.type+":"+_event.type+" "+object.name) if( uv ) _event.data.set( uv.x, 1 - uv.y ); object.dispatchEvent( _event ); }else{ if( object.selected ) { _event.type = 'mouseleave' object.dispatchEvent(_event) } } } const controller1 = renderer.xr.getController( 0 ); controller1.addEventListener( 'move', onXRControllerEvent ); controller1.addEventListener( 'select', onXRControllerEvent ); controller1.addEventListener( 'selectstart', onXRControllerEvent ); controller1.addEventListener( 'selectend', onXRControllerEvent ); const controller2 = renderer.xr.getController( 1 ); controller2.addEventListener( 'move', onXRControllerEvent ); controller2.addEventListener( 'select', onXRControllerEvent ); controller2.addEventListener( 'selectstart', onXRControllerEvent ); controller2.addEventListener( 'selectend', onXRControllerEvent ); } intersect( obj, far ){ //const mesh2Box = (mesh) => { // let b = new THREE.Box3() // b.expandByObject(mesh) // return b //} //const objBox = obj.box || (obj.box = mesh2Box(obj)) //let objects = this.raycastAll ? getAllMeshes(xrf.scene) : this.objects //let intersects = [] //objects.map( (objB) => { // if( !objB.box ) objB.box = mesh2Box(objB) // if( objB.box.intersectsBox(objBox) ) intersects.push(obj.box) //}) //return intersects this.raycaster.ray.origin.setFromMatrixPosition( obj.matrixWorld ); this.raycaster.ray.direction.set( 0, 0, -1 ) this.raycaster.far = far || Infinity return this.raycaster.intersectObjects( this.objects, true ) } // we create our own add to avoid unnecessary unparenting of buffergeometries from // their 3D model (which breaks animations) add(obj, unparent){ if( unparent ) Group.prototype.add.call( this, obj ) this.objects.push(obj) } clear(){ while( this.children[0] != undefined ) this.children[0].remove() this.objects = [] } } return new interactive(renderer,camera) } xrf.optimize = (opts) => { opts.animatedObjects = [] xrf.optimize .checkAnimations(opts) .freezeUnAnimatedObjects(opts) .disableShadows(opts) .disableEmbeddedLights(opts) .removeDuplicateLights() } // check unused animations xrf.optimize.checkAnimations = (opts) => { if( xrf.debug ) console.log("TODO: fix freezeUnAnimatedObjects for SRC's") return xrf.optimize let {model} = opts model.animations.map( (anim) => { // collect zombie animations and warn user let zombies = anim.tracks.map( (t) => { let name = t.name.replace(/\..*/,'') let obj = model.scene.getObjectByName(name) if( !model.scene.getObjectByName(name) ) return {anim:anim.name,obj:name} else opts.animatedObjects.push(name) return undefined }) if( zombies.length > 0 ){ // only warn for zombies in main scene (because src-scenes might be filtered anyways) zombies .filter( (z) => z ) // filter out undefined .map( (z) => console.warn(`gltf: object '${z.obj}' not found (anim: '${z.anim}'`) ) console.warn(`TIP: remove dots in objectnames in blender (which adds dots when duplicating)`) } }) return xrf.optimize } xrf.optimize.freezeUnAnimatedObjects = (opts) => { if( xrf.todo ) console.log("TODO: fix freezeUnAnimatedObjects for SRC's") return xrf.optimize let {model} = opts let scene = model.scene // increase performance by freezing all objects scene.traverse( (n) => n.matrixAutoUpdate = false ) // except animated objects and children scene.traverse( (n) => { if( ~opts.animatedObjects.indexOf(n.name) ){ n.matrixAutoUpdate = true n.traverse( (m) => m.matrixAutoUpdate = true ) } }) return xrf.optimize } xrf.optimize.disableShadows = (opts) => { opts.model.scene.traverse( (n) => { if( n.castShadow !== undefined ) n.castShadow = false }) return xrf.optimize } xrf.optimize.disableEmbeddedLights = (opts) => { if( !opts.isSRC ) return xrf.optimize // remove lights from SRC's opts.model.scene.traverse( (n) => { if( n.type.match(/light/i) ) n.remove() }) return xrf.optimize } xrf.optimize.removeDuplicateLights = () => { // local/extern src's can cause duplicate lights which tax performance let lights = {} xrf.scene.traverse( (n) => { if( n.type.match(/light/i) ){ if( !lights[n.name] ) lights[n.name] = true else n.remove() } }) return xrf.optimize } xrf.addEventListener('parseModel', (opts) => { xrf.optimize(opts) }) xrf.sceneToTranscript = (scene, ignoreMesh ) => { let transcript = '' scene.traverse( (n) => { let isSRC = false n.traverseAncestors( (m) => m.userData.src ? isSRC = true : false ) if( !isSRC && n.userData['aria-description'] && (!ignoreMesh || n.uuid != ignoreMesh.uuid) ){ transcript += `#${n.name} ${n.userData['aria-description']}. ` } }) return transcript } // switch camera when multiple cameras for url #mycameraname xrf.addEventListener('navigateLoaded', (opts) => { // select active camera if any let {id,match,v,THREE} = opts let envmap = {} let current = '' // Recursive function to traverse the graph function traverseAndSetEnvMap(node, closestAncestorMaterialMap = null) { // Check if the current node has a material if (node.isMesh && node.material ) { if (node.material.map && closestAncestorMaterialMap) { // If the node has a material map, set the closest ancestor material map node.material = node.material.clone() // dont affect objects which share same material node.material.envMap = closestAncestorMaterialMap; } } // Update the closest ancestor's material map if (node.isMesh && node.material && node.material.map) { closestAncestorMaterialMap = node.material.map.clone(); closestAncestorMaterialMap.mapping = xrf.THREE.EquirectangularReflectionMapping; closestAncestorMaterialMap.needsUpdate = true } // Recursively traverse all children node.children.forEach(child => traverseAndSetEnvMap(child, closestAncestorMaterialMap)); } // Start traversal from the root node traverseAndSetEnvMap(xrf.scene); }) const doFilter = (opts) => { let {scene,id,match,v} = opts if( v.filter ){ let frags = {} frags[ v.filter.key ] = v xrf.filter.scene({frag:frags,scene}) } } xrf.addEventListener('dynamicKey', doFilter ) xrf.addEventListener('dynamicKeyValue', (opts) => { if( xrf.debug ) console.log("*TODO* filter integers only") // doFilter(opts) }) // spec: https://xrfragment.org/#filters xrf.filter = function(query, cb){ let result = [] if( !query ) return result if( query[0] != '#' ) query = '#'+query // *TODO* jquery like utility func return result } xrf.filter.scene = function(opts){ let {scene,frag} = opts scene = xrf.filter .sort(frag) // get (sorted) filters from XR Fragments .process(frag,scene,opts) // show/hide things if( !scene ) return scene.visible = true // always enable scene return scene } xrf.filter.sort = function(frag){ // get all filters from XR Fragments frag.filters = Object.values(frag) .filter( (v) => v.filter ? v : null ) .sort( (a,b) => a.index > b.index ) return xrf.filter } // opts = {copyScene:true} in case you want a copy of the scene (not filter the current scene inplace) xrf.filter.process = function(frag,scene,opts){ if( !scene || scene.children.length == 0 ) return const cleanupKey = (k) => k.replace(/[-\*\/]/g,'') let firstFilter = frag.filters.length ? frag.filters[0].filter.get() : false const hasName = (m,name,filter) => m.name == name const hasNameOrTag = (m,name_or_tag,filter) => hasName(m,name_or_tag) || String(m.userData['tag']).match( new RegExp("(^| )"+name_or_tag) ) // utility functions const getOrCloneMaterial = (o) => { if( o.material ){ if( o.material.isXRF ) return o.material o.material = o.material.clone() o.material.isXRF = true return o.material } return {} } const setVisible = (n,visible,filter,processed) => { if( processed && processed[n.uuid] ) return getOrCloneMaterial(n).visible = visible if( filter.deep ) n.traverse( (m) => getOrCloneMaterial(m).visible = visible ) if( processed ) processed[n.uuid] == true } // spec 3 @ https://xrfragment.org/doc/RFC_XR_Macros.html#embedding-xr-content-using-src // reparent scene based on objectname in case it matches a (non-negating) selector if( opts.reparent && firstFilter && !firstFilter.value && firstFilter.show === true ){ let obj frag.target = firstFilter scene.traverse( (n) => hasName(n, firstFilter.key,firstFilter) && (obj = n) ) if( xrf.debug ) console.log("reparent "+firstFilter.key+" "+((opts.copyScene)?"copy":"inplace")) if(obj ){ obj.position.set(0,0,0) if( opts.copyScene ) { opts.copyScene = new xrf.THREE.Scene() opts.copyScene.children[0] = obj // we dont use .add() as that reparents it from the original scene }else{ // empty current scene and add obj while( scene.children.length > 0 ) scene.children[0].removeFromParent() scene.add( obj ) } }else{ console.warn("could not reparent scene to object "+firstFilter.key+" (not found)") opts.copyScene = new xrf.THREE.Scene() // return empty scene } if( opts.copyScene ) scene = opts.copyScene if( obj ) obj.isReparented = true } // then show/hide things based on secondary selectors // we don't use the XRWG (everything) because we process only the given (sub)scene frag.filters.map( (v) => { const filter = v.filter.get() const name_or_tag = cleanupKey(v.fragment) let processed = {} let extembeds = {} // hide external objects temporarely (prevent them getting filtered too) scene.traverse( (m) => { if( m.isSRCExternal ){ m.traverse( (n) => (extembeds[ n.uuid ] = m) && (m.visible = false) ) } }) scene.traverseVisible( (m) => { // filter on value(expression) #foo=>3 e.g. *TODO* do this in XRWG if( filter.value && m.userData[filter.key] ){ const visible = v.filter.testProperty(filter.key, m.userData[filter.key], filter.show === false ) setVisible(m,visible,filter,processed) return } if( hasNameOrTag(m,name_or_tag,filter ) ){ setVisible(m,filter.show,filter) } }) // show external objects again for ( let i in extembeds ) extembeds[i].visible = true }) return scene } xrf.frag.dynamic.material = function(v,opts){ let {match} = opts // 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 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.set = function(mesh,material,reset){ if( !mesh.materialOriginal ) mesh.materialOriginal = mesh.material let visible = mesh.material.visible //remember if( reset ){ mesh.material = mesh.materialOriginal }else mesh.material = material mesh.material.visible = visible } // for reset calls like href: xrf://!myobject e.g. xrf.addEventListener('dynamicKey', (opts) => { let {v,match} = opts if( v.reset ){ xrf.XRWG.deepApplyMatch(match,v, (match,v,node,type) => { if( node.material ) xrf.frag.dynamic.material.set( node, null, v.reset ) }) } }) // 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){ if( k.match(/^(name)$/) ) return true me[k] = v return true }, 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) => { const variables = /{([a-zA-Z0-9-]+)}/g if( n.userData ){ for( let i in n.userData ){ //if( i[0] == '#' || i.match(/^(href|tag)$/) ) continue // ignore XR Fragment aliases if( i.match(/^(src|href|tag)/) ){ // lets declare empty variables found in src-values ('https://foo.com/video.mp4#{somevar}') e.g. if( n.userData[i].match(variables) ){ let vars = [].concat( n.userData[i].match(variables) ) const strip = (v) => v.replace(/[{}]/g,'') vars.map( (v) => xrf.URI.vars[ strip(v) ] = () => '' ) } }else xrf.URI.vars[i] = () => n.userData[i] // declare variables with values } } }) }) 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 if( v.string.match(/(<|>)/) ) return // ignore filter values if( match.length > 0 ){ xrf.frag.dynamic.material(v,opts) // check if fragment is an objectname } if( !xrf.URI.vars[ v.string ] ) return // ignore non-template URI fragments //if( xrf.URI.vars[ id ] && !match.length ) return console.error(`'${id}' object/tag/metadata-key not found in scene`) if( xrf.debug ) console.log(`URI.vars[${id}] => '${v.string}'`) if( xrf.URI.vars[id] ){ xrf.URI.vars[ id ] = xrf.URI.vars[ v.string ] // update var if( xrf.debug ) console.log(`URI.vars[${id}] => '${xrf.URI.vars[ v.string ]()}'`) 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) }) } }) xrf.addEventListener('dynamicKey', (opts) => { let {scene,id,match,v} = opts if( !scene ) return let remove = [] // erase previous lines xrf.focusLine.lines.map( (line) => line.parent && (line.parent.remove(line)) ) xrf.focusLine.points = [] xrf.focusLine.lines = [] // drawlines match.map( (w) => { w.nodes.map( (mesh) => xrf.drawLineToMesh({ ...opts, mesh}) ) }) }) xrf.drawLineToMesh = (opts) => { let {scene,mesh,frag,id} = opts const THREE = xrf.THREE let oldSelection // Selection of Interest if predefined_view matches object name if( mesh.visible && mesh.material){ xrf.emit('focus',{...opts,frag}) .then( () => { const color = new THREE.Color(); const colors = [] let from = new THREE.Vector3() let getCenterPoint = (mesh) => { var geometry = mesh.geometry; geometry.computeBoundingBox(); var center = new THREE.Vector3(); geometry.boundingBox.getCenter( center ); mesh.localToWorld( center ); return center; } let cam = xrf.camera.getCam ? xrf.camera.getCam() : xrf.camera // *FIXME* camerarig/rig are conflicting cam.updateMatrixWorld(true); // always keeps me diving into the docs :] cam.getWorldPosition(from) from.y = 0.5 // originate from the heart chakra! :p const points = [from, getCenterPoint(mesh) ] const geometry = new THREE.BufferGeometry().setFromPoints( points ); let line = new THREE.Line( geometry, xrf.focusLine.material ); line.isXRF = true line.computeLineDistances(); xrf.focusLine.lines.push(line) xrf.focusLine.points.push(from) xrf.focusLine.opacity = 1 xrf.add(line) }) } } xrf.addEventListener('render', (opts) => { // update focusline let {time,model} = opts if( !xrf.clock || !xrf.focusLine ) return xrf.focusLine.material.color.r = (1.0 + Math.sin( xrf.clock.getElapsedTime()*10 ))/2 xrf.focusLine.material.dashSize = 0.2 + 0.02*Math.sin( xrf.clock.getElapsedTime() ) xrf.focusLine.material.gapSize = 0.1 + 0.02*Math.sin( xrf.clock.getElapsedTime() *3 ) xrf.focusLine.material.opacity = (0.25 + 0.15*Math.sin( xrf.clock.getElapsedTime() * 3 )) * xrf.focusLine.opacity; if( xrf.focusLine.opacity > 0.0 ) xrf.focusLine.opacity -= time*0.2 if( xrf.focusLine.opacity < 0.0 ) xrf.focusLine.opacity = 0 }) /* * mimetype: audio/aac * mimetype: audio/mpeg * mimetype: audio/ogg * mimetype: audio/weba * mimetype: audio/wav */ let loadAudio = (mimetype) => function(url,opts){ let {mesh,src,camera,THREE} = opts let URL = xrfragment.URI.toAbsolute( xrf.navigator.URI, url ) let frag = URL.XRF xrf.init.audio() let isPositionalAudio = !(mesh.position.x == 0 && mesh.position.y == 0 && mesh.position.z == 0) const audioLoader = new THREE.AudioLoader(); let sound = isPositionalAudio ? new THREE.PositionalAudio( camera.listener) : new THREE.Audio( camera.listener ) sound.isXRF = true mesh.media = mesh.media || {} mesh.media.audio = { set: (mediafragment,v) => mesh.media.audio[mediafragment] = v } let finalUrl = URL.URN + URL.file if( xrf.debug > 0 ) console.log("GET "+finalUrl) audioLoader.load( finalUrl, function( buffer ) { sound.setBuffer( buffer ); sound.setLoop(false); sound.setVolume( 1.0 ) if( isPositionalAudio ){ sound.setRefDistance( mesh.scale.x); sound.setRolloffFactor(20.0) //sound.setDirectionalCone( 360, 360, 0.01 ); }else sound.setVolume( mesh.scale.x ) mesh.add(sound) sound.set = (mediafragment,v) => { try{ sound[mediafragment] = v if( mediafragment == 't'){ if( sound.isPlaying && v.y != undefined && v.x == v.y ){ sound.offset = v.x * buffer.sampleRate ; sound.pause() return }else sound.stop() // apply embedded audio/video samplerate/fps or global mixer fps sound.setLoopStart(v.x); sound.setLoopEnd(v.y || buffer.duration); sound.offset = v.x; sound.play() } if( mediafragment == 's'){ // *TODO* https://stackoverflow.com/questions/12484052/how-can-i-reverse-playback-in-web-audio-api-but-keep-a-forward-version-as-well sound.pause() sound.setPlaybackRate( Math.abs(v.x) ) // WebAudio does not support negative playback sound.play() } if( mediafragment == 'loop'){ sound.pause() sound.setLoop( v.loop ) sound.play() } }catch(e){ console.warn(e) } } let lazySet = {} let mediaFragments = ['loop','s','t'] mediaFragments.map( (f) => mesh.media.audio[f] && (lazySet[f] = mesh.media.audio[f]) ) mesh.media.audio = sound // autoplay if user already requested play (before the sound was loaded) mediaFragments.map( (f) => { if( lazySet[f] ) mesh.media.audio.set(f, lazySet[f] ) }) }); // apply Media fragments from URL (['t','loop','s']).map( (f) => { if( frag[f] ){ mesh.media.audio.set( f, frag[f] ) } }) } xrf.init.audio = (opts) => { let camera = xrf.camera /* WebAudio: setup context via THREEjs */ if( !camera.listener ){ camera.listener = new xrf.THREE.AudioListener(); // *FIXME* camera vs camerarig conflict (camera.getCam ? camera.getCam() : camera).add( camera.listener ); xrf.emit('audioInited',{listener:camera.listener, ...opts}) } } xrf.addEventListener('init', xrf.init.audio ) // stop playing audio when loading another scene xrf.addEventListener('reset', () => { xrf.scene.traverse( (n) => { if( n.media && n.media.audio ){ if( n.media.audio.stop ) n.media.audio.stop() if( n.media.audio.remove ) n.media.audio.remove() n.remove() } }) }) let audioMimeTypes = [ 'audio/x-wav', 'audio/wav', 'audio/mpeg', 'audio/mp3', 'audio/weba', 'audio/aac', 'application/ogg' ] audioMimeTypes.map( (mimetype) => xrf.frag.src.type[ mimetype ] = loadAudio(mimetype) ) /* * mimetype: model/gltf+json */ xrf.frag.src.type['fbx'] = function( url, opts ){ return new Promise( async (resolve,reject) => { let {mesh,src} = opts let URL = xrfragment.URI.toAbsolute( xrf.navigator.URI, url ) let frag = URL.XRF let loader let {THREE} = await import('https://unpkg.com/three@0.161.0/build/three.module.js') let { FBXLoader } = await import('three/addons/loaders/FBXLoader.js') debugger //const Loader = xrf.loaders[ext] //if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext //if( !dir.match("://") ){ // force relative path // dir = dir[0] == './' ? dir : `./${dir}` // loader = new Loader().setPath( dir ) //}else loader = new Loader() //loader.load(url, (model) => { // model.isSRC = true // resolve(model) //}) }) } /* * extensions: .frag/.fs/.vs/.vert */ xrf.frag.src.type['x-shader/x-fragment'] = function(url,opts){ let {mesh,THREE} = opts let URL = xrfragment.URI.toAbsolute( xrf.navigator.URI, url ) let frag = URL.XRF let isFragmentShader = /\.(fs|frag|glsl)$/ let isVertexShader = /\.(vs|vert)$/ let shaderReqs = [] let shaderCode = {} let shader = { fragment: { code: '', url: url.match( isFragmentShader ) ? URL.URN + URL.file : '' }, vertex: { code: '', url: url.match( isVertexShader ) ? URL.URN + URL.file : '' } } var onShaderLoaded = ((args) => (type, status, code) => { if( !code ) return console.error('could not load shader') 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'] /* * mimetype: model/gltf+json */ xrf.frag.src.type['gltf'] = function( url, opts ){ return new Promise( (resolve,reject) => { let {mesh,src} = opts let URL = xrfragment.URI.toAbsolute( xrf.navigator.URI, url ) let {directory,file,fileExt,URN} = URL; let loader const Loader = xrf.loaders[fileExt] if( !Loader ) throw 'xrfragment: no loader passed to xrfragment for extension .'+ext loader = new Loader().setPath( URN ) loader.load(file, (model) => { model.isSRC = true resolve(model) }) }) } let loadHTML = (mimetype) => function(url,opts){ let {mesh,src,camera} = opts let {urlObj,dir,file,hash,ext} = xrf.parseUrl(url) let frag = xrf.URI.parse( url ).XRF console.warn("todo: html viewer for src not implemented") } let htmlMimeTypes = [ 'text/html' ] htmlMimeTypes.map( (mimetype) => xrf.frag.src.type[ mimetype ] = loadHTML(mimetype) ) /* * mimetype: image/png * mimetype: image/jpg * mimetype: image/gif */ xrf.frag.src.type['image/png'] = function(url,opts){ let {mesh,THREE} = opts let restrictTo3DBoundingBox = mesh.geometry let URL = xrfragment.URI.toAbsolute( xrf.navigator.URI, url ) let frag = URL.XRF mesh.material = new xrf.THREE.MeshBasicMaterial({ map: null, transparent: url.match(/\.(png|gif)/) ? true : false, side: THREE.DoubleSide, color: 0xFFFFFF, opacity:1 }); let renderImage = (texture) => { let img = {w: texture.source.data.width, h: texture.source.data.height} // stretch image by pinning uv-coordinates to corners if( mesh.geometry ){ if( mesh.geometry.attributes.uv ){ // buffergeometries let uv = mesh.geometry.attributes.uv; }else { console.warn("xrfragment: uv's of ${url} might be off for non-buffer-geometries *TODO*") //if( geometry.faceVertexUvs ){ // *TODO* force uv's of dynamically created geometries (in threejs) //} } } mesh.material.map = texture mesh.material.needsUpdate = true mesh.needsUpdate = true //// *TODO* update clones in portals or dont clone scene of portals.. //xrf.scene.traverse( (n) => { // if( n.userData.src == mesh.userData.src && mesh.uuid != n.uuid ){ // n.material = mesh.material // n.material.needsUpdate = true // } //}) } let onLoad = (texture) => { texture.colorSpace = THREE.SRGBColorSpace; texture.wrapS = THREE.RepeatWrapping; texture.wrapT = THREE.RepeatWrapping; renderImage(texture) } new THREE.TextureLoader().load( URL.URN + URL.file, onLoad, null, console.error ); } xrf.frag.src.type['image/gif'] = xrf.frag.src.type['image/png'] xrf.frag.src.type['image/jpeg'] = xrf.frag.src.type['image/png'] // spec 8: https://xrfragment.org/doc/RFC_XR_Macros.html#embedding-xr-content-using-src xrf.portalNonEuclidian = function(opts){ let { frag, mesh, model, camera, scene, renderer} = opts mesh.portal = { pos: mesh.position.clone(), posWorld: new xrf.THREE.Vector3(), posWorldCamera: new xrf.THREE.Vector3(), stencilRef: xrf.portalNonEuclidian.stencilRef, needUpdate: false, stencilObject: false, cameraDirection: new xrf.THREE.Vector3(), cameraPosition: new xrf.THREE.Vector3(), raycaster: new xrf.THREE.Raycaster(), isLocal: opts.isLocal, isLens: false, isInside: false, setStencil: (stencilRef) => mesh.portal.stencilObjects.traverse( (n) => showPortal(n, stencilRef == 0) && n.stencil && n.stencil(stencilRef) ), positionObjectsIfNeeded: (pos,scale) => !mesh.portal.isLens && mesh.portal.stencilObjects.traverse( (n) => n.positionAtStencil && (n.positionAtStencil(pos,scale)) ) } // allow objects to flip between original and stencil position (which puts them behind stencilplane) const addStencilFeature = (n) => { if( n.stencil ) return n // run once n.stencil = (sRef ) => xrf.portalNonEuclidian.selectStencil(n, sRef ) n.positionAtStencil = (pos,scale) => (newPos,newScale) => { n.position.copy( newPos || pos ) n.scale.copy( scale ) n.updateMatrixWorld(true) } // curry function n.positionAtStencil = n.positionAtStencil( n.position.clone(), n.scale.clone() ) return n } this.setupStencilObjects = (scene,opts) => { // collect related objects to render inside stencilplane let stencilObject = scene if( opts.srcFrag.target ){ stencilObject = scene.getObjectByName( opts.srcFrag.target.key ) // spec: if src-object is child of portal (then portal is lens, and should include all children ) mesh.traverse( (n) => n.name == opts.srcFrag.target.key && (stencilObject = n) && (mesh.portal.isLens = true) ) } if( !stencilObject ) return console.warn(`no objects were found (src:${mesh.userData.src}) for (portal)object name '${mesh.name}'`) mesh.portal.stencilObject = stencilObject // spec: if src points to child, act as lens if( !mesh.portal.isLocal || mesh.portal.isLens ) stencilObject.visible = false let stencilObjects = [stencilObject] stencilObjects = stencilObjects .filter( (n) => !n.portal ) // filter out (self)references to portals (prevent recursion) .map(addStencilFeature) // put it into a scene (without .add() because it reparents objects) so we can render it separately mesh.portal.stencilObjects = new xrf.THREE.Scene() mesh.portal.stencilObjects.children = stencilObjects mesh.portal.stencilObjects.isXRF = true xrf.portalNonEuclidian.stencilRef += 1 // each portal has unique stencil id if( xrf.debug ) console.log(`enabling portal for object '${mesh.name}' (stencilRef:${mesh.portal.stencilRef})`) return this } // enable the stencil-material of the stencil objects to prevent stackoverflow (portal in portal rendering) const showPortal = (n,show) => { if( n.portal ) n.visible = show return true } 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 } xrf.addEventListener('renderPost', (opts) => { let {scene,camera,time,render,renderer} = opts if( mesh.portal.needUpdate && mesh.portal && mesh.portal.stencilObjects ){ let cameraDirection = mesh.portal.cameraDirection let cameraPosition = mesh.portal.cameraPosition let stencilRef = mesh.portal.stencilRef let newPos = mesh.portal.posWorld let stencilObject = mesh.portal.stencilObject let newScale = mesh.scale let raycaster = mesh.portal.raycaster let cam = xrf.camera.getCam ? xrf.camera.getCam() : camera cam.getWorldPosition(cameraPosition) cam.getWorldDirection(cameraDirection) if( cameraPosition.distanceTo(newPos) > 15.0 ) return // dont render far portals // init if( !mesh.portal.isLocal || mesh.portal.isLens ) stencilObject.visible = true mesh.portal.setStencil(stencilRef) renderer.autoClear = false renderer.autoClearDepth = false renderer.autoClearColor = false renderer.autoClearStencil = false // render render( mesh.portal.stencilObjects, camera ) // de-init renderer.autoClear = true renderer.autoClearDepth = true renderer.autoClearColor = true renderer.autoClearStencil = true mesh.portal.setStencil(0) if( !mesh.portal.isLocal || mesh.portal.isLens ) stencilObject.visible = false // trigger href upon camera collide if( mesh.userData.XRF.href ){ raycaster.far = 0.35 raycaster.set(cameraPosition, cameraDirection ) let intersects = raycaster.intersectObjects([mesh], false) if (intersects.length > 0 && !mesh.portal.teleporting ){ mesh.portal.teleporting = true mesh.userData.XRF.href.exec({nocommit:true}) setTimeout( () => mesh.portal.teleporting = false, 500) // dont flip back and forth } } } mesh.portal.needUpdate = false }) return this } // turn mesh into stencilplane xrf .portalNonEuclidian .setMaterial(mesh) .getWorldPosition(mesh.portal.posWorld) this .setupListeners() .setupStencilObjects(scene,opts) // move portal objects to portalposition if( mesh.portal.stencilObjects ) mesh.portal.positionObjectsIfNeeded(mesh.portal.posWorld, mesh.scale) } xrf.portalNonEuclidian.selectStencil = (n, stencilRef, nested) => { if( n.material ){ n.material.stencilRef = stencilRef n.material.stencilWrite = stencilRef > 0 n.material.stencilFunc = xrf.THREE.EqualStencilFunc; } if( n.children && !nested ) n.traverse( (m) => !m.portal && (xrf.portalNonEuclidian.selectStencil(m,stencilRef,true)) ) } xrf.portalNonEuclidian.setMaterial = function(mesh){ mesh.material = new xrf.THREE.MeshBasicMaterial({ color: 'orange' }); mesh.material.depthWrite = false; mesh.material.colorWrite = false; mesh.material.stencilWrite = true; mesh.material.stencilRef = xrf.portalNonEuclidian.stencilRef; mesh.material.stencilFunc = xrf.THREE.AlwaysStencilFunc; mesh.material.stencilZPass = xrf.THREE.ReplaceStencilOp; mesh.material.stencilZFail = xrf.THREE.ReplaceStencilOp; return mesh } xrf.addEventListener('parseModel',(opts) => { const scene = opts.model.scene }) // (re)set portalObjects when entering/leaving a portal let updatePortals = (opts) => { xrf.scene.traverse( (n) => { if( !n.portal ) return // move objects back to the portal if( n.portal.isInside ) n.portal.positionObjectsIfNeeded( n.portal.posWorld, n.scale ) n.portal.isInside = false }) if( opts.mesh && opts.mesh.portal && opts.click ){ opts.mesh.portal.isInside = true opts.mesh.portal.positionObjectsIfNeeded() // move objects back to original pos (since we are teleporting there) } } xrf.addEventListener('href', (opts) => opts.click && updatePortals(opts) ) xrf.addEventListener('navigate', updatePortals ) xrf.portalNonEuclidian.stencilRef = 1 let loadVideo = (mimetype) => function(url,opts){ let {mesh,src,camera} = opts const THREE = xrf.THREE let URL = xrfragment.URI.toAbsolute( xrf.navigator.URI, url ) let frag = URL.XRF // patch VideoTexture so it doesn't upload videoframes when paused // https://github.com/mrdoob/three.js/pull/28575 THREE.VideoTexture.prototype.update = function(){ const video = this.image; const hasVideoFrameCallback = 'requestVideoFrameCallback' in video; if ( hasVideoFrameCallback === false && video.readyState >= video.HAVE_CURRENT_DATA && (!video.paused || !this.firstFrame) ){ console.log("updating..") this.needsUpdate = true; this.firstFrame = true } } mesh.media = mesh.media || {} let video = mesh.media.video = document.createElement('video') video.style.display = 'none' video.setAttribute("crossOrigin","anonymous") video.setAttribute("playsinline",'') video.addEventListener('loadedmetadata', function(){ let texture = new THREE.VideoTexture( video ); texture.colorSpace = THREE.SRGBColorSpace; let mat = new xrf.THREE.MeshBasicMaterial() mat.map = texture mesh.material = mat // 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.looping ) video.currentTime = video.t.x // speed means loop else video.pause() } },false) }) video.src = URL.URN + URL.file video.speed = 1.0 video.looping = false video.set = (mediafragment,v) => { video[mediafragment] = v if( mediafragment == 't'){ video.pause() if( v.x !== undefined && v.x == v.y ) return // stop paused else{ video.currentTime = v.x video.time = v.x video.play() } } if( mediafragment == 's' ){ video.playbackRate = Math.abs( v.x ) // html5 video does not support reverseplay :/ } if( mediafragment == 'loop' ){ video.looping = true } } } // stop playing audio when loading another scene xrf.addEventListener('reset', () => { xrf.scene.traverse( (n) => n.media && n.media.video && (n.media.video.pause()) && (n.media.video.remove()) ) }) let videoMimeTypes = [ 'video/ogg', 'video/mp4' ] videoMimeTypes.map( (mimetype) => xrf.frag.src.type[ mimetype ] = loadVideo(mimetype) )