Source: lib/xutils/assetkeepr.js

import * as THREE from 'three';
import * as oboe from 'oboe';

// For why using source instead of npm package, see packages/three/README.md
import { GLTFLoader } from '../../packages/three/GLTFLoader'
import { SVGLoader } from '../../packages/three/SVGLoader'
import html2canvas from '../../packages/misc/html2canvas.js'
import { XError, ramTexture } from './xcommon'

import { x } from '../xapp/xworld'
import { vec3, mat4 } from '../xmath/vec'
import xgeom from '../xmath/geom'

/**Static assets buffer for global resource management
 * @memberof AssetKeepr
 */
const assets = {
	/**@property {Object} loaders - gltf / svg loaders
	 * @member assets#loaders
	 * @memberof AssetKeepr
	 */
	loaders: {},
	/**@property {Object} canvas - {domId: Canvas} map of Canvas components.
	 * @member assets#canvs
	 * @memberof AssetKeepr
	 */
	canvs: {}, // {domId: Components.Canvas} map of Canvas components.
	/**Light weight dynamic text canvase pool.
	 * @property {Object} dynatex - {domId: Dynatex} map of Dynatex components.
	 * @member assets#dynatex
	 * @memberof AssetKeepr
	 */
	dynatex: {},
	/**Svg Assets {key: url, value: [paths]}
	 * @property {object} svg - {key: url, value: object} map of Dynatex components.
	 * @member assets#svg
	 * @memberof AssetKeepr
	 */
	svg: {}
}

// test
const svgloader = new SVGLoader();

/**Assets Manager
 *
 * Note it's spelled "keepr", not "keeper".
 * @class AssetKeepr
 */
export default class AssetKeepr {
	/**Initialize
	 * @param {object} x singleton
	 * @member AssetKeepr.init
	 * @function */
	static init(x) {
		x.assetKeepr = this;
		x.assets = assets;
		assets.xref = x;
	}

	/**@member AssetKeepr#canvs
	 * @property {THREE.Container} canvs - get container canvas */
	static get canvs() {
		return assets.canvs;
	}

	/**@member AssetKeepr#assets
	 * @property {object} assets - get global assets map */
	static get assets() {
		return assets;
	}

	/**@member AssetKeepr#log
	 * @property {number} log - get log level */
	static get log() {
		return assets.log;
	}

	/**@member AssetKeepr#dynatex
	 * @property {number} dynatex - get dynamic canvas texture */
	static get dynatex() {
		return assets.dynatex;
	}

	static get greyPixel() {
		return 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5AQSDjsH5tmdcQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAMSURBVAjXY2hoaAAAAwQBgZrwzU8AAAAASUVORK5CYII=';
	}

	/**Get a spark texture, in 'data:image/png;base64'.
	 * @return {THREE.Texture}
	 * @member AssetKeepr.defaultex
	 * @function */
	static defaultex() {
		if(!assets.sparktex) {
			debugger
			assets.sparktex = new THREE.TextureLoader().load(
				// https://onlinepngtools.com/convert-png-to-base64
				// 16x16 version of spark1.png
				'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH5AIKBwk6Rv00TgAAAaVJREFUOMvVkk9rE1EUxc99b0ycpFJqwAgNARFc6a7gNhs/UbbDhDBhCPk8LlvIJi7diFrEKS5iikrQaekkzrx3jytDUv9uXHi2l/Pj3HMv8N9LfjXo9/uo1+sGgDjndDKZMIoijEajPwPiOA5U9QBAS1Ubxpgrkp9I5mma+t8CoigKRORQVY9E5AGAGoA1gNcAnnvvFyR1PB4DAIJtc5qmKIritqo+FpEnInJfRG6q6gLAXQA3RORERHIA/AFQVZUFcCgiD40xj8IwPGq1Wna1WhXL5fJEVT+LyEvv/cV3gLkGEJL7APaCIDhot9u22+2i0+k0rLUdAPsAboVhuPHsAEQEAEoATlUvarUarLXI8xze+5WIlCLinHOb7nZWsNYqyQ8AFt770yzL9ubz+Z31en1O8i3J9wA+JkmyuYTdBkynU/Z6va8APMkGyS9lWWYkM5JvADyrqupsNpv5nyYYDAaw1hZVVb1Q1UtjzD0AIclCVc+ste+azWa17dnpYDgcwjmHJEkuAbwyxhyTfGqMOTbGnCZJcuW951/9eBzHuJ7un+gbY1HV8WP17N0AAAAASUVORK5CYII=	'
			);
		}
		return assets.sparktex;
	}

