@awayjs/view
Version:
View for AwayJS
548 lines (436 loc) • 16 kB
text/typescript
import {
Vector3D,
Matrix3D,
Box, Sphere,
AbstractionBase,
Plane3D,
Point,
} from '@awayjs/core';
import { IPartitionTraverser } from '../partition/IPartitionTraverser';
import { BoundsPickerPool, PickGroup } from '../PickGroup';
import { BoundingVolumePool } from '../bounds/BoundingVolumePool';
import { BoundingVolumeType } from '../bounds/BoundingVolumeType';
import { BoundingVolumeBase } from '../bounds/BoundingVolumeBase';
import { BoundingBox } from '../bounds/BoundingBox';
import { BoundingSphere } from '../bounds/BoundingSphere';
import { IBoundsPicker } from './IBoundsPicker';
import { PickEntity } from '../base/PickEntity';
import { ContainerNode } from '../partition/ContainerNode';
import { INode } from '../partition/INode';
/**
* Picks a 3d object from a view or scene by 3D raycast calculations.
* Performs an initial coarse boundary calculation to return a subset
* of entities whose bounding volumes intersect with the specified ray,
* then triggers an optional picking collider on individual renderable
* objects to further determine the precise values of the picking ray collision.
*
* @class away.pick.RaycastPicker
*/
export class BoundsPicker extends AbstractionBase implements IPartitionTraverser, IBoundsPicker {
private static tmpMatrix: Matrix3D = new Matrix3D();
private static tmpPoint: Point = new Point();
private static tmpBox: Box = new Box();
public static MINIMAL_SCALE = 0.00001;
private _boundingVolumePools: Record<string, BoundingVolumePool>;
private _pickGroup: PickGroup;
private _boundsPickers: IBoundsPicker[] = [];
// private _orientedBoxBounds: Box[] = [];
// private _orientedBoxBoundsDirty: boolean[] = [true, true];
// private _orientedSphereBounds: Sphere[] = [];
// private _orientedSphereBoundsDirty: boolean[] = [true, true];
/**
*
* @returns {ContainerNode}
*/
public get node(): ContainerNode {
return <ContainerNode> this._asset;
}
/**
* Indicates the width of the display object, in pixels. The width is
* calculated based on the bounds of the content of the display object. When
* you set the <code>width</code> property, the <code>scaleX</code> property
* is adjusted accordingly, as shown in the following code:
*
* <p>Except for TextField and Video objects, a display object with no
* content(such as an empty sprite) has a width of 0, even if you try to set
* <code>width</code> to a different value.</p>
*/
public get width(): number {
const box: Box = this.getBoxBounds();
if (box == null)
return 0;
// scale already should be applied, because we request width relative self
return box.width;
}
public set width(val: number) {
const transform = (<INode> this._asset).container.transform;
const selfBox = this.getBoxBounds();
//return if box is empty ie setting width for no content is impossible
if (selfBox == null)
return;
const rotation = transform.rotation;
const baseMatrix = transform.matrix3D;
const box = baseMatrix.transformBox(selfBox, BoundsPicker.tmpBox);
const scaleFactor = box.width > 0 ? val / box.width : 1;
// without rotation, fast case
if (rotation.z === 0) {
const s = transform.scale;
transform.scaleTo(
s.x * scaleFactor || BoundsPicker.MINIMAL_SCALE,
s.y,
s.z
);
return;
}
const matrix = BoundsPicker.tmpMatrix;
matrix.copyFrom(baseMatrix);
matrix.appendScale(
scaleFactor || BoundsPicker.MINIMAL_SCALE,
1,
1
);
// decompose matrix for grabbing transformed scale of transform
// this is target scale that applied (real?) by width
const realScale = matrix.decompose()[2];
transform.scaleTo(
realScale.x,
realScale.y,
realScale.z
);
}
/**
* Indicates the height of the display object, in pixels. The height is
* calculated based on the bounds of the content of the display object. When
* you set the <code>height</code> property, the <code>scaleY</code> property
* is adjusted accordingly, as shown in the following code:
*
* <p>Except for TextField and Video objects, a display object with no
* content (such as an empty sprite) has a height of 0, even if you try to
* set <code>height</code> to a different value.</p>
*/
public get height(): number {
const box: Box = this.getBoxBounds();
if (box == null)
return 0;
// if (this._node._registrationMatrix3D)
// return box.height*this._node.scaleY*this._node._registrationMatrix3D._rawData[5];
// already should be applied
return box.height;// * this._node.container.transform.scale.y;
}
public set height(val: number) {
const transform = (<INode> this._asset).container.transform;
const selfBox = this.getBoxBounds();
//return if box is empty ie setting height for no content is impossible
if (selfBox == null)
return;
const baseMatrix = transform.matrix3D;
const rotation = transform.rotation;
const box = baseMatrix.transformBox(selfBox, BoundsPicker.tmpBox);
const scaleFactor = box.height > 0 ? val / box.height : 1;
// without rotation, fast case
if (rotation.z === 0) {
const s = transform.scale;
transform.scaleTo(
s.x,
s.y * scaleFactor || BoundsPicker.MINIMAL_SCALE,
s.z
);
return;
}
// or we should use decomposition
const matrix = BoundsPicker.tmpMatrix;
matrix.copyFrom(baseMatrix);
matrix.appendScale(
1,
scaleFactor || BoundsPicker.MINIMAL_SCALE,
1
);
const realScale = matrix.decompose()[2];
transform.scaleTo(
realScale.x,
realScale.y,
realScale.z
);
}
/**
* Indicates the depth of the display object, in pixels. The depth is
* calculated based on the bounds of the content of the display object. When
* you set the <code>depth</code> property, the <code>scaleZ</code> property
* is adjusted accordingly, as shown in the following code:
*
* <p>Except for TextField and Video objects, a display object with no
* content (such as an empty sprite) has a depth of 0, even if you try to
* set <code>depth</code> to a different value.</p>
*/
public get depth(): number {
const box: Box = this.getBoxBounds();
if (box == null)
return 0;
// if (this._node._registrationMatrix3D)
// return box.depth*this._node.scaleZ*this._node._registrationMatrix3D._rawData[10];
return box.depth * (<INode> this._asset).container.transform.scale.z;
}
public set depth(val: number) {
const box: Box = this.getBoxBounds();
//return if box is empty ie setting depth for no content is impossible
if (box == null || box.depth == 0)
return;
//this._updateAbsoluteDimension();
const container = (<INode> this._asset).container;
container.transform.scaleTo(
container.transform.scale.x,
container.transform.scale.y,
val / box.depth
);
}
public init(node: INode, pool: BoundsPickerPool) {
super.init(node, pool);
this._pickGroup = pool.pickGroup;
this._boundingVolumePools = {};
}
public onInvalidate(): void {
super.onInvalidate();
// this._orientedBoxBoundsDirty[0] = true;
// this._orientedBoxBoundsDirty[1] = true;
// this._orientedSphereBoundsDirty[0] = true;
// this._orientedSphereBoundsDirty[1] = true;
for (const key in this._boundingVolumePools)
this._boundingVolumePools[key].abstractions.forEach((boundingVolume: BoundingVolumeBase) => boundingVolume.onInvalidate());
}
public traverse(): void {
this._invalid = false;
this._boundsPickers.length = 0;
(<INode> this._asset).acceptTraverser(this);
}
public getTraverser(node: INode): IPartitionTraverser {
// const traverser: BoundsPicker = this._pickGroup.getBoundsPicker(node);
// this._boundsPickers.push(traverser);
return this;
}
/**
* Returns true if the current node is at least partly in the frustum.
* If so, the partition node knows to pass on the traverser to its children.
*
* @param node The Partition3DNode object to frustum-test.
*/
public enterNode(node: INode): boolean {
return !(node.container.assetType == '[asset TextSprite]');
}
public getBoundingVolume(target: INode = null, type: BoundingVolumeType = null): BoundingVolumeBase {
if (target == null)
target = (<INode> this._asset);
if (type == null)
type = (<INode> this._asset).container.defaultBoundingVolume;
const pool: BoundingVolumePool = this._boundingVolumePools[type]
|| (this._boundingVolumePools[type] = new BoundingVolumePool(this, type));
return pool.abstractions.getAbstraction<BoundingVolumeBase>(target);
}
public getBoxBounds(
targetCoordinateSpace: INode = null, strokeFlag: boolean = false, fastFlag: boolean = false): Box {
return (<BoundingBox> this.getBoundingVolume(
targetCoordinateSpace,
strokeFlag
? (fastFlag ? BoundingVolumeType.BOX_BOUNDS_FAST : BoundingVolumeType.BOX_BOUNDS)
: (fastFlag ? BoundingVolumeType.BOX_FAST : BoundingVolumeType.BOX))
).getBox();
}
public getSphereBounds(
targetCoordinateSpace: INode = null, strokeFlag: boolean = false, fastFlag: boolean = false): Sphere {
return (<BoundingSphere> this.getBoundingVolume(
targetCoordinateSpace,
strokeFlag
? (fastFlag ? BoundingVolumeType.SPHERE_BOUNDS_FAST : BoundingVolumeType.SPHERE_BOUNDS)
: (fastFlag ? BoundingVolumeType.SPHERE_FAST : BoundingVolumeType.SPHERE))
).getSphere();
}
public hitTestPoint(x: number, y: number, shapeFlag: boolean = false): boolean {
return this._hitTestPointInternal(<INode> this._asset, x, y, shapeFlag, false);
}
public _hitTestPointInternal(
rootNode: INode,
x: number, y: number,
shapeFlag: boolean = false,
maskFlag: boolean = false
): boolean {
const node: ContainerNode = <ContainerNode> this._asset;
if (node.getMaskId() != -1 && (!maskFlag || !shapeFlag))//allow masks for bounds hit tests
return false;
if (this._invalid)
this.traverse();
//set local tempPoint for later reference
const tempPoint: Point = BoundsPicker.tmpPoint;
tempPoint.setTo(x, y);
node.globalToLocal(tempPoint, tempPoint);
//early out for box test
const box: Box = this.getBoxBounds(null, false, true);
if (box == null || !box.contains(tempPoint.x, tempPoint.y, 0))
return false;
//early out for non-shape tests
if (!shapeFlag ||
node.container.assetType == '[asset TextField]' ||
node.container.assetType == '[asset Billboard]'
)
return true;
const numPickers: number = this._boundsPickers.length;
if (numPickers)
for (let i: number = 0; i < numPickers; ++i)
if (this._boundsPickers[i]._hitTestPointInternal(rootNode, x, y, shapeFlag, maskFlag))
return true;
return false;
}
/**
* Evaluates the bounding box of the display object to see if it overlaps or
* intersects with the bounding box of the <code>obj</code> display object.
*
* @param obj The display object to test against.
* @return <code>true</code> if the bounding boxes of the display objects
* intersect; <code>false</code> if not.
*/
public hitTestObject(obj: BoundsPicker): boolean {
const node: INode = <INode> this._asset;
//TODO: getBoxBounds should be using the root partition root
//first do a fast box comparision
const objBox: Box = obj.getBoxBounds(node, true, true);
if (objBox == null)
return false;
const box: Box = this.getBoxBounds(node, true, true);
if (box == null)
return false;
if (!objBox.intersects(box))
return false;
//if the fast box passes, do the slow test
return obj.getBoxBounds(node, true).intersects(this.getBoxBounds(node, true));
}
public _getBoxBoundsInternal(
invTargetMatrix: Matrix3D = null,
strokeFlag: boolean = true,
fastFlag: boolean = true,
cache: Box = null,
target: Box = null
): Box {
if (this._invalid)
this.traverse();
const numPickers: number = this._boundsPickers.length;
if (numPickers > 0) {
const node: INode = <INode> this._asset;
const m: Matrix3D = new Matrix3D();
let matrix3D;
// if (fastFlag) {
// let obb: Box;
// const strokeIndex: number = strokeFlag ? 1 : 0;
// const invMatrix3D = (<ContainerNode> this._asset).getInverseMatrix3D().clone();
// if (invTargetMatrix) { // a null invTargetMatrix means local coords to the node so matrix3D is identity
// matrix3D = (<ContainerNode> this._asset).getMatrix3D().clone()
// matrix3D.append(invTargetMatrix);
// }
// if (this._orientedBoxBoundsDirty[strokeIndex]) {
// this._orientedBoxBoundsDirty[strokeIndex] = false;
// for (let i: number = 0; i < numPickers; ++i) {
// obb = this._boundsPickers[i]
// ._getBoxBoundsInternal(
// this._boundsPickers[i].node != node
// ? invMatrix3D
// : null,
// strokeFlag,
// fastFlag,
// this._orientedBoxBounds[strokeIndex],
// obb);
// }
// this._orientedBoxBounds[strokeIndex] = obb;
// } else {
// obb = this._orientedBoxBounds[strokeIndex];
// }
// if (obb != null) {
// target = (matrix3D)
// ? matrix3D.transformBox(obb).union(target, target || cache)
// : obb.union(target, target || cache);
// }
// } else {
matrix3D = invTargetMatrix ? invTargetMatrix : (<ContainerNode> this._asset).getInverseMatrix3D();
for (let i: number = 0; i < numPickers; ++i)
target = this._boundsPickers[i]
._getBoxBoundsInternal(
matrix3D,
strokeFlag,
fastFlag,
cache,
target);
// }
}
return target;
}
public _getSphereBoundsInternal(
center: Vector3D = null,
matrix3D: Matrix3D = null,
strokeFlag: boolean = true,
fastFlag: boolean = true,
cache: Sphere = null,
target: Sphere = null
): Sphere {
if (this._invalid)
this.traverse();
const box: Box = this._getBoxBoundsInternal(matrix3D, strokeFlag, fastFlag);
if (box == null)
return;
if (!center) {
center = new Vector3D();
center.x = box.x + box.width / 2;
center.y = box.y + box.height / 2;
center.z = box.z + box.depth / 2;
}
const numPickers: number = this._boundsPickers.length;
if (numPickers > 0) {
const node: INode = <INode> this._asset;
const m: Matrix3D = new Matrix3D();
for (let i: number = 0; i < numPickers; ++i) {
if (this._boundsPickers[i].node != node) {
if (matrix3D)
m.copyFrom(matrix3D);
else
m.identity();
m.prepend(this._boundsPickers[i].node.container.transform.matrix3D);
if (this._boundsPickers[i].node.container._registrationMatrix3D)
m.prepend(this._boundsPickers[i].node.container._registrationMatrix3D);
target = this._boundsPickers[i]._getSphereBoundsInternal(center, m, strokeFlag, fastFlag, cache, target);
} else {
target = this._boundsPickers[i]._getSphereBoundsInternal(center, matrix3D, strokeFlag, fastFlag, cache, target);
}
}
}
return target;
}
/**
*
* @param planes
* @param numPlanes
* @returns {boolean}
*/
public isInFrustum(planes: Array<Plane3D>, numPlanes: number): boolean {
return this._isInFrustumInternal(<INode> this._asset, planes, numPlanes);
}
public _isInFrustumInternal(rootNode: INode, planes: Array<Plane3D>, numPlanes: number): boolean {
return this.getBoundingVolume(rootNode).isInFrustum(planes, numPlanes);
}
public onClear(): void {
super.onClear();
for (const key in this._boundingVolumePools)
this._boundingVolumePools[key].abstractions.forEach((boundingVolume: BoundingVolumeBase) => boundingVolume.onClear());
this._boundingVolumePools = null;
this._boundsPickers.length = 0;
// this._orientedBoxBoundsDirty[0] = true;
// this._orientedBoxBoundsDirty[1] = true;
// this._orientedSphereBoundsDirty[0] = true;
// this._orientedSphereBoundsDirty[1] = true;
}
/**
*
* @param entity
*/
public applyEntity(node: INode): void {
if (node.container.getEntity())
this._boundsPickers.push(this._pickGroup.abstractions.getAbstraction<PickEntity>(node));
else
//check if we have a PickEntity abstraction and if so, clear it!
this._pickGroup.abstractions.checkAbstraction(node)?.onClear();
}
}