major refactor

This commit is contained in:
Leon van Kammen 2023-04-14 14:45:50 +02:00
parent a01fb08e5d
commit d757dab4e0
8 changed files with 519 additions and 70 deletions

21
.github/workflows/website.yml vendored Normal file
View File

@ -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

View File

@ -1,11 +1,6 @@
package xrfragment; package xrfragment;
/** import xrfragment.XRF;
* # XR Fragments (key/value params)
*
* > = define in 3D asset-file (as custom property or default projection)<br>
* > = mutable, using navigator URI (`document.location.href` e.g.)<br>
*/
@:expose // <- makes the class reachable from plain JavaScript @:expose // <- makes the class reachable from plain JavaScript
@:keep // <- avoids accidental removal by dead code elimination @:keep // <- avoids accidental removal by dead code elimination
@ -14,14 +9,51 @@ class Parser {
@:keep @:keep
public static function parse(key:String,value:String,resultMap:haxe.DynamicAccess<Dynamic>):Bool { public static function parse(key:String,value:String,resultMap:haxe.DynamicAccess<Dynamic>):Bool {
var Frag:Map<String, EReg> = new Map<String, EReg>(); // | 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 | | // here we define allowed characteristics & datatypes for each fragment (stored as bitmasked int for performance purposes)
Frag.set("q", Type.isString); // | q | string | ⛁ |Query Selector | | var Frag:Map<String, Int> = new Map<String, Int>();
var vec:String = "1,2,3"; //
//if( Type.isVector(vec) ) trace("ja"); // 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 * # XR Fragments parser
* *
@ -29,65 +61,19 @@ class Parser {
* the gist of it: * the gist of it:
*/ */
if( Frag.exists(key) ){ // 1. check if param exist 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:XRF = new XRF(key, Frag.get(key));
var v:Value = new Value(); if( !v.validate(value) ){
guessType(v, value); // 1. extract the type trace("[ i ] fragment '"+key+"' has incompatible value ("+value+")");
// process multiple values return false;
if( value.split("|").length > 1 ){ // 1. use `|` on stringvalues, to split multiple values }
v.args = new Array<Value>(); resultMap.set(key, v );
var args:Array<String> = value.split("|"); }else{ trace("[ i ] fragment '"+key+"' does not exist or has no type typed (yet)"); return false; }
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; }
return true; 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<String> = 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<Value>; // |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 /// # Tests
/// ///

95
src/xrfragment/Parser.lua Normal file
View File

@ -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)

View File

@ -59,6 +59,10 @@ class Query {
return classAlias.match(token) ? StringTools.replace(token,".","class:") : token; 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 { public function parse(str:String,recurse:Bool = false) : Dynamic {
var token = str.split(" "); var token = str.split(" ");
@ -103,8 +107,7 @@ class Query {
} }
} }
for( i in 0...token.length ) process( expandAliases(token[i]) ); for( i in 0...token.length ) process( expandAliases(token[i]) );
this.q = q; return this.q = q;
return this.q;
} }
@:keep @:keep

View File

@ -1,6 +1,7 @@
package xrfragment; package xrfragment;
import xrfragment.Parser; import xrfragment.Parser;
import xrfragment.XRF;
/** /**
* <link rel="stylesheet" href="style.css"/> * <link rel="stylesheet" href="style.css"/>
@ -41,7 +42,7 @@ import xrfragment.Parser;
@:keep // <- avoids accidental removal by dead code elimination @:keep // <- avoids accidental removal by dead code elimination
class URI { class URI {
@:keep @:keep
public static function parse(qs:String):haxe.DynamicAccess<Dynamic> { public static function parse(qs:String,browser_override:Bool):haxe.DynamicAccess<Dynamic> {
var fragment:Array<String> = qs.split("#"); // 1. fragment URI starts with `#` var fragment:Array<String> = qs.split("#"); // 1. fragment URI starts with `#`
var splitArray:Array<String> = fragment[1].split('&'); // 1. fragments are split by `&` var splitArray:Array<String> = fragment[1].split('&'); // 1. fragments are split by `&`
var resultMap:haxe.DynamicAccess<Dynamic> = {}; // 1. store key/values into a associative array or dynamic object var resultMap:haxe.DynamicAccess<Dynamic> = {}; // 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 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; return resultMap;
} }
} }

107
src/xrfragment/XRF.hx Normal file
View File

@ -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<XRF>; // |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<XRF>();
var args:Array<String> = 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<String> = 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);
}
}

112
src/xrfragment/XRF.lua Normal file
View File

@ -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

118
test/test.html Normal file
View File

@ -0,0 +1,118 @@
<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/axist@latest/dist/axist.min.css" />
</head>
<body>
<script src="./../dist/xrfragment.js"></script>
<script>
var DOMReady = function(a,b,c){b=document,c='addEventListener';b[c]?b[c]('DOMContentLoaded',a):window.attachEvent('onload',a)}
DOMReady(function () {
let XRF = xrfragment;
let $ = (e) => document.querySelector(e)
log = (str) => {
$("#console").innerHTML = JSON.stringify(str, null, 2)
$("#console").style.border = "10px solid #"+ ((1 << 24) * Math.random() | 0).toString(16).padStart(6, "0")+"66";
}
let update = () => {
let result = {}
XRF.Parser.parse( $('#fragment').value, $('#value').value, result );
log(result)
}
$("#fragment").addEventListener("change", (e) => {
let opt = e.target.options[ e.target.selectedIndex ]
$('#value').value = opt.getAttribute("x")
update()
})
$('#value').addEventListener("change", update )
addEventListener("hashchange", (e) => log( XRF.URI.parse( document.location.href, true ) ) );
document.location.hash = "#pos=0,0,0"
})
</script>
<section id="forms">
<a class="title-link" href="#forms"><h3 class="title">XR Fragments</h3></a>
<form>
<fieldset>
<p>
<table>
<tr>
<td width="200">
<label for="fragment">Property in 3D file</label>
<br><br>
<select id="fragment" class="w-100">
<optgroup label="asset loading / linking">
<option x="1">prio</option>
<option x="pos=0,0,1&fov=2">#</option>
<option x="foo">class</option>
<option x="./2.gtlf">src</option>
<option x="radio.mp3">src_audio</option>
<option x="vert.glsl|frag.glsl">src_shader</option>
<option x="://env.jpg|1|equirect" >src_env</option>
<option x="podcast.mp3">src_env_audio</option>
</optgroup>
<optgroup label="href navigation / portals / teleporting">
<option x="1,2,0">pos</option>
<option x="3.gltf#q=.kitchen">href</option>
</optgroup>
<optgroup label="query selector / object manipulation">
<option x=".summer -.winter cube" selected="selected" default>q</option>
<option x="2" >scale</option>
<option x="1,2,3">rot</option>
<option x="1,1,1">translate</option>
<option x="1">visible</option>
</optgroup>
<optgroup label="animation">
<option x="1,200">t</option>
<option x="0,-9.8,0">gravity</option>
<option x="0.2,1">physics</option>
<option x=#scroll=1,0|2s">scroll</option>
</optgroup>
<optgroup label="device / viewport settings">
<option x="90">fov</option>
<option x="1,100">clip</option>
<option x="5m|FFAACC">fog</option>
</optgroup>
<optgroup label="author / metadata">
<option x="XXX">namespace</option>
<option x="GPL-3.0-or-later">SPFX</option>
<option x="1m">unit</option>
<option x="this is an example scene">description</option>
</optgroup>
<optgroup label="multiparty">
<option x="matrix://matrix.org/#myroom&room.key=123">src_session</option>
</optgroup>
</select>
</td>
<td style="text-align:left">
<label for="value">Value</label>
<br><br>
<input id="value" class="w-100" type="text" value=".summer -.winter cube" placeholder="" >
</td>
</tr>
<tr>
<td colspan="2" style="border=bottom:none;color:#666">
<br><br>
ps. you can test BROWSER_OVERRIDE-enabled fragments (<i>#pos=1,2,3</i> or <i>#t=1,100</i> e.g.) by updating the url
</td>
</tr>
</table>
</p>
<p>
<label for="console">parser output:</label>
<br><br>
<textarea id="console" style="width:100%;height:50vh;" placeholder="" class="w-100"></textarea>
</p>
</fieldset>
</form>
</section>
</body>
</html>