	static cheapixelTex() {
		return AssetKeepr.loadTexure('data:application/x-visual+img,gray-pixel');
	}

 	// FIXME fix all asynchronous callback binding
 	// FIXME fix all asynchronous callback binding
 	// FIXME fix all asynchronous callback binding
	static loadTexure(url, onload) {
		// Debug Notes:
		// npm parse-data-url can't handling svg tags: https://github.com/killmenot/parse-data-url
		// var data = parseDataUrl(url);
		var tex;
		if (!url)
			return tex;
		else if (Array.isArray(url)) {
			tex = [];
			for (var ul of url) {
				tex.push(AssetKeepr.loadTexure(ul, onload));
			}
		}
		else if (url.startsWith('data:application/x-visual+img')) {
			if (url.substr(30, 10) === 'gray-pixel' ||
				url.substr(30, 10) === 'grey-pixel')
				tex = new THREE.TextureLoader().load(AssetKeepr.greyPixel, onload);
			else if (url.substr(30, 4) === 'stub')
				// no onload callback to stop replacing any ready texture
				tex = new THREE.TextureLoader().load(AssetKeepr.greyPixel);
			else if (url.substr(30, 11) === 'color-pixel') {
				var color = eval(url.substr(42));
				if(Array.isArray(color)) {
					var data = new Uint8Array(4);
					data[0] = color[0] * 255;
					data[1] = color[1] * 255;
					data[2] = color[2] * 255;
					data[3] = color.length > 3 ? color[3] * 255 : 255;
					tex = new THREE.DataTexture(data, 1, 1, THREE.RGBAFormat);

					if(typeof onload === 'function')
						onload(tex);
				}
			}
			else throw new XError('TODO Unknow data url: ', url);
		}
		else if (url.startsWith('data:')) {
			tex = new THREE.TextureLoader().load(url, onload);
		}
		else {
			var tex = new THREE.TextureLoader().load('assets/' + url, onload);
		}

		// if(typeof onload === 'function')
		// 	onload(tex);

		// should work in asynchronous
		return tex;
	}

	/**Load path of 3857 - not geojson, but json data are supposed the same format,
	 * except coordinates' values.
	 * @param {url} url
	 * @param {object} options
	 * @param {function} onLoad callback on assets loaded.
	 * @param {function} [onError] callback on oboe failed.
	 * @return {array} a 3d array of a fake path: [[[0, 0], [1, 0]]]
	 * @member AssetKeepr.geojsonPaths
	 * @function */
	static geojsonPaths(url, options, onLoad,
		onError = (args) => {
			console.error(args);
		}) {
		var op = Object.assign(new Object(), {
			start: 0,
			end: -1,
			count: -1,
			paths: new Array()
		});
		op = Object.assign(op, options);

		oboe(url)
			.node('features.*', (feature) => {
				op.count++;
				if(op.start >= op.count && (op.end < 0 || op.end > op.count)) {
					var elem = feature.geometry.coordinates;
					op.paths.push(elem);
				}
				if(op.end >= 0 && op.end >= op.count)
					this.abort();
			})
			.done((args) => {
				// console.log(args);
				onLoad(op.paths, op.count)
			})
			.fail(onError);

		return [ [ [0, 0], [1, 0] ] ]; // stub for creating mesh / path line
	}

