Source: lib/xapp/xworld.js

import * as ECS from '../../packages/ecs-js/index';

import * as THREE from 'three'

// npm i --save-dev babel-plugin-wildcard
// .babelrc: { "plugins": ["wildcard"] }
import {vec3} from '../xmath/vec'
import {XError} from '../xutils/xcommon'
import AssetKeepr from '../xutils/assetkeepr'
import Input from '../sys/input'
import GpuPicker from '../sys/gpupicker'
import Mapctrl from '../sys/mapctrl'
import Camctrl from '../sys/camctrl'
import ChannelFilter from '../sys/channelfilter'
import Thrender from '../sys/thrender'
import XMaterials from '../sys/materials'
import Hud from '../sys/hud'
import CanvTex from '../sys/canvtex'
import {MorphingAnim} from '../sys/tween/animizer'
import XTweener from '../sys/tween/xtweener'
import AffineCombiner3 from '../sys/tween/affinecombiner'
import FinalComposer from '../sys/ext/finalcomposer'
import PathEffect from '../sys/ext/patheffect'
import FilmEffect from '../sys/ext/filmeffect'
import GlowEffect from '../sys/ext/gloweffect'

// import {CmpCluster} from '../chart/clusters'
import * as visual from '../component/visual'
import * as obj3 from '../component/obj3'
import {GpuPickable} from '../component/pickable'
import * as Tweens from '../component/tween'
import * as CAnims from '../component/morph'
import {CameraCtrl} from '../component/mvc'
import * as CmpMvc from '../component/mvc'
import * as Composers from '../component/ext/effects'

/**Global singleton, xworld, options, ...
 *
 * FIXME merge with x.js/x
 * @class x
 */
const x = {
	ver: 'v0.1',
	/**log level,
	 * @property {int} log - 6: verbose for debugging<br>
	 * 5: informal for test<br>
	 * 4: testing<br>
	 * 3: alpha<br>
	 * 1: running
	 * @memberof x */
	log: 5,
	lastUpdate: -Infinity,
	/**
	 * @property {Map} assets - assets buffer (Don't modify)
	 * @member x#assets */
	assets: undefined,

	/**
	 * @property {int} xview - global view (Don't modify)
	 * @member x#xview */
	xview: undefined,
	/** XCamera: {pos: [0, 0, 0], cam: camera}
	 * @property {object} xcam
	 * @member x#xcam */
	xcam: undefined,
	up: new THREE.Vector3(0, 1, 0),

	/** default alpha used for test. E.g. FinalComposer use this to render bg. */
	alpha0: 0.2,
	// Thrender object
	xthrender:null
};

// Patches
	THREE.Layers.prototype.push = function (mask) {
		if (!this.stack)
			this.stack = [];
		this.stack.push(this.mask);
		this.mask = mask;
		return this;
	}

	THREE.Layers.prototype.pop = function () {
		var msk = this.maks;
		if (!this.stack) {
			this.mask = 0;
		}
		else {
			this.mask = this.stack.pop();
		}
		return msk;
	}


/**
 * x-visual world
 * @class XWorld
 */
export default class XWorld {
	/**@property {ECS} x - x-visual global data (singleton) */
	get x() { return x; }
	/**@property {ECS} xecs - ECS instance
	 * @member XWorld#xecs */
	get xecs() { return x.ecs; }
	/**@property {THREE.Scene} xscene - main scene
	 * @member XWorld#xscene */
	get xscene() { return x.scene; }
	/**@property {object} xview - X view singleton: {flag, cmds}.
	 * @member XWorld#xview */
	get xview() { return x.xview; }
	/**@property {THREE.PerspectiveCamera} xcam - main camera
	 * @member XWorld#xcam */
	get xcam() { return x.xcam.XCamera.cam; }

	/**@property {THREE.HemisphereLight} xlight - light, with property "options"
	 * @member XWorld#xlight */
	get xlight() { return x.light; }
	/**xlight <a href='https://stackoverflow.com/questions/19531845/can-js-have-getters-and-setters-methods-named-same-as-property'>
	 * getter</a>. see {@link  XMaterial#light}
	 * @property {object} xlight - set light parameters that can be applied to
	 * objects, of which xlight.hemisphere = THREE.HemisphereLight
	 * @param {object} p argument to setup light.
	 * @member XWorld#xlight */
	set xlight(p) {
		this.materials.changeLight(p);
		return this;
	}
	/**@property {XMaterials} xmaterials - material & light,
	 * @member XWorld#xlight */
	get xmaterials() { return this.materials; }

