UNPKG

@awayjs/graphics

Version:
652 lines (512 loc) 15.6 kB
import { Box, Matrix3D, Sphere, Vector3D, AssetBase, Rectangle, Matrix } from '@awayjs/core'; import { PickingCollision, PickEntity, _Pick_PickableBase, IPartitionContainer, PickGroup } from '@awayjs/view'; import { IMaterial, RenderableEvent, StyleEvent, Style, ElementsEvent, IRenderContainer, ElementsBase, TriangleElements } from '@awayjs/renderer'; import { IFillStyle } from '../draw/IGraphicsData'; import { ParticleCollection } from '../animators/data/ParticleCollection'; /** * Graphic wraps a Elements as a scene graph instantiation. A Graphic is owned by a Sprite object. * * * @see away.base.ElementsBase * @see away.entities.Sprite * * @class away.base.Graphic */ export class Shape<T extends ElementsBase = ElementsBase> extends AssetBase { private static _pool: Array<Shape> = new Array<Shape>(); public static getShape<T extends ElementsBase>( elements: T, material: IMaterial = null, style: Style = null, count: number = 0, offset: number = 0, ): Shape<T> // eslint-disable-next-line brace-style { if (Shape._pool.length) { const shape: Shape = Shape._pool.pop(); shape.elements = elements; shape.material = material; shape.style = style; shape.count = count; shape.offset = offset; return shape as Shape<T>; } return new Shape<T>(elements, material, style, count, offset); } public static clearPool() { Shape._pool = []; } public static assetType: string = '[asset Shape]'; public static quadElement (rect: Rectangle, slices: number = 1, genUv: boolean = false): TriangleElements { const verts = []; const uvs = []; const w = rect.width / slices; const h = rect.height / slices; const ix = rect.x; const iy = rect.y; for (let i = 0; i < slices; i++) { for (let j = 0; j < slices; j++) { const x = ix + j * w; const y = iy + i * h; const right = x + w; const bottom = y + h; verts.push( x, y , 0, right, bottom, 0, right, y, 0, x, y, 0, x, bottom, 0, right, bottom, 0, ); if (uvs) { uvs.push( j / slices, i / slices, (j + 1) / slices, (i + 1) / slices, (j + 1) / slices, i / slices, j / slices, i / slices, j / slices, (i + 1) / slices, (j + 1) / slices, (i + 1) / slices, ); } } } const elements = new TriangleElements(); elements.setPositions(verts); genUv && elements.setUVs(uvs); return elements; } // legacy public static getElement(rectangle: Rectangle): TriangleElements { const { x, y, right, bottom } = rectangle; const elements = new TriangleElements(); elements.setPositions( [ x, y, 0, right, y, 0, right, bottom, 0, x, y, 0, x, bottom, 0, right, bottom, 0, ]); return elements; } private static _imageShapeElements: Record<string, TriangleElements> = {}; public static getTriangleElement(rectangle: Rectangle, cache = true, uv = false, slices = 1): TriangleElements { const id = rectangle.toString(); let elements = cache ? this._imageShapeElements[id] : null; if (!elements) { elements = Shape.quadElement(rectangle, slices, uv); if (cache) { this._imageShapeElements[id] = elements; // remove it from pool, when forget about shared usage elements.addEventListener(AssetEvent.CLEAR, () => { delete this._imageShapeElements[id]; elements.usages = 0; }); elements.usages++; } } return elements; } private _onInvalidatePropertiesDelegate: (event: StyleEvent) => void; private _onInvalidateVerticesDelegate: (event: ElementsEvent) => void; private _elements: T; private _material: IMaterial; private _style: Style; public usages: number = 0; public count: number = 0; public offset: number = 0; public particleCollection: ParticleCollection = null; public originalFillStyle: IFillStyle = null; /** * Process per-triangle hit test - superslow for huge elements */ public deepHitCheck: boolean = true; /** * The Elements object which provides the geometry data for this Shape. */ public get elements(): T { return this._elements; } public set elements(value: T) { if (this._elements == value) return; if (this._elements) { this._elements.removeEventListener(ElementsEvent.INVALIDATE_VERTICES, this._onInvalidateVerticesDelegate); this._elements.usages--; if (!this._elements.usages) this._elements.dispose(); } this._elements = value; if (this._elements) { this._elements.addEventListener(ElementsEvent.INVALIDATE_VERTICES, this._onInvalidateVerticesDelegate); this._elements.usages++; } this.invalidateElements(); } /** * */ public get assetType(): string { return Shape.assetType; } /** * The material used to render the current Shape. * If set to null, the containing Graphics's material will be used instead. */ public get material(): IMaterial { return this._material; } public set material(value: IMaterial) { if (this._material == value) return; this._material = value; this.invalidateMaterial(); } /** * The style used to render the current Shape. If set to null, its parent Sprite's style will be used instead. */ public get style(): Style { return this._style; } public set style(value: Style) { if (this._style == value) return; if (this._style) this._style.removeEventListener(StyleEvent.INVALIDATE_PROPERTIES, this._onInvalidatePropertiesDelegate); this._style = value; if (this._style) this._style.addEventListener(StyleEvent.INVALIDATE_PROPERTIES, this._onInvalidatePropertiesDelegate); this.invalidateStyle(); } /** * Creates a new Shape object */ constructor( elements: T, material: IMaterial = null, style: Style = null, count: number = 0, offset: number = 0, ) { super(); this._onInvalidatePropertiesDelegate = (event: StyleEvent) => this._onInvalidateProperties(event); this._onInvalidateVerticesDelegate = (event: ElementsEvent) => this._onInvalidateVertices(event); this._elements = elements; this._elements.addEventListener(ElementsEvent.INVALIDATE_VERTICES, this._onInvalidateVerticesDelegate); this._elements.usages++; this._material = material; this._style = style; if (this._style) this._style.addEventListener(StyleEvent.INVALIDATE_PROPERTIES, this._onInvalidatePropertiesDelegate); this.count = count; this.offset = offset; } /** * */ public dispose(): void { super.clear(); this.usages = 0; this.elements = null; this.material = null; this.style = null; this.particleCollection = null; Shape._pool.push(this); } public invalidateElements(): void { this.dispatchEvent(new RenderableEvent(RenderableEvent.INVALIDATE_ELEMENTS, this)); } public invalidateMaterial(): void { this.dispatchEvent(new RenderableEvent(RenderableEvent.INVALIDATE_MATERIAL, this)); } public invalidateStyle(): void { this.dispatchEvent(new RenderableEvent(RenderableEvent.INVALIDATE_STYLE, this)); } private _onInvalidateProperties(event: StyleEvent): void { this.invalidateStyle(); } private _onInvalidateVertices(event: ElementsEvent): void { if (event.attributesView != (<TriangleElements>event.target).positions) return; this.invalidate(); } /** * //TODO * * @param shortestCollisionDistance * @param findClosest * @returns {boolean} * * @internal */ public applyTransformation(transform: Matrix3D): void { this._elements.applyTransformation(transform, this.count, this.offset); } public scale(scale: number): void { this._elements.scale(scale, this.count, this.offset); } public scaleUV(scaleU: number = 1, scaleV: number = 1): void { this._elements.scaleUV(scaleU, scaleV, this.count, this.offset); } } import { AssetEvent } from '@awayjs/core'; import { _Render_RenderableBase, RenderEntity, _Stage_ElementsBase, _Render_MaterialBase, MaterialUtils, LineElements } from '@awayjs/renderer'; import { AnimatorBase } from '../animators/AnimatorBase'; import { BitmapFillStyle } from '../draw/fills/BitmapFillStyle'; /** * @class away.pool._Render_Shape */ export class _Render_Shape extends _Render_RenderableBase { private _scaleX: number; private _scaleY: number; private _scale9Elements: ElementsBase; private _globalBounds: Box = new Box(); public globalBounds (): Box { return this.node.getMatrix3D().transformBox( PickGroup.getInstance().getBoundsPicker(this.node.partition).getBoxBounds(this.node, true, true), this._globalBounds ); } /** * */ public shape: Shape; /** * //TODO * * @param renderEntity * @param shape * @param level * @param indexOffset */ public init(shape: Shape, renderEntity: RenderEntity): void { super.init(shape, renderEntity); this.shape = shape; } public onClear(event: AssetEvent): void { super.onClear(event); this.shape = null; this._scaleX = null; this._scaleY = null; this._scale9Elements = null; } /** * * @returns {ElementsBase} * @protected */ protected _getStageElements(): _Stage_ElementsBase { this._offset = this.shape.offset; this._count = this.shape.count; const _scale9Container: IPartitionContainer = this.node.getScale9Container(); if (_scale9Container) { return this.updateScale9( _scale9Container.scale9Grid, _scale9Container.transform.scale.x, _scale9Container.transform.scale.y) .getAbstraction<_Stage_ElementsBase>(this._stage); } const container = (<IRenderContainer> this.node.container); const elements = container.animator ? (<AnimatorBase> container.animator).getRenderableElements(this, this.shape.elements) : this.shape.elements; return elements.getAbstraction<_Stage_ElementsBase>(this._stage); } protected _getRenderMaterial(): _Render_MaterialBase { const material: IMaterial = (<Shape> this._asset).material || (<IRenderContainer> this.node.container).material || this.getDefaultMaterial(); return material.getAbstraction<_Render_MaterialBase>(this.renderer.getRenderElements(this.shape.elements)); } protected _getStyle(): Style { return (<Shape> this._asset).style || (<IRenderContainer> this.node.container).style; } protected getDefaultMaterial(): IMaterial { return this.stageElements.elements instanceof LineElements ? MaterialUtils.getDefaultColorMaterial() : MaterialUtils.getDefaultTextureMaterial(); } public updateScale9 (scale9Grid: Rectangle, scaleX: number, scaleY: number): ElementsBase { if (!this._scale9Elements) { let uvMatrix: Matrix = null; let generateUV: boolean = false; if (this.shape.originalFillStyle instanceof BitmapFillStyle) uvMatrix = this.shape.originalFillStyle.getUVMatrix(); if (this.shape.elements instanceof TriangleElements) { generateUV = !this.shape.elements.uvs && !!uvMatrix; } // kill UV matrix if we will generate UV if (generateUV) { this.shape.style.uvMatrix = null; } const bounds = PickGroup.getInstance() .getBoundsPicker(this.node.partition) .getBoxBounds(this.node, true, true); this._scale9Elements = this.shape.elements.prepareScale9( <any>bounds, scale9Grid, true, generateUV, uvMatrix); } if (this._scaleX != scaleX || this._scaleY != scaleY) { this._scaleX = scaleX; this._scaleY = scaleY; this._scale9Elements.updateScale9(scaleX, scaleY); } return this._scale9Elements; } } /** * @class away.pool._Render_Shape */ export class _Pick_Shape extends _Pick_PickableBase { private _orientedBoxBounds: Box; private _orientedBoxBoundsDirty: boolean = true; private _orientedSphereBounds: Sphere; private _orientedSphereBoundsDirty = true; private _onInvalidateElementsDelegate: (event: RenderableEvent) => void; constructor() { super(); this._onInvalidateElementsDelegate = (event: RenderableEvent) => this._onInvalidateElements(event); } /** * //TODO * * @param renderEntity * @param shape * @param level * @param indexOffset */ public init(shape: Shape, pickEntity: PickEntity): void { super.init(shape, pickEntity); this._asset.addEventListener(RenderableEvent.INVALIDATE_ELEMENTS, this._onInvalidateElementsDelegate); } public onInvalidate(event: AssetEvent): void { super.onInvalidate(event); this._orientedBoxBoundsDirty = true; this._orientedSphereBoundsDirty = true; } private _onInvalidateElements(_event: RenderableEvent): void { this._orientedBoxBoundsDirty = true; this._orientedSphereBoundsDirty = true; } public onClear(event: AssetEvent): void { this._asset.removeEventListener(RenderableEvent.INVALIDATE_ELEMENTS, this._onInvalidateElementsDelegate); super.onClear(event); this._orientedBoxBounds = null; this._orientedBoxBoundsDirty = true; this._orientedSphereBounds = null; this._orientedSphereBoundsDirty = true; } public hitTestPoint(x: number, y: number, z: number): boolean { const box: Box = this.getBoxBounds(); //early out for box test if (box == null || !box.contains(x, y, z)) return false; return (<Shape> this._asset).elements.hitTestPoint( this._view, this._node, x, y, z, box, (<Shape> this._asset).count, (<Shape> this._asset).offset, ); } public getBoxBounds( matrix3D: Matrix3D = null, strokeFlag: boolean = true, cache: Box = null, target: Box = null, ): Box { if (matrix3D) return (<Shape> this._asset).elements.getBoxBounds( this._view, this._node, strokeFlag, matrix3D, cache, target, (<Shape> this._asset).count, (<Shape> this._asset).offset, ); if (this._orientedBoxBoundsDirty) { this._orientedBoxBoundsDirty = false; this._orientedBoxBounds = (<Shape> this._asset).elements.getBoxBounds( this._view, this._node, strokeFlag, null, this._orientedBoxBounds, null, (<Shape> this._asset).count, (<Shape> this._asset).offset, ); } if (this._orientedBoxBounds != null) target = this._orientedBoxBounds.union(target, target || cache); return target; } public getSphereBounds( center: Vector3D, matrix3D: Matrix3D = null, strokeFlag: boolean = true, cache: Sphere = null, target: Sphere = null, ): Sphere { if (matrix3D) return (<Shape> this._asset).elements.getSphereBounds( this._view, center, matrix3D, strokeFlag, cache, target, (<Shape> this._asset).count, (<Shape> this._asset).offset, ); if (this._orientedSphereBoundsDirty) { this._orientedSphereBoundsDirty = false; this._orientedSphereBounds = (<Shape> this._asset).elements.getSphereBounds( this._view, center, null, strokeFlag, this._orientedSphereBounds, null, (<Shape> this._asset).count, (<Shape> this._asset).offset, ); } if (this._orientedSphereBounds != null) target = this._orientedSphereBounds.union(target, target || cache); return target; } public testCollision(collision: PickingCollision, findClosestCollision: boolean): boolean { const box = this.getBoxBounds(); const shape = <Shape> this._asset; //early out for box test if (box == null || !box.rayIntersection(collision.rayPosition, collision.rayDirection)) return false; if (!shape.deepHitCheck) { return true; } return shape.elements.testCollision( this._view, collision, box, findClosestCollision, (<Shape> this._asset).material || (<IRenderContainer>collision.containerNode.container).material, (<Shape> this._asset).count || (<Shape> this._asset).elements.numVertices, (<Shape> this._asset).offset, ); } } RenderEntity.registerRenderable(_Render_Shape, Shape); PickEntity.registerPickable(_Pick_Shape, Shape);