	/**Load hexatile from points in 3857 - not geojson, but json data are supposed
	 * the as same format of geojson, except coordinates' values.
	 * @param {url} url
	 * @param {object} options
	 * @param {function} onload callback on assets loaded.
	 * arguments:
	 * BufferGeometry, each vertices has 'position', 'a_h', 'a_tan', 'uv' & 'normal' attributes
	 * @param {function} [onError] callback on oboe failed.
	 * @return {array} a 3d array of a fake cell vertices
	 * @member AssetKeepr.geoHexaprismAsync
	 * @function */
	static geoHexaprismAsync(url, options, onload,
		onError = (args) => {
			console.error(args);
		}) {

		var heightName = options.heightName || 'height';
		var verts = options.count * (14 + 12);
		var ctx = new Object();
		ctx.features = 0;
		ctx.vx = 0; // starting vert index for each feature. (26 vert / feature)
		ctx.r = options.radius >= 0 ? options.radius : 1;
		ctx.pos = new Float32Array(verts * 3);
		ctx.loc = new Float32Array(verts * 3);
		ctx.uvs = new Float32Array(verts * 2);
		ctx.normals = new Float32Array(verts * 3);
		ctx.dirs = new Float32Array(verts * 3);
		ctx.index = [];

		const tile0 = xgeom.hexatile(ctx.r);
		const hs = options.height;

		var hex = new THREE.PlaneBufferGeometry(100, 100, 0);
		oboe(url)
			.node('features.*', (feature) => {
				if(ctx.features >= options.count)
					return;

				ctx.features++;
				var h = feature.properties[heightName] || feature.geometry[heightName] || 1;
				var coord = feature.geometry.coordinates;
				var tuple = xgeom.hexaprism3857({
					coord,
					height: h * hs,
					geoScale: options.geoScale || 1,
					group: options.onGroup ? options.onGroup(fx, f) : 1
				}, options.geoCentre, tile0, ctx);

				// if(ctx.features >= options.count)
					// this.abort();
			})
			.done((args) => {
				var geom = new THREE.BufferGeometry();
				geom.setAttribute("position", new THREE.BufferAttribute(ctx.pos, 3));
				geom.setAttribute("a_tan", new THREE.BufferAttribute(ctx.dirs, 3));
				geom.setAttribute("a_loc", new THREE.BufferAttribute(ctx.loc, 3));
				geom.setAttribute('normal', new THREE.BufferAttribute(ctx.normals, 3));
				geom.setAttribute('uv', new THREE.BufferAttribute(ctx.uvs, 2));
				geom.setIndex(ctx.index);
				onload(geom, ctx.pos, ctx.vx);
			})
			.fail(onError);

		return { geom: hex };
	}

	/**Load hexatile from points in 3857 - not geojson, but json data are supposed
	 * the as same format of geojson, except coordinates' values.
	 * @param {array.<GeoFeature>} featrues
	 * @param {object} options
	 * @return {array} a 3d array of a fake cell vertices
	 * @member AssetKeepr.geoHexaprism
	 * @function */
	static geoHexaprism(features, options) {
		if(!features) return;

		var heightName = options.heightName || 'height';
		var verts = (features.length || 0) * (14 + 12);
		var ctx = new Object();
		ctx.vx = 0; // starting vert index for each feature. (26 vert / feature)
		ctx.r = options.radius >= 0 ? options.radius : 1;
		ctx.pos = new Float32Array(verts * 3);
		ctx.loc = new Float32Array(verts * 3);
		ctx.uvs = new Float32Array(verts * 2);
		ctx.normals = new Float32Array(verts * 3);
		ctx.dirs = new Float32Array(verts * 3);
		ctx.index = [];

		const tile0 = xgeom.hexatile(ctx.r);
		const hs = options.height;

		var fx = 0;
		for(var f of features) {
			var h = f.properties[heightName] || f.geometry[heightName] || 1;
			var coord = f.geometry.coordinates;
			xgeom.hexaprism3857({
				coord,
				height: h * hs,
				geoScale: options.geoScale || 1,
				group: options.onGroup ? options.onGroup(fx, f) : 1
			}, options.geoCentre, tile0, ctx);
			fx++;
		}

		var geom = new THREE.BufferGeometry();
		geom.setAttribute("position", new THREE.BufferAttribute(ctx.pos, 3));
		geom.setAttribute("a_tan", new THREE.BufferAttribute(ctx.dirs, 3));
		geom.setAttribute("a_loc", new THREE.BufferAttribute(ctx.loc, 3));
		geom.setAttribute('normal', new THREE.BufferAttribute(ctx.normals, 3));
		geom.setAttribute('uv', new THREE.BufferAttribute(ctx.uvs, 2));
		geom.setIndex(ctx.index);
		return {
			geom,
			points: ctx.pos
		};
	}