	get stamp() { return x.lastUpdate; }
	get lastick() { return x.ecs.lastick; }
	get xthrender() {return x.xthrender; }
	/**@property {THREE.HemisphereLight} bgColor - background color
	 * @param {object} c argument to create THREE.Color
	 * @member XWorld#bgColor */
	set bgColor(c) {
		if (x.scene)
			x.scene.background = new THREE.Color(c);
		else console.warn(
			'background can only been set after rendering stated');
		return this;
	}

	/**Create x-world.
	 * @param {Canvas} canvas html dom canvas
	 * @param {Window} wind
	 * @param {object} options <br>
	 * options.frustum: { fov, ratio, near, far },<br>
	 * options.camera: { position, lookAt }, where value in [x, y, z]<br>
	 * options.control: {@link CameraCtrl}
	 * @constructor XWorld */
	constructor(canvas, wind, opts) {
		if (opts && opts.log > 0) x.log = opts.log;
		x.options = opts || {};
		x.world = this;
		x.window = wind;
		x.container = canvas;
		const ecs = new ECS.ECS();
		// Cons of ECS - how to be polymorhpism? Extends a new ECS?
		ecs.componentTriggered (['FlowingPath', 'GlowingEdge'],
			(def) => x.options.pathEffect = true);
		ecs.componentTriggered (['Glow'],
			(def) => x.options.glowEffect = true);
		ecs.componentTriggered (['Filming'],
			(def) => x.options.filmEffect = true);
		x.ecs = ecs;

		if (x.options.outlinePass === undefined || x.options.outlinePass !== false)
			x.options.outlinePass = true;

		Object.assign(x.options, {canvas});

		this.registerComponents(CmpMvc);	// Input
		this.registerComponents({GpuPickable});
		this.registerComponents(visual);

		x.xview = {
			tick: -1,
			Input: undefined,
			// e.g. [ {code: 'key', cmd: 'left'}, {code: 'mouse', cmd: 'click'} ]
			cmds: [],
			flag: 0 };
		x.xview.Input = new Input(ecs, x, x.xview);

		var camopt = Object.assign( {fov: 30, ratio: 2.0, near: 1, far: 5000},
									x.options.frustum );
		if (x.options && x.options.camera) {
			Object.assign(camopt, x.options.camera);
		}
		var camera = new THREE.PerspectiveCamera(
						camopt.fov, camopt.ratio,
						camopt.near, camopt.far );

		if (x.options.camera && x.options.camera.position) {
			camera.position.x = x.options.camera.position[0];
			camera.position.y = x.options.camera.position[1];
			camera.position.z = x.options.camera.position[2];
		}
		else {
			camera.position.z = 400;
			camera.position.y = 50;
		}

		if (x.options.camera && x.options.camera.lookAt) {
			var l = x.options.camera.lookAt;
			// camera.lookAt is overriden by Mapctrl or Camctrl
			camera.lookAt(l[0], l[1], l[2]);
			camera.xtarget = new vec3(l);
		}
		else {
			camera.lookAt(0, 50, 0);
			camera.xtarget = new vec3(0, 50, 0);
		}

		x.xcam = ecs.createEntity({
			id: 'xcam',
			XCamera: {pos: [0, 0, 0], cam: camera}
		});

		this.registerComponents(Tweens);
		this.registerComponents(CAnims);

		this.registerComponents(obj3);

		// this.registerComponents(CmpCluster)

		this.registerComponents(Composers);

		x.lastUpdate = performance.now();
		// x.tickTime = 0;
		AssetKeepr.init(x);
	}

	/** ecs.registerComponent(name, ComponentExports[name]);
	 * @param {object | Component} comps a component or an object of {comp-name, comp}
	 * @member XWorld#registerComponents
	 * @function
	 */
	registerComponents(comps) {
		if (comps) {
			for (const name of Object.keys(comps)) {
				if (x.log >= 5) console.log('[5] register components ', name);
				x.ecs.registerComponent(name, comps[name]);
			}
		}
	}

