infamous
Version:
A CSS3D/WebGL UI library.
285 lines (222 loc) • 10.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.initImperativeBase = initImperativeBase;
exports.default = void 0;
var _lowclass = _interopRequireDefault(require("lowclass"));
var _three = require("three");
var _Mixin = _interopRequireDefault(require("./Mixin"));
var _ElementOperations = _interopRequireDefault(require("./ElementOperations"));
var _Transformable = _interopRequireDefault(require("./Transformable"));
var _Node = _interopRequireDefault(require("./Node"));
var _Scene = _interopRequireDefault(require("./Scene"));
var _Motor = _interopRequireDefault(require("./Motor"));
var _Utility = require("./Utility");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
let threeObject3d = null;
let domPlane = null; // We explicitly use `var` instead of `let` here because it is hoisted for the
// Node and Scene modules. This, along with the following initImperativeBase
// function, allows the circular dependency between this module and the Node and
// Scene modules to work. For details on why, see
// https://esdiscuss.org/topic/how-to-solve-this-basic-es6-module-circular-dependency-problem.
var ImperativeBase; // Here we wrap the definition of the ImperativeBase class with this function in
// order to solve the circular depdendency problem caused by the
// Node<->ImperativeBase and Scene<->ImperativeBase circles. The Node and Scene
// modules call initImperativeBase to ensure that the ImperativeBase declaration
// happens first, and then those modules can use the live binding in their
// declarations.
exports.default = ImperativeBase;
initImperativeBase();
function initImperativeBase() {
if (ImperativeBase) return;
/**
* The ImperativeBase class is the base class for the Imperative version of the
* API, for people who chose to take the all-JavaScript approach and who will
* not use the HTML-based API (infamous/motor-html).
*
* In the future when there is an option to disable the HTML-DOM rendering (and
* render only WebGL, for example) then the imperative API will be the only API
* available since the HTML API will be turned off as a result of disabling
* HTML rendering. Disabling both WebGL and HTML won't make sense, as we'll need
* at least one of those to render with.
*/
exports.default = ImperativeBase = (0, _Mixin.default)(Base => (0, _lowclass.default)('ImperativeBase').extends(_Transformable.default.mixin(Base), ({
Super
}) => ({
constructor(options = {}) {
const self = Super(this).constructor(options);
self._lastKnownParent = null; // we don't need this, keep for backward compatibility (mainly
// all my demos at trusktr.io).
self.imperativeCounterpart = self;
self._willBeRendered = false; // Here we create the DOM HTMLElement associated with this
// Imperative-API Node.
self._elementOperations = new _ElementOperations.default(self); // stores a ref to this Node's root Scene when/if this Node is
// in a scene.
self._scene = null; // See Transformable/Sizeable propertychange event.
// TODO: defer size calculation to render task
self.on('propertychange', self._onPropertyChange, self);
if (!(self instanceof _Scene.default)) self.initWebGl();
return self;
},
connectedCallback() {
Super(this).connectedCallback(); // If a subclass needs to initialize values in its Three.js
// object, it will have the passInitialValuesToThree method for
// that.
//
// TODO we shouldn't need to define passInitialValuesToThree in
// sub classes, the default values of the props should
// automatically be in place.
this.passInitialValuesToThree && this.passInitialValuesToThree();
},
_onPropertyChange(prop) {
if (prop == 'sizeMode' || prop == 'size') {
this._calcSize();
}
this._needsToBeRendered();
},
initWebGl() {
this.threeObject3d = this.makeThreeObject3d(); // we don't let Three update local matrices, we provide world
// matrices ourselves.
this.threeObject3d.matrixAutoUpdate = false;
},
disposeWebGL() {
console.log('TODO: dispose WebGL when it is no longer needed');
},
makeThreeObject3d() {
throw new Error('The makeThreeObject3d method should be defined by sub classes.');
},
// TODO use one of init/deinit, or connected/connected, or
// connectedCallback/disconnectedCallback, instead of being
// inconsistent across classes, so we can better understand order of
// operations more easily.
connected() {
this._lastKnownParent = this.parent;
this.parent.threeObject3d.add(this.threeObject3d);
this.on('worldMatrixUpdate', this._onWorldMatrixUpdate, this);
},
disconnected() {
this._lastKnownParent.threeObject3d.remove(this.threeObject3d);
this.off('worldMatrixUpdate', this._onWorldMatrixUpdate);
},
_onWorldMatrixUpdate() {
threeObject3d = this.threeObject3d;
domPlane = this.threeDOMPlane; // Three Matrix4#elements is in the same major order as our
// DOMMatrix#_matrix. If we were to use Matrix4#set here, we'd have
// to swap the order when passing in our DOMMatrix#_matrix.
// Three.js r88, Issue #12602
for (let i = 0; i < 16; i += 1) {
threeObject3d.matrixWorld.elements[i] = this._worldMatrix._matrix[i];
if (domPlane) domPlane.matrixWorld.elements[i] = this._worldMatrix._matrix[i];
} // Since we're not letting Three auto update matrices, we also need
// to update the inverse matrix for cameras.
if (threeObject3d instanceof _three.Camera) threeObject3d.matrixWorldInverse.getInverse(threeObject3d.matrixWorld);
},
/**
* Subclasses are required to override this. It should return the HTML-API
* counterpart for this Imperative-API instance. See Node or Scene classes
* for example.
*
* @private
*/
_makeElement() {
throw new Error('Subclasses need to override ImperativeBase#_makeElement.');
},
/**
* @readonly
*/
get element() {
return this._elementOperations.element;
},
/**
* Get the Scene that this Node is in, null if no Scene. This is recursive
* at first, then cached.
*
* This traverses up the scene graph tree starting at this Node and finds
* the root Scene, if any. It caches the value for performance. If this
* Node is removed from a parent node with parent.removeChild(), then the
* cache is invalidated so the traversal can happen again when this Node is
* eventually added to a new tree. This way, if the scene is cached on a
* parent Node that we're adding this Node to then we can get that cached
* value instead of traversing the tree.
*
* @readonly
*/
get scene() {
// NOTE: this._scene is initally null, created in the constructor.
// if already cached, return it. Or if no parent, return it (it'll be null).
// Additionally, Scenes have this._scene already set to themselves.
if (this._scene || !this.parent) return this._scene; // if the parent node already has a ref to the scene, use that.
if (this.parent._scene) {
this._scene = this.parent._scene;
} else if (this.parent instanceof _Scene.default) {
this._scene = this.parent;
} // otherwise call the scene getter on the parent, which triggers
// traversal up the scene graph in order to find the root scene (null
// if none).
else {
this._scene = this.parent.scene;
}
return this._scene;
},
/**
* @override
*/
add(childNode) {
if (!(0, _Utility.isInstanceof)(childNode, ImperativeBase)) return; // We cannot add Scenes to Nodes, for now.
if (childNode instanceof _Scene.default) {
throw new Error(`
A Scene cannot be added to another Node or Scene (at
least for now). To place a Scene in a Node, just mount
a new Scene onto a MotorHTMLNode with Scene.mount().
`);
}
Super(this).add(childNode); // Pass this parent node's Scene reference (if any, checking this cache
// first) to the new child and the child's children.
if (childNode._scene || childNode.scene) {
childNode._giveSceneRefToChildren();
} // Calculate sizing because proportional size might depend on
// the new parent.
childNode._calcSize();
childNode._needsToBeRendered(); // child should watch the parent for size changes.
this.on('sizechange', childNode._onParentSizeChange);
this._elementOperations.connectChildElement(childNode);
return this;
},
remove(childNode,
/*private use*/
leaveInDom) {
if (!(childNode instanceof _Node.default)) return;
Super(this).remove(childNode);
this.off('sizechange', childNode._onParentSizeChange);
childNode._resetSceneRef();
if (!leaveInDom) this._elementOperations.disconnectChildElement(childNode);
},
_needsToBeRendered() {
// we don't need to render until we're connected into a tree with a scene.
if (!this.scene || !this.isConnected) return; // TODO make sure we render when connected into a tree with a scene
this._willBeRendered = true;
_Motor.default.setNodeToBeRendered(this);
},
// This method is used by Motor._renderNodes().
_getAncestorThatShouldBeRendered() {
let parent = this.parent;
while (parent) {
if (parent._willBeRendered) return parent;
parent = parent.parent;
}
return false;
},
_render(timestamp) {
if (Super(this)._render) Super(this)._render(); // applies the transform matrix to the element's style property.
this._elementOperations.applyImperativeNodeProperties(this);
}
}) // TODO make a classes prop?
// set properties(properties = {}) {
// Super(this).properties = properties
//
// if (properties.classes)
// this._elementOperations.setClasses(...properties.classes);
// },
));
}