	/**Load hexatile from points in 3857 - not geojson, but json data are supposed
	 * the as same format of geojson, except coordinates' values.
	 * This should only used for real buildings with textures. For vitual buildings,
	 * use @{@link AssetKeepr.geoPrismBoxes}
	 * @param {array.<GeoFeature>} featrues
	 * @param {object} options
	 * @return {array} a 3d array of a fake cell vertices
	 * @member AssetKeepr.geoTexturePrism
	 * @function */
	static geoTexturePrism(features, options) {
		if(!features) return;

		var verts = options.maxVerts || 2048; // it's user's responsibility to find how many vertices is
		var ctx = new Object();
			ctx.features = 0;
			ctx.vx = 0; // starting vert index for each feature.
			ctx.pos = new Float32Array(verts * 3);
			ctx.loc = new Float32Array(verts * 3);
			ctx.atiles = new Float32Array(verts * 3);
			ctx.uvs = new Float32Array(verts * 2);
			ctx.normals = new Float32Array(verts * 3);
			ctx.box = new Float32Array(verts * 3);
			ctx.index = [];

		var fx = 0;
		for(var f of features) {
			// if (options.filter && f.properties
			// 	&& f.properties.area !== options.filter)
			if ( options.filter && (
				 !f.properties
				|| f.properties.area !== options.filter ))
				continue;
			AssetKeepr.feature2Prism(ctx, f, options);
		}

		var geom = new THREE.BufferGeometry();
			geom.setAttribute("position", new THREE.BufferAttribute(ctx.pos, 3));
			geom.setAttribute("a_box", new THREE.BufferAttribute(ctx.box, 3));// box.y = groupidx
			geom.setAttribute("a_tiles", new THREE.BufferAttribute(ctx.atiles, 3));
			geom.setAttribute("a_loc", new THREE.BufferAttribute(ctx.loc, 3));
			geom.setAttribute('normal', new THREE.BufferAttribute(ctx.normals, 3));
			geom.setAttribute('uv', new THREE.BufferAttribute(ctx.uvs, 2));
			geom.setIndex(ctx.index);
		return {
			geom,
			points: ctx.pos
		};
	}

	static feature2Prism(ctx, f, options) {
		const heightName = options.heightName || 'height';
		const hs = options.height;

		if (!f.geometry || !Array.isArray(f.geometry.coordinates)) {
			console.error('Incorrect data found to generate geoprism: ', f);
			return;
		}

		var h = f.properties ?
				f.properties[heightName] || f.geometry[heightName] || 1
				: 1;

		for (var points of f.geometry.coordinates) {
			// var points = f.geometry.coordinates;
			// var {xzwh, loc} = xgeom.xzBox(f.geometry.coordinates);
			var {xzwh, loc} = xgeom.xzBox(points);

			if (Array.isArray(f.geometry.center))
				loc = f.geometry.center;

			var boxSize = options.box || [xzwh.w, hs * h, xzwh.h];

			var tile = options.tiles ||
						[4, options.tile ? options.tile.layers || 1 : 1, 3];

			xgeom.texPrism3857( {
				points,
				boxSize, xzwh, tile,
				height: h * hs,
				prismCentre: loc,
				geoScale: options.geoScale || 1,
				group: options.onGroup ? options.onGroup(ctx.features, f) : 1
			}, options.geoCentre, ctx );
		}

		ctx.features++;
	}

	// prism with boxes
	static geoPrismAsync(url, options, onload,
		onError = (args) => {
			console.error(args);
		}) {

		var verts = options.verts || 4096 * (6*4 + 6); // 4k hexagon (FIXME)
		// context
		var ctx = new Object();
			ctx.features = 0;
			ctx.vx = 0; // starting vert index for each feature.
			ctx.pos = new Float32Array(verts * 3);
			ctx.loc = new Float32Array(verts * 3);
			ctx.atiles = new Float32Array(verts * 3);
			ctx.uvs = new Float32Array(verts * 2);
			ctx.normals = new Float32Array(verts * 3);
			ctx.box = new Float32Array(verts * 3);
			ctx.index = [];

		oboe(url)
		.node('features.*', (f) => {
			// FIXME is it safe using "options" like this?
			if(ctx.features > options.count)
				return; // FIXME why can't like this: this.abort();

			if ( options.filter && (
				 !f.properties
				|| f.properties.area !== options.filter ))
				return;

			AssetKeepr.feature2Prism(ctx, f, options);

			if (typeof options.onFeature === 'function')
				options.onFeature( ctx, f, options );
		})
		.done((args) => {
			var geom = new THREE.BufferGeometry();
			geom.setAttribute("position", new THREE.BufferAttribute(ctx.pos, 3));
			geom.setAttribute("a_box", new THREE.BufferAttribute(ctx.box, 3));// box.y = groupidx
			geom.setAttribute("a_tiles", new THREE.BufferAttribute(ctx.atiles, 3));
			geom.setAttribute("a_loc", new THREE.BufferAttribute(ctx.loc, 3));
			geom.setAttribute('normal', new THREE.BufferAttribute(ctx.normals, 3));
			geom.setAttribute('uv', new THREE.BufferAttribute(ctx.uvs, 2));
			geom.setIndex(ctx.index);
			onload(geom, ctx.pos, ctx.vx, options);
		})
		.fail(onError);

		return { geom: new THREE.PlaneBufferGeometry(100, 10, 0) };
	}

