@awayjs/view
Version:
View for AwayJS
508 lines (400 loc) • 14.7 kB
text/typescript
import {
Vector3D,
Matrix3D,
Box, Sphere,
AbstractionBase,
AssetEvent,
Plane3D,
Point,
WeakAssetSet
} from '@awayjs/core';
import { IPartitionTraverser } from '../partition/IPartitionTraverser';
import { INode } from '../partition/INode';
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';
/**
* 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: Partial<Record<BoundingVolumeType, BoundingVolumePool>>;
private _boundingVolumes: WeakAssetSet;
private _pickGroup: PickGroup;
private _boundsPickers: IBoundsPicker[] = [];
/**
*
* @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()[3];
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()[3];
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: ContainerNode, pool: BoundsPickerPool) {
super.init(node, pool);
this._pickGroup = pool.pickGroup;
this._boundingVolumes = new WeakAssetSet('BoundingVolumeBase');
this._boundingVolumePools = {};
}
public onInvalidate(event: AssetEvent): void {
super.onInvalidate(event);
this._boundingVolumes.forEach((boundingVolume: BoundingVolumeBase) => boundingVolume.onInvalidate(event));
}
public traverse(): void {
this._invalid = false;
this._boundsPickers.length = 0;
(<INode> this._asset).acceptTraverser(this);
}
public getTraverser(node: ContainerNode): IPartitionTraverser {
const traverser: BoundsPicker = this._pickGroup.getBoundsPicker(node);
this._boundsPickers.push(traverser);
return traverser;
}
/**
* 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 true;
}
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 <BoundingVolumeBase> target.getAbstraction(pool);
}
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 addBoundingVolume(boundingVolume: BoundingVolumeBase): void {
this._boundingVolumes.add(boundingVolume);
}
public removeBoundingVolume(boundingVolume: BoundingVolumeBase): void {
this._boundingVolumes.remove(boundingVolume);
}
public hitTestPoint(x: number, y: number, shapeFlag: boolean = false): boolean {
return this._hitTestPointInternal(<INode> this._asset, x, y, shapeFlag, false);
}
public _hitTestPointInternal(
node: INode,
x: number, y: number,
shapeFlag: boolean = false,
maskFlag: boolean = false
): boolean {
if ((<INode> this._asset).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);
(<ContainerNode> this._asset).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 ||
(<INode> this._asset).container.assetType == '[asset TextField]' ||
(<INode> this._asset).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(node, 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 {
//TODO: getBoxBounds should be using the root partition root
//first do a fast box comparision
const objBox: Box = obj.getBoxBounds((<INode> this._asset), true, true);
if (objBox == null)
return false;
const box: Box = this.getBoxBounds((<INode> this._asset), 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((<INode> this._asset), true).intersects(this.getBoxBounds((<INode> this._asset), true));
}
public _getBoxBoundsInternal(
matrix3D: 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 m: Matrix3D = new Matrix3D();
for (let i: number = 0; i < numPickers; ++i) {
if (this._boundsPickers[i].node != (<INode> this._asset)) {
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]._getBoxBoundsInternal(m, strokeFlag, fastFlag, cache, target);
} else {
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 m: Matrix3D = new Matrix3D();
for (let i: number = 0; i < numPickers; ++i) {
if (this._boundsPickers[i].node != (<INode> this._asset)) {
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(node: INode, planes: Array<Plane3D>, numPlanes: number): boolean {
return this.getBoundingVolume(node).isInFrustum(planes, numPlanes);
}
public onClear(event: AssetEvent): void {
super.onClear(event);
this._boundingVolumes.forEach((boundingVolume: BoundingVolumeBase) => boundingVolume.onClear(event));
this._boundingVolumePools = null;
this._boundsPickers.length = 0;
}
/**
*
* @param entity
*/
public applyEntity(node: ContainerNode): void {
if (node.container.getEntity())
this._boundsPickers.push(node.getAbstraction<PickEntity>(this._pickGroup));
else
//check if we have a PickEntity abstraction and if so, clear it!
node.checkAbstraction(this._pickGroup)?.onClear(null);
}
}