From d757dab4e0efa6fe62d5af7ac0f2e6ec15d34981 Mon Sep 17 00:00:00 2001 From: Leon van Kammen Date: Fri, 14 Apr 2023 14:45:50 +0200 Subject: [PATCH] major refactor --- .github/workflows/website.yml | 21 ++++++ src/xrfragment/Parser.hx | 120 +++++++++++++++------------------- src/xrfragment/Parser.lua | 95 +++++++++++++++++++++++++++ src/xrfragment/Query.hx | 7 +- src/xrfragment/URI.hx | 9 ++- src/xrfragment/XRF.hx | 107 ++++++++++++++++++++++++++++++ src/xrfragment/XRF.lua | 112 +++++++++++++++++++++++++++++++ test/test.html | 118 +++++++++++++++++++++++++++++++++ 8 files changed, 519 insertions(+), 70 deletions(-) create mode 100644 .github/workflows/website.yml create mode 100644 src/xrfragment/Parser.lua create mode 100644 src/xrfragment/XRF.hx create mode 100644 src/xrfragment/XRF.lua create mode 100644 test/test.html diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml new file mode 100644 index 0000000..eaf28fd --- /dev/null +++ b/.github/workflows/website.yml @@ -0,0 +1,21 @@ +name: Deploy to GitHub Pages +on: + push: + branches: + - main +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + ref: main + - name: Build + run: cp test/test.html doc/. + working-directory: ./ + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./doc diff --git a/src/xrfragment/Parser.hx b/src/xrfragment/Parser.hx index 91566e5..1fcd70c 100644 --- a/src/xrfragment/Parser.hx +++ b/src/xrfragment/Parser.hx @@ -1,11 +1,6 @@ package xrfragment; -/** - * # XR Fragments (key/value params) - * - * > ⛁ = define in 3D asset-file (as custom property or default projection)
- * > ☇ = mutable, using navigator URI (`document.location.href` e.g.)
- */ +import xrfragment.XRF; @:expose // <- makes the class reachable from plain JavaScript @:keep // <- avoids accidental removal by dead code elimination @@ -14,14 +9,51 @@ class Parser { @:keep public static function parse(key:String,value:String,resultMap:haxe.DynamicAccess):Bool { - var Frag:Map = new Map(); // | param | type | scope(s) | category | notes | - // |---------|---------------|-------|--------------------|---------------------------------| - Frag.set("prio", Type.isInt); // | prio | int (-10..1) | ⛁ | Asset loading / linking | $(cat doc/notes/prio.md) | - Frag.set("pos", Type.isVector); // | pos | 3D vector | ⛁ ☇ |HREF navigation/portals | | - Frag.set("q", Type.isString); // | q | string | ⛁ |Query Selector | | - var vec:String = "1,2,3"; // - //if( Type.isVector(vec) ) trace("ja"); + // here we define allowed characteristics & datatypes for each fragment (stored as bitmasked int for performance purposes) + var Frag:Map = new Map(); + + // category: asset loading linking + Frag.set("prio", XRF.ASSET_OBJ | XRF.T_INT ); + Frag.set("#", XRF.ASSET | XRF.T_PREDEFINED_VIEW ); + Frag.set("class", XRF.ASSET_OBJ | XRF.T_STRING ); + Frag.set("src", XRF.ASSET_OBJ | XRF.T_URL ); + Frag.set("src_audio", XRF.ASSET_OBJ | XRF.T_URL ); + Frag.set("src_shader", XRF.ASSET_OBJ | XRF.T_URL ); + Frag.set("src_env", XRF.ASSET | XRF.T_URL ); + Frag.set("src_env_audio", XRF.ASSET | XRF.T_URL ); + + // category: href navigation / portals / teleporting + Frag.set("pos", XRF.PV_OVERRIDE | XRF.ROUNDROBIN | XRF.T_VECTOR3 | XRF.T_STRING_OBJ ); + Frag.set("href", XRF.ASSET_OBJ | XRF.T_URL | XRF.T_PREDEFINED_VIEW ); + + // category: query selector | object manipulation + Frag.set("q", XRF.PV_OVERRIDE | XRF.T_STRING ); + Frag.set("scale", XRF.QUERY_OPERATOR | XRF.PV_OVERRIDE | XRF.ROUNDROBIN | XRF.T_INT ); + Frag.set("rot", XRF.QUERY_OPERATOR | XRF.PV_OVERRIDE | XRF.ROUNDROBIN | XRF.T_VECTOR3 ); + Frag.set("translate", XRF.QUERY_OPERATOR | XRF.PV_OVERRIDE | XRF.ROUNDROBIN | XRF.T_VECTOR3 ); + Frag.set("visible", XRF.QUERY_OPERATOR | XRF.PV_OVERRIDE | XRF.ROUNDROBIN | XRF.T_INT ); + + // category: animation + Frag.set("t", XRF.ASSET | XRF.PV_OVERRIDE | XRF.ROUNDROBIN | XRF.T_VECTOR2 | XRF.BROWSER_OVERRIDE ); + Frag.set("gravity", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_VECTOR3 ); + Frag.set("physics", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_VECTOR3 ); + Frag.set("scroll", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_STRING ); + + // category: device / viewport settings + Frag.set("fov", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_INT | XRF.BROWSER_OVERRIDE ); + Frag.set("clip", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_VECTOR2 | XRF.BROWSER_OVERRIDE ); + Frag.set("fog", XRF.ASSET | XRF.PV_OVERRIDE | XRF.T_STRING | XRF.BROWSER_OVERRIDE ); + + // category: author / metadata + Frag.set("namespace", XRF.ASSET | XRF.T_STRING ); + Frag.set("SPFX", XRF.ASSET | XRF.T_STRING ); + Frag.set("unit", XRF.ASSET | XRF.T_STRING ); + Frag.set("description", XRF.ASSET | XRF.T_STRING ); + + // category: multiparty + Frag.set("src_session", XRF.ASSET | XRF.T_URL | XRF.PV_OVERRIDE | XRF.BROWSER_OVERRIDE | XRF.PROMPT ); + /** * # XR Fragments parser * @@ -29,65 +61,19 @@ class Parser { * the gist of it: */ if( Frag.exists(key) ){ // 1. check if param exist - if( Frag.get(key).match(value) ){ // 1. each key has a regex to validate its value-type (see regexes) - var v:Value = new Value(); - guessType(v, value); // 1. extract the type - // process multiple values - if( value.split("|").length > 1 ){ // 1. use `|` on stringvalues, to split multiple values - v.args = new Array(); - var args:Array = value.split("|"); - for( i in 0...args.length){ - var x:Value = new Value(); - guessType(x, args[i]); // 1. for each multiple value, guess the type - v.args.push( x ); - } - } - resultMap.set(key, v ); - }else { trace("[ i ] fragment '"+key+"' has incompatible value ("+value+")"); return false; } - }else { trace("[ i ] fragment '"+key+"' does not exist or has no type defined (yet)"); return false; } + var v:XRF = new XRF(key, Frag.get(key)); + if( !v.validate(value) ){ + trace("[ i ] fragment '"+key+"' has incompatible value ("+value+")"); + return false; + } + resultMap.set(key, v ); + }else{ trace("[ i ] fragment '"+key+"' does not exist or has no type typed (yet)"); return false; } return true; } - @:keep - public static function guessType(v:Value, str:String):Void { - v.string = str; - if( str.split(",").length > 1){ // 1. `,` assumes 1D/2D/3D vector-values like x[,y[,z]] - var xyz:Array = str.split(","); // 1. parseFloat(..) and parseInt(..) is applied to vector/float and int values - if( xyz.length > 0 ) v.x = Std.parseFloat(xyz[0]); // 1. anything else will be treated as string-value - if( xyz.length > 1 ) v.y = Std.parseFloat(xyz[1]); // 1. incompatible value-types will be dropped / not used - if( xyz.length > 2 ) v.y = Std.parseFloat(xyz[2]); // - } // > the xrfragment specification should stay simple enough - // > for anyone to write a parser using either regexes or grammar/lexers - if( Type.isColor.match(str) ) v.color = str; // > therefore expressions/comprehensions are not supported (max wildcard/comparison operators for queries e.g.) - if( Type.isFloat.match(str) ) v.float = Std.parseFloat(str); - if( Type.isInt.match(str) ) v.int = Std.parseInt(str); - } +} -} - // # Parser Value types - // - // | type | info | format | example | -class Value { // |------|------|--------|----------------------------------| - public var x:Float; // |vector| x,y,z| comma-separated | #pos=1,2,3 | - public var y:Float; - public var z:Float; - public var color:String; // |string| color| FFFFFF (hex) | #fog=5m,FFAACC | - public var string:String; // |string| | | #q=-sun | - public var int:Int; // |int | | [-]x[xxxxx] | #price:>=100 | - public var float:Float; // |float | | [-]x[.xxxx] (ieee)| #prio=-20 | - public var args:Array; // |array | mixed| \|-separated | #pos=0,0,0\|90,0,0 | - public function new(){} // - // > rule for thumb: type-limitations will piggyback JSON limitations (IEEE floatsize e.g.) -} - // Regexes: -class Type { // - static public var isColor:EReg = ~/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; // 1. hex colors are detected using regex `/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/` - static public var isInt:EReg = ~/^[0-9]+$/; // 1. integers are detected using regex `/^[0-9]+$/` - static public var isFloat:EReg = ~/^[0-9]+\.[0-9]+$/; // 1. floats are detected using regex `/^[0-9]+\.[0-9]+$/` - static public var isVector:EReg = ~/([,]+|\w)/; // 1. vectors are detected using regex `/[,]/` (but can also be an string referring to an entity-ID in the asset) - static public var isString:EReg = ~/.*/; // 1. anything else is string `/.*/` -} /// # Tests /// diff --git a/src/xrfragment/Parser.lua b/src/xrfragment/Parser.lua new file mode 100644 index 0000000..8732f86 --- /dev/null +++ b/src/xrfragment/Parser.lua @@ -0,0 +1,95 @@ +XF = {} + +function split (inputstr, sep) + if sep == nil then + sep = "%s" + end + local t={} + for str in string.gmatch(inputstr, "([^"..sep.."]+)") do + table.insert(t, str) + end + return t +end + +function XF.parse(key, value, resultMap) + local frag = {} + frag["prio"] = "^%d+$" + frag["pos"] = "(%d+),(%d+),(%d+)" + frag["q"] = ".+" + + if frag[key] ~= nil then + local regex = frag[key] + if string.match(value, regex) then + local v = Value:new() + XF.guessType(v, value) + if string.find(value, "|") then + v.args = {} + local args = split(value, "|") + for k, arg in ipairs(args) do + local x = Value:new() + XF.guessType(x, arg) + table.insert(v.args, x) + end + end + resultMap[key] = v; + else + error("[ i ] fragment '"..key.."' has incompatible value ("..value..")") + return false + end + else + error("[ i ] fragment '"..key.."' does not exist or has no type defined (yet)") + return false + end + return true +end + +function XF.guessType(v, str) + v.string = str + local parts = split(str, ",") + if #parts > 1 then + v.x = tonumber(parts[1]) + v.y = tonumber(parts[2]) + if #parts > 2 then + v.z = tonumber(parts[3]) + end + end + + if string.match(str, Type.isColor) then + v.color = str + end + if string.match(str, Type.isFloat) then + v.float = tonumber(str) + end + if string.match(str, Type.isInt) then + v.int_val = tonumber(str) + end +end + +Value = {} + +function Value:new() + local obj = {} + obj.x = nil + obj.y = nil + obj.z = nil + obj.color = nil + obj.string = nil + obj.int_val = nil + obj.float = nil + obj.args = nil + setmetatable(obj, self) + self.__index = self + return obj +end + +Type = {} +Type.isColor = "^#([a-fA-F0-9]{6}|[a-fA-F0-9]{3})$" +Type.isInt = "^[0-9]+$" +Type.isFloat = "^[0-9]+%.[0-9]+$" +Type.isVector = "([,]+|%w)" + +local map = {} +XF.parse("pos","1,2,3",map) +print(map.pos.z) +XF.parse("pos","1,2,3|3,4,5",map) +print(map.pos.args[2].z) diff --git a/src/xrfragment/Query.hx b/src/xrfragment/Query.hx index c3fa92f..65482b0 100644 --- a/src/xrfragment/Query.hx +++ b/src/xrfragment/Query.hx @@ -59,6 +59,10 @@ class Query { return classAlias.match(token) ? StringTools.replace(token,".","class:") : token; } + public function get() : Dynamic { + return this.q; + } + public function parse(str:String,recurse:Bool = false) : Dynamic { var token = str.split(" "); @@ -103,8 +107,7 @@ class Query { } } for( i in 0...token.length ) process( expandAliases(token[i]) ); - this.q = q; - return this.q; + return this.q = q; } @:keep diff --git a/src/xrfragment/URI.hx b/src/xrfragment/URI.hx index 0f20597..04c5c94 100644 --- a/src/xrfragment/URI.hx +++ b/src/xrfragment/URI.hx @@ -1,6 +1,7 @@ package xrfragment; import xrfragment.Parser; +import xrfragment.XRF; /** * @@ -41,7 +42,7 @@ import xrfragment.Parser; @:keep // <- avoids accidental removal by dead code elimination class URI { @:keep - public static function parse(qs:String):haxe.DynamicAccess { + public static function parse(qs:String,browser_override:Bool):haxe.DynamicAccess { var fragment:Array = qs.split("#"); // 1. fragment URI starts with `#` var splitArray:Array = fragment[1].split('&'); // 1. fragments are split by `&` var resultMap:haxe.DynamicAccess = {}; // 1. store key/values into a associative array or dynamic object @@ -56,6 +57,12 @@ class URI { var ok:Bool = Parser.parse(key,value,resultMap); // 1. every recognized fragment key/value-pair is added to a central map/associative array/object } } + if( browser_override ){ + for (key in resultMap.keys()) { + var xrf:XRF = resultMap.get(key); + if( !xrf.is( XRF.BROWSER_OVERRIDE ) ) resultMap.remove(key); + } + } return resultMap; } } diff --git a/src/xrfragment/XRF.hx b/src/xrfragment/XRF.hx new file mode 100644 index 0000000..7dfe7d6 --- /dev/null +++ b/src/xrfragment/XRF.hx @@ -0,0 +1,107 @@ +package xrfragment; + +class XRF { + + /* + * this class represents a fragment (value) + */ + + // public static inline readonly ASSET + public static var ASSET:Int = 1; // fragment is immutable (typed in asset) globally + public static var ASSET_OBJ:Int = 2; // fragment is immutable (typed in object in asset) + public static var PV_OVERRIDE:Int = 4; // fragment can be overriden when specified in predefined view value + public static var QUERY_OPERATOR = 8; // fragment will be applied to result of queryselecto + public static var PROMPT:Int = 16; // ask user whether this fragment value can be changed + public static var ROUNDROBIN:Int = 32; // evaluation of this (multi) value can be roundrobined + public static var BROWSER_OVERRIDE:Int = 64; // fragment can be overriden by (manual) browser URI change + + // highlevel types + public static var T_COLOR:Int = 128; + public static var T_INT:Int = 256; + public static var T_FLOAT:Int = 512; + public static var T_VECTOR2:Int = 1024; + public static var T_VECTOR3:Int = 2048; + public static var T_URL:Int = 4096; + public static var T_PREDEFINED_VIEW:Int = 8192; + public static var T_STRING:Int = 16384; + public static var T_STRING_OBJ:Int = 32768; + + // regexes + public static var isColor:EReg = ~/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; // 1. hex colors are detected using regex `/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/` + public static var isInt:EReg = ~/^[0-9]+$/; // 1. integers are detected using regex `/^[0-9]+$/` + public static var isFloat:EReg = ~/^[0-9]+\.[0-9]+$/; // 1. floats are detected using regex `/^[0-9]+\.[0-9]+$/` + public static var isVector:EReg = ~/([,]+|\w)/; // 1. vectors are detected using regex `/[,]/` (but can also be an string referring to an entity-ID in the asset) + public static var isUrl:EReg = ~/(:\/\/)?\..*/; // 1. url/file */` + public static var isUrlOrPretypedView:EReg = ~/(^#|:\/\/)?\..*/; // 1. url/file */` + public static var isString:EReg = ~/.*/; // 1. anything else is string `/.*/` + + // value holder(s) // |------|------|--------|----------------------------------| + public var fragment:String; + public var flags:Int; + public var x:Float; // |vector| x,y,z| comma-separated | #pos=1,2,3 | + public var y:Float; + public var z:Float; + public var color:String; // |string| color| FFFFFF (hex) | #fog=5m,FFAACC | + public var string:String; // |string| | | #q=-sun | + public var int:Int; // |int | | [-]x[xxxxx] | #price:>=100 | + public var float:Float; // |float | | [-]x[.xxxx] (ieee)| #prio=-20 | + public var args:Array; // |array | mixed| \|-separated | #pos=0,0,0\|90,0,0 | + public var query:Query; + // + public function new(_fragment:String,_flags:Int){ + fragment = _fragment; + flags = _flags; + } + + public function is(flag:Int):Bool { + return (flags & flag) != 0; + } + + public static function set(flag:Int, flags:Int):Int { + return flags | flag; + } + + public static function unset(flag:Int, flags:Int):Int { + return flags & ~flag; + } + + public function validate(value:String) : Bool{ + guessType(this, value); // 1. extract the type + // process multiple values + if( value.split("|").length > 1 ){ // 1. use `|` on stringvalues, to split multiple values + this.args = new Array(); + var args:Array = value.split("|"); + for( i in 0...args.length){ + var x:XRF = new XRF(fragment,flags); + guessType(x, args[i]); // 1. for each multiple value, guess the type + this.args.push( x ); + } + } + // special case: query has its own DSL (*TODO* allow fragments to have custom validators) + if( fragment == "q" ) query = (new Query(value)).get(); + // validate + var ok:Bool = true; + if( !Std.isOfType(args,Array) ){ + if( is(T_VECTOR3) && !(Std.isOfType(x,Float) && Std.isOfType(y,Float) && Std.isOfType(z,Float)) ) ok = false; + if( is(T_VECTOR2) && !(Std.isOfType(x,Float) && Std.isOfType(y,Float)) ) ok = false; + if( is(T_INT) && !Std.isOfType(int,Int) ) ok = false; + } + return ok; + } + + @:keep + public function guessType(v:XRF, str:String):Void { + v.string = str; + if( str.split(",").length > 1){ // 1. `,` assumes 1D/2D/3D vector-values like x[,y[,z]] + var xyz:Array = str.split(","); // 1. parseFloat(..) and parseInt(..) is applied to vector/float and int values + if( xyz.length > 0 ) v.x = Std.parseFloat(xyz[0]); // 1. anything else will be treated as string-value + if( xyz.length > 1 ) v.y = Std.parseFloat(xyz[1]); // 1. incompatible value-types will be dropped / not used + if( xyz.length > 2 ) v.z = Std.parseFloat(xyz[2]); // + } // > the xrfragment specification should stay simple enough + // > for anyone to write a parser using either regexes or grammar/lexers + if( isColor.match(str) ) v.color = str; // > therefore expressions/comprehensions are not supported (max wildcard/comparison operators for queries e.g.) + if( isFloat.match(str) ) v.float = Std.parseFloat(str); + if( isInt.match(str) ) v.int = Std.parseInt(str); + } + +} diff --git a/src/xrfragment/XRF.lua b/src/xrfragment/XRF.lua new file mode 100644 index 0000000..2f9701f --- /dev/null +++ b/src/xrfragment/XRF.lua @@ -0,0 +1,112 @@ +local XRF = {} + +XRF.ASSET = 1 +XRF.ASSET_OBJ = 2 +XRF.PV_OVERRIDE = 4 +XRF.QUERY_OPERATOR = 8 +XRF.PROMPT = 16 +XRF.ROUNDROBIN = 32 +XRF.BROWSER_OVERRIDE = 64 +XRF.T_COLOR = 128 +XRF.T_INT = 256 +XRF.T_FLOAT = 512 +XRF.T_VECTOR2 = 1024 +XRF.T_VECTOR3 = 2048 +XRF.T_URL = 4096 +XRF.T_PREDEFINED_VIEW = 8192 +XRF.T_STRING = 16384 +XRF.T_STRING_OBJ = 32768 + +XRF.isColor = string.match("#([A-fa-f0-9]{6}|[A-fa-f0-9]{3})", "") +XRF.isInt = string.match("^[0-9]+$", "") +XRF.isFloat = string.match("^[0-9]+%.[0-9]+$", "") +XRF.isVector = string.match("([,]+|%w)", "") +XRF.isUrl = string.match("(://)?..*", "") +XRF.isUrlOrPretypedView = string.match("(^#|://)?..*", "") +XRF.isString = string.match(".+", "") + +function XRF.new(_fragment, _flags) + local self = {} + self.fragment = _fragment + self.flags = _flags + self.x = 0 + self.y = 0 + self.z = 0 + self.color = "" + self.string = "" + self.int = 0 + self.float = 0 + self.args = {} + self.query = {} + function self.is(flag) + print(flag) + print(self.flags) + return self.flags & flag ~= 0 + end + return self +end + +function XRF.set(flag, flags) + return flags | flag +end + +function XRF.unset(flag, flags) + return flags & ~flag +end + +function XRF.validate(self, value) + XRF.guessType(self, value) + if string.len(value:split("|")) then + self.args = {} + for i, val in ipairs(value:split("|")) do + local xrf = XRF.new(self.fragment, self.flags) + XRF.guessType(xrf, val) + table.insert(self.args, xrf) + end + end + if self.fragment == "q" then + --self.query = (new Query(value)):get() + end + if not isinstance(self.args, 'array') then + if self:is(XRF.T_VECTOR3) and (not isinstance(self.x, 'number') or not isinstance(self.y, 'number') or not isinstance(self.z, 'number')) then + ok = false + end + if self:is(XRF.T_VECTOR2) and (not isinstance(self.x, 'number') or not isinstance(self.y, 'number')) then + ok = false + end + if self:is(XRF.T_INT) and not isinstance(self.int, 'number') then + ok = false + end + end + return ok +end + +function XRF.guessType(v, str) + v.string = str + if string.len(str:split(",")) > 1 then + local xyz = string.split(str, ",") + if #xyz > 0 then + v.x = tonumber(xyz[1]) + end + if #xyz > 1 then + v.y = tonumber(xyz[2]) + end + if #xyz > 2 then + v.z = tonumber(xyz[3]) + end + end + if XRF.isColor:match(str) then + v.color = str + end + if XRF.isFloat:match(str) then + v.float = tonumber(str) + end + if XRF.isInt:match(str) then + v.int = tonumber(str) + end +end + +x = XRF.new("pos", XRF.ASSET | XRF.ROUNDROBIN ) +print( "roundrobin: " .. ( x.is( XRF.ROUNDROBIN ) and "ja" or "nee" ) ) + +return XRF diff --git a/test/test.html b/test/test.html new file mode 100644 index 0000000..abe04fe --- /dev/null +++ b/test/test.html @@ -0,0 +1,118 @@ + + + + + + + +
+

XR Fragments

+
+
+

+ + + + + + + + +
+ +

+ +
+ +

+ +
+

+ ps. you can test BROWSER_OVERRIDE-enabled fragments (#pos=1,2,3 or #t=1,100 e.g.) by updating the url +
+

+

+ +

+ +

+
+
+
+ +