	/**load gltf model<br>
	 * If obj3 is provided, then after loaded, set the scene transform according
	 * to obj3.transfrom, set obj3.mesh = gltf.scene.<br>
	 * For details, see reference: GLTF format from x-visual docs.<br>
	 * Also see <a href='https://threejs.org/docs/#examples/en/loaders/GLTFLoader'>
	 * Three.js example - GLTFLoader</a>
	 * and <a href='https://threejsfundamentals.org/threejs/lessons/threejs-load-gltf.html'>
	 * Three.js tuturial of gltf loader</a>
	 * @param {THREE.Scene} [scene] if provided, add the loaded gltf.scene to it.
	 * @param {Obj3} [obj3] [in out] after loaded, set the scene transform according
	 * to obj3.transfrom, set obj3.mesh = gltf.scene
	 * @param {stirng} url A string containing the path/URL of the .gltf or .glb file.
	 * @param {function} onload callback on assets loaded.<br>
	 * If not provided, load entire gltf scene as child of main scene (the argument)<br>
	 * @member AssetKeepr.loadGltf
	 * @function */
	static loadGltf(scene, obj3, url, onload) {
		if(assets.loaders[url] === undefined)
			assets.loaders[url] = new GLTFLoader();
		assets.loaders[url].load(url, function(gltf, nodeMap) {
				if(typeof onload === 'function')
					onload(gltf, nodeMap);

				else {
					// default onload handling, load entire gltf scene as child of main scene (the argument)
					if(obj3) {
						var m4 = new mat4();
						if(obj3.transform) {
							for(var trs of obj3.transform)
								m4.appAffine(trs);
						}

						gltf.scene.matrixAutoUpdate = false;
						m4.put2js(gltf.scene.matrix);
						obj3.mesh = gltf.scene;

						//fix  refrence AffineCombiner3 initCombined  	e.Obj3.m0.decompose(e.Obj3.mesh.matrix);
						//set mesh matrix origin value
						if(obj3.m0){
							obj3.m0.decompose(obj3.mesh.matrix);
						}

					}

					if(scene) {
						scene.add(gltf.scene);
					} else console.warn(
						'AssetKeepr.loadGltf(): Loaded gltf assets without THREE.Scene instance: ',
						gltf.scene);
				}
			},
			function(xhr) {
				if(x.log >= 5) console.log(`[5] ${xhr.loaded / xhr.total * 100}% loaded`);
			},
			// called when loading has errors
			function(error) {
				console.error('AssetKeepr.loadGltf(): ', error);
			});
	}

	/**
	 * @param {Obj3} obj3
	 * @param {string} url
	 * @param {array<string>} nnames
	 * @param {function} onParsed
	 * @member AssetKeepr.loadGltfNodes
	 * @function */
	static loadGltfNodes(obj3, url, nnames, onParsed) {
		if(!nnames) onParsed(undefined);
		else {
			AssetKeepr.loadGltf(undefined, obj3, url, function(gltf, nodeMap) {
				var nods = [];
				for(var nname of nnames) {
					var nix;
					if(typeof nname === 'number')
						nix = nname;
					// Debug Notes;
					// 2020.05.18 node's names have been sanitized
					// see debug notes in docs/reference/gltf.html#the-x-visual-loader
					// also see packages/three/GLTFLoader.loadNode.then():
					// ... node.name = PropertyBinding.sanitizeNodeName( nodeDef.name );
					else nix = nodeMap[THREE.PropertyBinding.sanitizeNodeName(nname)];
					if(nix >= 0)
						nods.push(gltf.nodes[nix]);
				}
				onParsed(nods);
			});
		}
	}