	/**
	 * @member XWorld#startUpdate
	 * @function
	 */
	startUpdate() {
		// NOTE The initializing order can not been changed.
		// dependency:
		// PathEffect <-/ FinalComposer (options.effects == true)
		// Thrender <--/
		// Thrender <- LayerFilter
		// Thrender <- XMaterial
		// Thrender <- Hud
		// Thrender <- PathEffect
		// Thrender <- Inputs
		// Thrender <- Animizer
		// XTweener <- Animizer
		var ecs = x.ecs;

		this.channelFilter = new ChannelFilter(ecs, x);
		ecs.addSystem('render', this.channelFilter);

		var sys3 = new Thrender(ecs, x);
		ecs.addSystem('render', sys3);

			if (x.options.effects || x.options.pathEffect) {
				var pathEffect = new PathEffect(ecs, x);
				ecs.addSystem('render', pathEffect);
			}

			if (x.options.effects || x.options.filmEffect) {
				var filmEffect = new FilmEffect(ecs, x);
				ecs.addSystem('render', filmEffect);
			}

			if (x.options.effects || x.options.glowingEdgeEffect) {
				var glowEffect = new GlowEffect(ecs, x);
				ecs.addSystem('render', glowEffect);
			}

			if (x.options.effects ||
				x.options.pathEffect || x.options.filmEffect || x.options.glowingEdgeEffect) {
				var finalComposer = new FinalComposer(ecs, x);
				ecs.addSystem('render', finalComposer);
			}

			this.materials = new XMaterials(ecs, x);
			ecs.addSystem('render', this.materials);

			ecs.addSystem('render', new Hud(ecs, x));
			ecs.addSystem('render', new CanvTex(ecs, x));

		// add subsystems
		ecs.addSystem('mvc', x.xview.Input);
		ecs.addSystem('mvc', new GpuPicker(ecs, x));

		if (x.options.control === CameraCtrl.Mapctrl) {
			ecs.addSystem('mvc', new Mapctrl(ecs,
					{canvas: x.container, renderer: sys3.renderer,
					 camera: sys3.camera, control: sys3.control}));
		}
		else if (x.options.control === CameraCtrl.Orbitctrl) {
			ecs.addSystem('mvc', new Mapctrl(ecs,
					{canvas: x.container, renderer: sys3.renderer,
					 camera: sys3.camera, control: sys3.control}));
		}
		else {
			ecs.addSystem('mvc', new Camctrl(ecs,
					{canvas: x.container, renderer: sys3.renderer,
					 camera: sys3.camera, control: sys3.control}));
		}

		// animize & tweener
		var resolvingStarts = {}; // Animizer triggered tweens to be started when initing
		var animizer = new MorphingAnim(ecs, {});
		Object.assign(resolvingStarts, animizer.startings);
		ecs.addSystem('tween', new XTweener(ecs, x, resolvingStarts));
		ecs.addSystem('tween', new AffineCombiner3(ecs, x));

		// start animation
		this.update();
	}

	/**this.ecs.runSystemGroup('input');<br>
	 * this.ecs.runSystemGroup('render');<br>
	 * ...
	 * @param {number} time
	 * @member XWorld.update
	 * @function
	 */
	update(time) {
		if (typeof x.window === 'object') {
			x.window.requestAnimationFrame(this.update.bind(this));
		}
		// else {testing without window object}

		x.lastUpdate = performance.now();
		x.ecs.tick();

		x.ecs.runSystemGroup('mvc');
		x.ecs.runSystemGroup('render');
		x.ecs.runSystemGroup('tween');

		if (x.userSysGroup) {
			for (const g in x.userSysGroup) {
				x.ecs.runSystemGroup(x.userSysGroup[g]);
			}
		}
	}

	/**Add systems.
	 * @param {string} group group name
	 * @param {System} sys ECS system
	 * @return {XWorld} this
	 * @member XWorld#addSystem
	 * @function
	 */
	addSystem(group, sys) {
		if (x.userSysGroup === undefined) {
			x.userSysGroup = {};
		}
		x.userSysGroup[group] = group;

		x.ecs.addSystem(group, sys);
		return this;
	}

	/**Add entities.
	 * @param {array | object} defs e.g.<br>
	 * [{ Obj3: {...}, ... }]
	 * @return {array} entities been added
	 * @member XWorld#addEntities
	 * @function
	 */
	addEntities(defs) {
		if (!Array.isArray(defs))
			defs = [defs];
		const es = [];
		defs.forEach(function(d) {
			es.push(x.ecs.createEntity(d));
		});
		return es;
	}

	/**Set channel state.
	 * @param {int} ch channel number 0 ~ 31, where 0 is all visible
	 * @param {bool} pass
	 * @return {XWorld} this
	 * @member XWorld#setChannel
	 * @function
	 * */
	setChannel(ch, pass) {
		if (typeof ch !== 'number')
			throw new XError('ch must be a number in [0 ~ 31].');
		if (this.channelFilter === undefined)
			throw new XError('xworld.setChannel() can only been called after starting updating.');
		if (pass)
			this.channelFilter.pass = ch;
		else
			this.channelFilter.occlude = ch;
		return this;
	}
}

export {x}