playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
654 lines (653 loc) • 16.5 kB
JavaScript
import { EventHandler } from "../core/event-handler.js";
import { Tags } from "../core/tags.js";
import { Mat3 } from "../core/math/mat3.js";
import { Mat4 } from "../core/math/mat4.js";
import { Quat } from "../core/math/quat.js";
import { Vec3 } from "../core/math/vec3.js";
const scaleCompensatePosTransform = new Mat4();
const scaleCompensatePos = new Vec3();
const scaleCompensateRot = new Quat();
const scaleCompensateRot2 = new Quat();
const scaleCompensateScale = new Vec3();
const scaleCompensateScaleForParent = new Vec3();
const tmpMat4 = new Mat4();
const tmpQuat = new Quat();
const position = new Vec3();
const invParentWtm = new Mat4();
const rotation = new Quat();
const invParentRot = new Quat();
const matrix = new Mat4();
const target = new Vec3();
const up = new Vec3();
function createTest(attr, value) {
if (attr instanceof Function) {
return attr;
}
return (node) => {
let x = node[attr];
if (x instanceof Function) {
x = x();
}
return x === value;
};
}
function findNode(node, test) {
if (test(node)) {
return node;
}
const children = node._children;
const len = children.length;
for (let i = 0; i < len; ++i) {
const result = findNode(children[i], test);
if (result) {
return result;
}
}
return null;
}
class GraphNode extends EventHandler {
name;
tags = new Tags(this);
// Local space properties of transform (only first 3 are settable by the user)
localPosition = new Vec3();
localRotation = new Quat();
localScale = new Vec3(1, 1, 1);
localEulerAngles = new Vec3();
// Only calculated on request
// World space properties of transform
position = new Vec3();
rotation = new Quat();
eulerAngles = new Vec3();
_scale = null;
localTransform = new Mat4();
_dirtyLocal = false;
_aabbVer = 0;
_frozen = false;
worldTransform = new Mat4();
_dirtyWorld = false;
_worldScaleSign = 0;
_normalMatrix = new Mat3();
_dirtyNormal = true;
_right = null;
_up = null;
_forward = null;
_parent = null;
_children = [];
_graphDepth = 0;
_enabled = true;
_enabledInHierarchy = false;
scaleCompensation = false;
constructor(name = "Untitled") {
super();
this.name = name;
}
get right() {
if (!this._right) {
this._right = new Vec3();
}
return this.getWorldTransform().getX(this._right).normalize();
}
get up() {
if (!this._up) {
this._up = new Vec3();
}
return this.getWorldTransform().getY(this._up).normalize();
}
get forward() {
if (!this._forward) {
this._forward = new Vec3();
}
return this.getWorldTransform().getZ(this._forward).normalize().mulScalar(-1);
}
get normalMatrix() {
const normalMat = this._normalMatrix;
if (this._dirtyNormal) {
normalMat.invertMat4(this.getWorldTransform()).transpose();
this._dirtyNormal = false;
}
return normalMat;
}
set enabled(enabled) {
if (this._enabled !== enabled) {
this._enabled = enabled;
if (enabled && this._parent?.enabled || !enabled) {
this._notifyHierarchyStateChanged(this, enabled);
}
}
}
get enabled() {
return this._enabled && this._enabledInHierarchy;
}
get parent() {
return this._parent;
}
get path() {
let node = this._parent;
if (!node) {
return "";
}
let result = this.name;
while (node && node._parent) {
result = `${node.name}/${result}`;
node = node._parent;
}
return result;
}
get root() {
let result = this;
while (result._parent) {
result = result._parent;
}
return result;
}
get children() {
return this._children;
}
get graphDepth() {
return this._graphDepth;
}
_notifyHierarchyStateChanged(node, enabled) {
node._onHierarchyStateChanged(enabled);
const c = node._children;
for (let i = 0, len = c.length; i < len; i++) {
if (c[i]._enabled) {
this._notifyHierarchyStateChanged(c[i], enabled);
}
}
}
_onHierarchyStateChanged(enabled) {
this._enabledInHierarchy = enabled;
if (enabled && !this._frozen) {
this._unfreezeParentToRoot();
}
}
_cloneInternal(clone) {
clone.name = this.name;
const tags = this.tags._list;
clone.tags.clear();
for (let i = 0; i < tags.length; i++) {
clone.tags.add(tags[i]);
}
clone.localPosition.copy(this.localPosition);
clone.localRotation.copy(this.localRotation);
clone.localScale.copy(this.localScale);
clone.localEulerAngles.copy(this.localEulerAngles);
clone.position.copy(this.position);
clone.rotation.copy(this.rotation);
clone.eulerAngles.copy(this.eulerAngles);
clone.localTransform.copy(this.localTransform);
clone._dirtyLocal = this._dirtyLocal;
clone.worldTransform.copy(this.worldTransform);
clone._dirtyWorld = this._dirtyWorld;
clone._dirtyNormal = this._dirtyNormal;
clone._aabbVer = this._aabbVer + 1;
clone._enabled = this._enabled;
clone.scaleCompensation = this.scaleCompensation;
clone._enabledInHierarchy = false;
}
clone() {
const clone = new this.constructor();
this._cloneInternal(clone);
return clone;
}
copy(source) {
source._cloneInternal(this);
return this;
}
destroy() {
this.remove();
const children = this._children;
while (children.length) {
const child = children.pop();
child._parent = null;
child.destroy();
}
this.fire("destroy", this);
this.off();
}
find(attr, value) {
const results = [];
const test = createTest(attr, value);
this.forEach((node) => {
if (test(node)) {
results.push(node);
}
});
return results;
}
findOne(attr, value) {
const test = createTest(attr, value);
return findNode(this, test);
}
findByTag(...query) {
const results = [];
const queryNode = (node, checkNode) => {
if (checkNode && node.tags.has(...query)) {
results.push(node);
}
for (let i = 0; i < node._children.length; i++) {
queryNode(node._children[i], true);
}
};
queryNode(this, false);
return results;
}
findByName(name) {
return this.findOne("name", name);
}
findByPath(path) {
const parts = Array.isArray(path) ? path : path.split("/");
let result = this;
for (let i = 0, imax = parts.length; i < imax; ++i) {
result = result.children.find((c) => c.name === parts[i]);
if (!result) {
return null;
}
}
return result;
}
forEach(callback, thisArg) {
callback.call(thisArg, this);
const children = this._children;
const len = children.length;
for (let i = 0; i < len; ++i) {
children[i].forEach(callback, thisArg);
}
}
isDescendantOf(node) {
let parent = this._parent;
while (parent) {
if (parent === node) {
return true;
}
parent = parent._parent;
}
return false;
}
isAncestorOf(node) {
return node.isDescendantOf(this);
}
getEulerAngles() {
this.getWorldTransform().getEulerAngles(this.eulerAngles);
return this.eulerAngles;
}
getLocalEulerAngles() {
this.localRotation.getEulerAngles(this.localEulerAngles);
return this.localEulerAngles;
}
getLocalPosition() {
return this.localPosition;
}
getLocalRotation() {
return this.localRotation;
}
getLocalScale() {
return this.localScale;
}
getLocalTransform() {
if (this._dirtyLocal) {
this.localTransform.setTRS(this.localPosition, this.localRotation, this.localScale);
this._dirtyLocal = false;
}
return this.localTransform;
}
getPosition() {
this.getWorldTransform().getTranslation(this.position);
return this.position;
}
getRotation() {
this.rotation.setFromMat4(this.getWorldTransform());
return this.rotation;
}
getScale() {
if (!this._scale) {
this._scale = new Vec3();
}
return this.getWorldTransform().getScale(this._scale);
}
getWorldTransform() {
if (!this._dirtyLocal && !this._dirtyWorld) {
return this.worldTransform;
}
if (this._parent) {
this._parent.getWorldTransform();
}
this._sync();
return this.worldTransform;
}
get worldScaleSign() {
if (this._worldScaleSign === 0) {
this._worldScaleSign = this.getWorldTransform().scaleSign;
}
return this._worldScaleSign;
}
remove() {
this._parent?.removeChild(this);
}
reparent(parent, index) {
this.remove();
if (parent) {
if (index >= 0) {
parent.insertChild(this, index);
} else {
parent.addChild(this);
}
}
}
setLocalEulerAngles(x, y, z) {
this.localRotation.setFromEulerAngles(x, y, z);
if (!this._dirtyLocal) {
this._dirtifyLocal();
}
}
setLocalPosition(x, y, z) {
if (x instanceof Vec3) {
this.localPosition.copy(x);
} else {
this.localPosition.set(x, y, z);
}
if (!this._dirtyLocal) {
this._dirtifyLocal();
}
}
setLocalRotation(x, y, z, w) {
if (x instanceof Quat) {
this.localRotation.copy(x);
} else {
this.localRotation.set(x, y, z, w);
}
if (!this._dirtyLocal) {
this._dirtifyLocal();
}
}
setLocalScale(x, y, z) {
if (x instanceof Vec3) {
this.localScale.copy(x);
} else {
this.localScale.set(x, y, z);
}
if (!this._dirtyLocal) {
this._dirtifyLocal();
}
}
_dirtifyLocal() {
if (!this._dirtyLocal) {
this._dirtyLocal = true;
if (!this._dirtyWorld) {
this._dirtifyWorld();
}
}
}
_unfreezeParentToRoot() {
let p = this._parent;
while (p) {
p._frozen = false;
p = p._parent;
}
}
_dirtifyWorld() {
if (!this._dirtyWorld) {
this._unfreezeParentToRoot();
}
this._dirtifyWorldInternal();
}
_dirtifyWorldInternal() {
if (!this._dirtyWorld) {
this._frozen = false;
this._dirtyWorld = true;
for (let i = 0; i < this._children.length; i++) {
if (!this._children[i]._dirtyWorld) {
this._children[i]._dirtifyWorldInternal();
}
}
}
this._dirtyNormal = true;
this._worldScaleSign = 0;
this._aabbVer++;
}
setPosition(x, y, z) {
if (x instanceof Vec3) {
position.copy(x);
} else {
position.set(x, y, z);
}
if (this._parent === null) {
this.localPosition.copy(position);
} else {
invParentWtm.copy(this._parent.getWorldTransform()).invert();
invParentWtm.transformPoint(position, this.localPosition);
}
if (!this._dirtyLocal) {
this._dirtifyLocal();
}
}
setRotation(x, y, z, w) {
if (x instanceof Quat) {
rotation.copy(x);
} else {
rotation.set(x, y, z, w);
}
if (this._parent === null) {
this.localRotation.copy(rotation);
} else {
const parentRot = this._parent.getRotation();
invParentRot.copy(parentRot).invert();
this.localRotation.copy(invParentRot).mul(rotation);
}
if (!this._dirtyLocal) {
this._dirtifyLocal();
}
}
setPositionAndRotation(position2, rotation2) {
if (this._parent === null) {
this.localPosition.copy(position2);
this.localRotation.copy(rotation2);
} else {
const parentWtm = this._parent.getWorldTransform();
invParentWtm.copy(parentWtm).invert();
invParentWtm.transformPoint(position2, this.localPosition);
this.localRotation.setFromMat4(invParentWtm).mul(rotation2);
}
if (!this._dirtyLocal) {
this._dirtifyLocal();
}
}
setEulerAngles(x, y, z) {
this.localRotation.setFromEulerAngles(x, y, z);
if (this._parent !== null) {
const parentRot = this._parent.getRotation();
invParentRot.copy(parentRot).invert();
this.localRotation.mul2(invParentRot, this.localRotation);
}
if (!this._dirtyLocal) {
this._dirtifyLocal();
}
}
addChild(node) {
this._prepareInsertChild(node);
this._children.push(node);
this._onInsertChild(node);
}
addChildAndSaveTransform(node) {
const wPos = node.getPosition();
const wRot = node.getRotation();
this._prepareInsertChild(node);
node.setPosition(tmpMat4.copy(this.worldTransform).invert().transformPoint(wPos));
node.setRotation(tmpQuat.copy(this.getRotation()).invert().mul(wRot));
this._children.push(node);
this._onInsertChild(node);
}
insertChild(node, index) {
this._prepareInsertChild(node);
this._children.splice(index, 0, node);
this._onInsertChild(node);
}
_prepareInsertChild(node) {
node.remove();
}
_fireOnHierarchy(name, nameHierarchy, parent) {
this.fire(name, parent);
for (let i = 0; i < this._children.length; i++) {
this._children[i]._fireOnHierarchy(nameHierarchy, nameHierarchy, parent);
}
}
_onInsertChild(node) {
node._parent = this;
const enabledInHierarchy = node._enabled && this.enabled;
if (node._enabledInHierarchy !== enabledInHierarchy) {
node._enabledInHierarchy = enabledInHierarchy;
node._notifyHierarchyStateChanged(node, enabledInHierarchy);
}
node._updateGraphDepth();
node._dirtifyWorld();
if (this._frozen) {
node._unfreezeParentToRoot();
}
node._fireOnHierarchy("insert", "inserthierarchy", this);
if (this.fire) this.fire("childinsert", node);
}
_updateGraphDepth() {
this._graphDepth = this._parent ? this._parent._graphDepth + 1 : 0;
for (let i = 0, len = this._children.length; i < len; i++) {
this._children[i]._updateGraphDepth();
}
}
removeChild(child) {
const index = this._children.indexOf(child);
if (index === -1) {
return;
}
this._children.splice(index, 1);
child._parent = null;
child._fireOnHierarchy("remove", "removehierarchy", this);
this.fire("childremove", child);
}
_sync() {
if (this._dirtyLocal) {
this.localTransform.setTRS(this.localPosition, this.localRotation, this.localScale);
this._dirtyLocal = false;
}
if (this._dirtyWorld) {
if (this._parent === null) {
this.worldTransform.copy(this.localTransform);
} else {
if (this.scaleCompensation) {
let parentWorldScale;
const parent = this._parent;
let scale = this.localScale;
let parentToUseScaleFrom = parent;
if (parentToUseScaleFrom) {
while (parentToUseScaleFrom && parentToUseScaleFrom.scaleCompensation) {
parentToUseScaleFrom = parentToUseScaleFrom._parent;
}
if (parentToUseScaleFrom) {
parentToUseScaleFrom = parentToUseScaleFrom._parent;
if (parentToUseScaleFrom) {
parentWorldScale = parentToUseScaleFrom.worldTransform.getScale();
scaleCompensateScale.mul2(parentWorldScale, this.localScale);
scale = scaleCompensateScale;
}
}
}
scaleCompensateRot2.setFromMat4(parent.worldTransform);
scaleCompensateRot.mul2(scaleCompensateRot2, this.localRotation);
let tmatrix = parent.worldTransform;
if (parent.scaleCompensation) {
scaleCompensateScaleForParent.mul2(parentWorldScale, parent.getLocalScale());
scaleCompensatePosTransform.setTRS(
parent.worldTransform.getTranslation(scaleCompensatePos),
scaleCompensateRot2,
scaleCompensateScaleForParent
);
tmatrix = scaleCompensatePosTransform;
}
tmatrix.transformPoint(this.localPosition, scaleCompensatePos);
this.worldTransform.setTRS(scaleCompensatePos, scaleCompensateRot, scale);
} else {
this.worldTransform.mulAffine2(this._parent.worldTransform, this.localTransform);
}
}
this._dirtyWorld = false;
}
}
syncHierarchy() {
if (!this._enabled) {
return;
}
if (this._frozen) {
return;
}
this._frozen = true;
if (this._dirtyLocal || this._dirtyWorld) {
this._sync();
}
const children = this._children;
for (let i = 0, len = children.length; i < len; i++) {
children[i].syncHierarchy();
}
}
lookAt(x, y, z, ux = 0, uy = 1, uz = 0) {
if (x instanceof Vec3) {
target.copy(x);
if (y instanceof Vec3) {
up.copy(y);
} else {
up.copy(Vec3.UP);
}
} else if (z === void 0) {
return;
} else {
target.set(x, y, z);
up.set(ux, uy, uz);
}
matrix.setLookAt(this.getPosition(), target, up);
rotation.setFromMat4(matrix);
this.setRotation(rotation);
}
translate(x, y, z) {
if (x instanceof Vec3) {
position.copy(x);
} else {
position.set(x, y, z);
}
position.add(this.getPosition());
this.setPosition(position);
}
translateLocal(x, y, z) {
if (x instanceof Vec3) {
position.copy(x);
} else {
position.set(x, y, z);
}
this.localRotation.transformVector(position, position);
this.localPosition.add(position);
if (!this._dirtyLocal) {
this._dirtifyLocal();
}
}
rotate(x, y, z) {
rotation.setFromEulerAngles(x, y, z);
if (this._parent === null) {
this.localRotation.mul2(rotation, this.localRotation);
} else {
const rot = this.getRotation();
const parentRot = this._parent.getRotation();
invParentRot.copy(parentRot).invert();
rotation.mul2(invParentRot, rotation);
this.localRotation.mul2(rotation, rot);
}
if (!this._dirtyLocal) {
this._dirtifyLocal();
}
}
rotateLocal(x, y, z) {
rotation.setFromEulerAngles(x, y, z);
this.localRotation.mul(rotation);
if (!this._dirtyLocal) {
this._dirtifyLocal();
}
}
}
export {
GraphNode
};