	/**
	 * Initialize a Canvas component with a THREE.CanvasTexture from canvas, which
	 * is not initialized when return.
	 *
	 * The component will be updated when it's ready
	 *
	 * **DESIGN-MEMO**
	 *
	 * 1. This function maintance dirty with tick flag. Referenced canvase won't
	 * been updated if the component's stamp less than the asset's stamp
	 * 2. Texture canvas are sharable.
	 *
	 * *Reference*
	 *
	 * https://stackoverflow.com/questions/3768565/drawing-an-svg-file-on-a-html5-canvas<br>
	 * https://jsfiddle.net/Wijmo5/h2L3gw88/<br>
	 * https://bl.ocks.org/mbostock/1276463<br>
	 * threejs example: https://threejs.org/examples/?q=canvas#webgl_materials_texture_canvas<br>
	 * example source: https://github.com/mrdoob/three.js/blob/master/examples/webgl_materials_texture_canvas.html<br>
	 * threejs doc CanvasTexture: https://threejs.org/docs/index.html#api/en/textures/CanvasTexture
	 *
	 * @param {Canvas} cmpCanv Canvas component
	 * @param {number} stamp optional, if stamp is less than x.lastUpdate, ignore
	 * the request; if undefined, always update. So set this carefully for performance
	 * optimization - updating canvas is a heavy work load.
	 * @param {function} onUpdate optional, callback when html canvas texture updated.
	 * parameter: canvas {HTML.Canvas}, texture {THREE.CanvasTexture}
	 * @return {Canvas} the Canvas component referencing object:<br>
	 * where<br>
	 * **tex**: THREE.CanvasTexture when html canvas is initialized. But before this is undefined.<br>
	 * **canvas**: HTML canvas when html canvas is initialized. But before this is undefined.<br>
	 * **ctx2d**: HTML canvas 2d context when html canvas is initialized.<br>
	 * But before this its is undefined - can not been used for sampling before onUpdate called.
	 * @member AssetKeepr.loadCanvtex
	 * @function
	 */
	static loadCanvtex(cmpCanv, stamp, onUpdate) {
		const x = assets.xref;
		if(stamp === undefined)
			stamp = x.lastUpdate;
		// if stamp handled, no more update
		if(stamp < x.lastUpdate)
			return cmpCanv;

		var h5 = assets.canvs[cmpCanv.domId];
		if(!h5) {
			h5 = {
				domId: cmpCanv.domId,
				stamp: assets.xref.lastUpdate
			};
			assets.canvs[cmpCanv.domId] = h5;
		}
		if(h5.stamp <= x.lastUpdate) {
			if(typeof document !== 'object')
			; // testing ?
			else {
				var dom = document.getElementById(cmpCanv.domId);
				var opts = Object.assign({
						width: 256,
						height: 256
					},
					cmpCanv.options);

				html2canvas(dom, opts).then(function(canvas) {
					var drawingContext = canvas.getContext('2d');

					cmpCanv.canvas = drawingContext.canvas;
					cmpCanv.tex = new THREE.CanvasTexture(drawingContext.canvas);
					cmpCanv.ctx2d = drawingContext;
					// set dirty flag now to avoid updating by renderer before this texture is ready
					cmpCanv.dirty = true;
					// Memo:
					// reference entity.Obje in wrong way, but this method is to be deprecated
					cmpCanv.entity.Obj3.mesh.visible = true;

					if(typeof onUpdate === 'function')
						onUpdate(canvas, cmpCanv.tex);
				});
			}
		}
		return cmpCanv;
	}

	static loadCanvtex2(cmpCanv, options = {}, obj3, onUpdate) {
		var width = options && options.width ? options.width : 256;
		var height = options && options.heigth ? options.heigth : 256;
		var opts = Object.assign({
			width,
			height,
			// https://github.com/niklasvh/html2canvas/issues/1164
			allowTaint: true,
			useCORS: true,
			backgroundColor: "rgba(0,0,0,0)",
			removeContainer: true,
			x: 0,
			// y: options.y === undefined ? 0 : options.y
			y: 0,
			// y: 30.5
		}, options);
		var dom = document.getElementById(cmpCanv.domId);

		html2canvas(dom, opts).then(function(canvas) {
			var drawingContext = canvas.getContext('2d');

			cmpCanv.canvas = drawingContext.canvas;
			cmpCanv.tex = new THREE.CanvasTexture(drawingContext.canvas);
			// cmpCanv.tex = new ramTexture(7, 11, {alpha: 1});
			cmpCanv.ctx2d = drawingContext;
			cmpCanv.dirty = true;

			if(typeof onUpdate === 'function')
				onUpdate(canvas, cmpCanv.tex);
		});
		return cmpCanv;
	}

	static loadSvgPath(url, nodes, onload) {
		if(assets.loaders[url] && assets.svg[url].paths) {
			return getPaths(assets.svg[url].paths, nodes, onload);
		}

		function getPaths(mapath, nodes, onload) {
			var paths = [];
			for(var n of nodes) {
				paths.push(mapath[n]);
			}
			onload(paths);
		}

		AssetKeepr.loadSVG(url, {}, (group, paths) => {
			if(!assets.svg[url])
				assets.svg[url] = {
					paths: {}
				};
			for(var p of paths) {
				assets.svg[url].paths[p.pathId] = p;
			}
			return getPaths(assets.svg[url].paths, nodes, onload);
		});
	}

	/**
	 * Load svg - don't use this to load pathes
	 *
	 * @param {string} url string
	 * @param {object} opts
	 * opts.withMesh: bool - convert fill to mesh ( z = 0 )<br>
	 * opts.withStroke: bool - convert stroke to polygon ( z = 0 )<br>
	 * {drawFillShapes: bool, wireframe: THREE.material.wireframe, stroke: bool}
	 * @param {function} onload function(group: THREE.Group)
	 * @member AssetKeepr.loadSVG
	 * @function
	 */
	static loadSVG(url, opts, onload) {
		if(assets.loaders[url] === undefined)
			assets.loaders[url] = new SVGLoader();

		var loader = assets.loaders[url];
		loader.load(url, function(data) {
			var paths = data.paths;
			var group = new THREE.Group();

			if(opts.withMesh || opts.withStroke) {
				for(var i = 0; i < paths.length; i++) {
					var path = paths[i];
					if(opts.withMesh) {
						var fillColor = path.userData.style.fill;
						if(opts.drawFillShapes && fillColor !== undefined && fillColor !== 'none') {
							var material = new THREE.MeshBasicMaterial({
								color: new THREE.Color().setStyle(fillColor),
								opacity: path.userData.style.fillOpacity,
								transparent: path.userData.style.fillOpacity < 1,
								side: THREE.DoubleSide,
								depthWrite: false,
								wireframe: opts.wireframe
							});

							var shapes = path.toShapes(true);

							for(var j = 0; j < shapes.length; j++) {
								var shape = shapes[j];
								var geometry = new THREE.ShapeBufferGeometry(shape);
								var mesh = new THREE.Mesh(geometry, material);
								group.add(mesh);
							}
						}
					}

					if(opts.withStroke) {
						var strokeColor = path.userData.style.stroke;

						if(opts.stroke && strokeColor !== undefined && strokeColor !== 'none') {
							var material = new THREE.MeshBasicMaterial({
								color: new THREE.Color().setStyle(strokeColor),
								opacity: path.userData.style.strokeOpacity,
								transparent: path.userData.style.strokeOpacity < 1,
								side: THREE.DoubleSide,
								depthWrite: false,
								wireframe: opts.wireframe
							});

							for(var j = 0, jl = path.subPaths.length; j < jl; j++) {
								var subPath = path.subPaths[j];
								var geometry = SVGLoader.pointsToStroke(subPath.getPoints(), path.userData.style);
								if(geometry) {
									var mesh = new THREE.Mesh(geometry, material);
									group.add(mesh);
								}
							}
						}
					}
				}
			}

			onload(group, paths);
		});
	}

	static parseSVG(strSVG) {
		return svgloader.parse(strSVG);
	}

	/**
	 * <p>draw text - synchrodous</p>
	 * v0.3.21 change log: try scaling plane to adapt more or less words than
	 * paras defined canvas can hold, via only scaling texture;
	 *
	 * Test: test/html/dynatex.html
	 *
	 *see <a href='odys-z.github.io/x-visual/design-memo/renderer.html?highlight=dynatex'>docs/Dynamic Text</a>
	 *
	 * Reference:
	 * // https://threejsfundamentals.org/threejs/lessons/threejs-textures.html
	 * // https://threejsfundamentals.org/threejs/lessons/threejs-canvas-textures.html
	 *
	 * @param {Dynatex} cmp - options
	 * @param {string} cmp.text - the text to display
	 * @param {object} cmp.xywh see <a href='odys-z.github.io/x-visual/design-memo/renderer.html?highlight=dynatex'>docs/Dynamic Text</a>
	 * @param {string} [fillStyle] - the fillStyle to clear with, if not provided, fallback on .clearRect
	 * @param {string} [contextFont] - the font to use
	 * @return {THREE.CanvasTextrue}
	 * @member AssetKeepr.drawText
	 * @function
	 */
	static drawText(cmp) {
		var {text, xywh, style, font} = cmp;
		if(!assets.dynatex[text]) {
			var canvas = document.createElement('canvas');
			var ctx = canvas.getContext('2d');

			// fnt must set before measure text width
			var fnt;
			if(font !== undefined)
				// fnt = `${xywh.size}px bold sans-serif`;
				fnt = `${xywh.size}px ${font}`;
			else if(xywh.size)
				fnt = `${xywh.size}px Arial`;
			else fnt = "32px Arial";
			ctx.font = fnt;

			// https://threejsfundamentals.org/threejs/lessons/threejs-canvas-textures.html
			var margin = xywh.margin || 0;
			const doubleBorderSize = margin * 2;
			const width = ctx.measureText(text).width + doubleBorderSize;
			var height = (xywh.h || xywh.size) + doubleBorderSize;
			var hscale = 1;
			// scale plane? this is scale texture
			if (xywh.w > 0 && width > 0 && width > xywh.w) {
				hscale *= width/xywh.w;
				canvas.width = xywh.w*hscale;
				canvas.height = (xywh.h || xywh.size)*hscale;
			}
			else {
				canvas.width = xywh.w;
				canvas.height = xywh.h || xywh.size;
			}
			// debug notes: it's critical to set font after setting size
			ctx.font = fnt;
			ctx.textBaseline = 'top';

			// actually draw the text
			if(cmp['bg-color']) {
				ctx.fillStyle = cmp['bg-color'];
				ctx.fillRect(0, 0, ctx.canvas.width,ctx.canvas.height );
			}
			else {
				ctx.clearRect(0, 0, canvas.width, canvas.height);
			}
			ctx.fillStyle = style;
			ctx.fillText(text, (xywh.x || 0) + margin, (xywh.y || 0) + margin);

			//
			// https://threejsfundamentals.org/threejs/lessons/threejs-textures.html
			// https://threejsfundamentals.org/threejs/lessons/threejs-canvas-textures.html
			var texture = new THREE.CanvasTexture(canvas);
			texture.minFilter = THREE.LinearFilter;
			texture.wrapS = THREE.ClampToEdgeWrapping;
			texture.wrapT = THREE.ClampToEdgeWrapping;

			assets.dynatex[text] = {
				context: ctx,
				texture: texture
				};
		}
		return assets.dynatex[text].texture;
	};
}

/**Splashing screean helper.
 * <br>See <a href='https://stackoverflow.com/questions/647440/showing-a-splash-screen-during-script-execution'>AnthonyWJones' Answer @ stackoverflow</a>
 * @example
 * splash("mySplashDiv", longRunner)
 * function longRunner() {
 *    //This may take a while
 * }
 * @param {string} splashImgId
 * @param {function} xworker the long rounner
 * @param {string} src image
 *
 * @memberof xutils
 * @function
 */
export function splash(splashImgId, xworker, src) {
	var splashImg = document.getElementById(splashImgId)
	splashImg.style.display = "block";
	if(src)
		splashImg.src = src;
	else
		splashImg.src = 'assets/splash.gif';
	setTimeout(function() {
		xworker()
		splashImg.style.display = "none";
	}, 100);
}