UNPKG

@awayjs/renderer

Version:
714 lines (556 loc) 19.4 kB
import { Matrix3D, Vector3D, Box, Sphere, Rectangle } from '@awayjs/core'; import { AttributesView, Short2Attributes } from '@awayjs/stage'; import { HitTestCache } from './HitTestCache'; import { LineElements } from '../elements/LineElements'; const MAX_INT = 268435456 - 1; // 2^28 - 1 const MIN_INT = -268435456; // -2^28 export class LineElementsUtils { //TODO - generate this dyanamically based on num tris public static hitTest(x: number, y: number, z: number, thickness: number, box: Box, lineElements: LineElements, count: number, offset: number = 0): boolean { const positionAttributes: AttributesView = lineElements.positions; const posStride: number = positionAttributes.stride; let positions: ArrayBufferView = positionAttributes.get(count, offset); let indices: Uint16Array; let len: number; if (lineElements.indices) { indices = lineElements.indices.get(count, offset); positions = positionAttributes.get(positionAttributes.count); len = count * lineElements.indices.dimensions; } else { positions = positionAttributes.get(count, offset); len = count; } let id0: number; let id1: number; let ax: number; let ay: number; let bx: number; let by: number; const hitTestCache: HitTestCache = lineElements.hitTestCache[offset] || (lineElements.hitTestCache[offset] = new HitTestCache()); const index: number = hitTestCache.lastCollisionIndex; if (index != -1 && index < len) { precheck: { if (indices) { id0 = indices[index] * posStride; id1 = indices[index + 1] * posStride; } else { id0 = index * posStride; id1 = (index + 1) * posStride; } ax = positions[id0]; ay = positions[id0 + 1]; bx = positions[id1]; by = positions[id1 + 1]; //from a to p const dx: number = ax - x; const dy: number = ay - y; //edge normal (a-b) const nx: number = by - ay; const ny: number = -(bx - ax); const D: number = Math.sqrt(nx * nx + ny * ny); //TODO: should strictly speaking be an elliptical calculation, use circle to approx temp if (Math.abs((dx * nx) + (dy * ny)) > thickness * D) break precheck; //edge vector const dot: number = (dx * ny) - (dy * nx); if (dot > D * D || dot < 0) break precheck; return true; } } //hard coded min vertex count to bother using a grid for if (len > 150) { const cells: Array<Array<number>> = hitTestCache.cells; const divisions: number = cells.length ? hitTestCache.divisions : (hitTestCache.divisions = Math.min(Math.ceil(Math.sqrt(len)), 32)); const conversionX: number = divisions / box.width; const conversionY: number = divisions / box.height; const minx: number = box.x; const miny: number = box.y; if (!cells.length) { //build grid //now we have bounds start creating grid cells and filling cells.length = divisions * divisions; for (let k: number = 0; k < len; k += 3) { if (indices) { id0 = indices[k] * posStride; id1 = indices[k + 1] * posStride; } else { id0 = k * posStride; id1 = (k + 1) * posStride; } ax = positions[id0]; ay = positions[id0 + 1]; bx = positions[id1]; by = positions[id1 + 1]; //subtractions to push into positive space const min_index_x: number = Math.floor((Math.min(ax, bx) - minx) * conversionX); const min_index_y: number = Math.floor((Math.min(ay, by) - miny) * conversionY); const max_index_x: number = Math.floor((Math.max(ax, bx) - minx) * conversionX); const max_index_y: number = Math.floor((Math.max(ay, by) - miny) * conversionY); for (let i: number = min_index_x; i <= max_index_x; i++) { for (let j: number = min_index_y; j <= max_index_y; j++) { const c: number = i + j * divisions; const nodes: Array<number> = cells[c] || (cells[c] = new Array<number>()); //push in the triangle ids nodes.push(k); } } } } const index_x: number = Math.floor((x - minx) * conversionX); const index_y: number = Math.floor((y - miny) * conversionY); const nodes: Array<number> = cells[index_x + index_y * divisions]; if (nodes == null) { hitTestCache.lastCollisionIndex = -1; return false; } const nodeCount: number = nodes.length; for (let n: number = 0; n < nodeCount; n++) { const k: number = nodes[n]; if (indices) { id0 = indices[k] * posStride; id1 = indices[k + 1] * posStride; } else { id0 = k * posStride; id1 = (k + 1) * posStride; } ax = positions[id0]; ay = positions[id0 + 1]; bx = positions[id1]; by = positions[id1 + 1]; //from a to p const dx: number = ax - x; const dy: number = ay - y; //edge normal (a-b) const nx: number = by - ay; const ny: number = -(bx - ax); const D: number = Math.sqrt(nx * nx + ny * ny); //TODO: should strictly speaking be an elliptical calculation, use circle to approx temp if (Math.abs((dx * nx) + (dy * ny)) > thickness * D) continue; //edge vector const dot: number = (dx * ny) - (dy * nx); if (dot > D * D || dot < 0) continue; hitTestCache.lastCollisionIndex = k; return true; } hitTestCache.lastCollisionIndex = -1; return false; } //brute force for (let k: number = 0; k < len; k += 6) { if (indices) { id0 = indices[k] * posStride; id1 = indices[k + 1] * posStride; } else { id0 = k * posStride; id1 = (k + 1) * posStride; } ax = positions[id0]; ay = positions[id0 + 1]; bx = positions[id1]; by = positions[id1 + 1]; //from a to p const dx: number = ax - x; const dy: number = ay - y; //edge normal (a-b) const nx: number = by - ay; const ny: number = -(bx - ax); const D: number = Math.sqrt(nx * nx + ny * ny); //TODO: should strictly speaking be an elliptical calculation, use circle to approx temp if (Math.abs((dx * nx) + (dy * ny)) > thickness * D) continue; //edge vector const dot: number = (dx * ny) - (dy * nx); if (dot > D * D || dot < 0) continue; hitTestCache.lastCollisionIndex = k; return true; } hitTestCache.lastCollisionIndex = -1; return false; } public static getBoxBounds(positionAttributes: AttributesView, indexAttributes: Short2Attributes, matrix3D: Matrix3D, thicknessScale: Vector3D, cache: Box, target: Box, count: number, offset: number = 0): Box { let positions: ArrayBufferView; const posDim: number = positionAttributes.dimensions; const posStride: number = positionAttributes.stride; let minX: number = 0, minY: number = 0, minZ: number = 0; let maxX: number = 0, maxY: number = 0, maxZ: number = 0; let indices: Uint16Array; let len: number; if (indexAttributes) { len = count * indexAttributes.dimensions; indices = indexAttributes.get(count, offset); positions = positionAttributes.get(positionAttributes.count); } else { len = count; positions = positionAttributes.get(count, offset); } if (len == 0) return target; let index: number; let pos1: number, pos2: number, pos3: number, rawData: Float32Array; if (matrix3D) rawData = matrix3D._rawData; for (let i: number = 0; i < len; i += 3) { index = (indices) ? indices[i] * posStride : i * posStride; if (matrix3D) { if (posDim == 6) { pos1 = positions[index] * rawData[0] + positions[index + 1] * rawData[4] + positions[index + 2] * rawData[8] + rawData[12]; pos2 = positions[index] * rawData[1] + positions[index + 1] * rawData[5] + positions[index + 2] * rawData[9] + rawData[13]; pos3 = positions[index] * rawData[2] + positions[index + 1] * rawData[6] + positions[index + 2] * rawData[10] + rawData[14]; } else { pos1 = positions[index] * rawData[0] + positions[index + 1] * rawData[4] + rawData[12]; pos2 = positions[index] * rawData[1] + positions[index + 1] * rawData[5] + rawData[13]; } } else { pos1 = positions[index]; pos2 = positions[index + 1]; pos3 = (posDim == 6) ? positions[index + 2] : 0; } if (i == 0) { maxX = minX = pos1; maxY = minY = pos2; maxZ = minZ = (posDim == 6) ? pos3 : 0; } else { if (pos1 < minX) minX = pos1; else if (pos1 > maxX) maxX = pos1; if (pos2 < minY) minY = pos2; else if (pos2 > maxY) maxY = pos2; if (posDim == 6) { if (pos3 < minZ) minZ = pos3; else if (pos3 > maxZ) maxZ = pos3; } } } const box: Box = new Box(minX, minY); box.right = maxX; box.bottom = maxY; this.mergeThinkness(box, thicknessScale, matrix3D); return box.union(target, target || cache); } public static mergeThinkness(target: Box, thicknessScale: Vector3D, matrix3D: Matrix3D): Box { const rawData = matrix3D?._rawData; const thicknessX = matrix3D ? thicknessScale.x * rawData[0] + thicknessScale.y * rawData[4] : thicknessScale.x; const thicknessY = matrix3D ? thicknessScale.x * rawData[1] + thicknessScale.y * rawData[5] : thicknessScale.y; target.x -= thicknessX; target.y -= thicknessY; target.width += thicknessX * 2; target.height += thicknessY * 2; return target; } public static getSphereBounds(positionAttributes: AttributesView, center: Vector3D, matrix3D: Matrix3D, cache: Sphere, output: Sphere, count: number, offset: number = 0): Sphere { const positions: ArrayBufferView = positionAttributes.get(count, offset); const posDim: number = positionAttributes.dimensions; const posStride: number = positionAttributes.stride; let maxRadiusSquared: number = 0; let radiusSquared: number; const len = count * posStride; let distanceX: number; let distanceY: number; let distanceZ: number; for (let i: number = 0; i < len; i += posStride) { distanceX = positions[i] - center.x; distanceY = positions[i + 1] - center.y; distanceZ = (posDim == 6) ? positions[i + 2] - center.z : -center.z; radiusSquared = distanceX * distanceX + distanceY * distanceY + distanceZ * distanceZ; if (maxRadiusSquared < radiusSquared) maxRadiusSquared = radiusSquared; } if (output == null) output = new Sphere(); output.x = center.x; output.y = center.y; output.z = center.z; output.radius = Math.sqrt(maxRadiusSquared); return output; } public static prepareScale9 ( elem: LineElements, bounds: Rectangle, grid: Rectangle, clone: boolean ): LineElements { const target = clone ? elem.clone() : elem; const shapeBounds = LineElementsUtils.getBoxBounds ( elem.positions, elem.indices, null, new Vector3D(), null, null, elem._numElements || elem._numVertices ); const sliceX = [ MIN_INT, grid.x, grid.right, MAX_INT ]; const sliceY = [ MIN_INT, grid.y, grid.bottom, MAX_INT ]; const chunkX = { from: 0, to: 0 }; const chunkY = { from: 0, to: 0 }; for (let i = 1; i < 3; i++) { if (shapeBounds.x > sliceX[i]) { chunkX.from = chunkX.to = i; } if (shapeBounds.y > sliceY[i]) { chunkY.from = chunkY.to = i; } } for (let i = 0; i < 3; i++) { if (shapeBounds.right >= sliceX[i] && i >= chunkX.from) { chunkX.to = i; } if (shapeBounds.bottom >= sliceY[i] && i >= chunkY.from) { chunkY.to = i; } } target.scale9Grid = grid; target.originalScale9Bounds = bounds; const indices = target.scale9Indices = Array.from({ length: 9 }, (_) => 0); const stack = this.restoreLineSegments (target); // shape already in valid region // not require run slicer for this case if (chunkX.from === chunkX.to && chunkY.from === chunkY.to) { const buff = new Float32Array(stack.length * 3); for (let i = 0; i < stack.length; i++) { buff[i * 3 + 0] = stack[i].x; buff[i * 3 + 1] = stack[i].y; buff[i * 3 + 2] = 0; } target.initialScale9Positions = <any>buff; target.scale9Indices[chunkY.from * 3 + chunkX.from] = target._numElements || target._numVertices; return target; } const posByChunks: number[][] = []; let count = 0; while (stack.length && count < 10000) { const b = stack.pop(); const a = stack.pop(); let cX = 0; let cY = 0; let breakAll = false; for (let y = chunkY.from; y <= chunkY.to; y++) { const day = ~~((sliceY[y + 1] - a.y) * 10000) / 10000; const dby = ~~((sliceY[y + 1] - b.y) * 10000) / 10000; // slicer is crossed, emit point if (day * dby < 0) { const alpha = day / (day - dby); const c = new Vector3D(a.x + alpha * (b.x - a.x), a.y + alpha * (b.y - a.y), 0, 0); // push new segs; stack.push(c, b, a, c); // drop process breakAll = true; break; } if ((a.y + b.y) * 0.5 > sliceY[y] && (a.y + b.y) * 0.5 <= sliceY[y + 1]) { cY = y; } for (let x = chunkX.from; x <= chunkX.to; x++) { const dax = ~~((sliceX[x + 1] - a.x) * 10000) / 10000; const dbx = ~~((sliceX[x + 1] - b.x) * 10000) / 10000; // slicer is crossed, emit point if (dax * dbx < 0) { const alpha = dax / (dax - dbx); const c = new Vector3D(a.x + alpha * (b.x - a.x), a.y + alpha * (b.y - a.y), 0, 0); // push new segs; stack.push(c, b, a, c); // drop process breakAll = true; break; } if ((a.x + b.x) * 0.5 > sliceX[x] && (a.x + b.x) * 0.5 <= sliceX[x + 1]) { cX = x; } } if (breakAll) break; } if (!breakAll) { if (!posByChunks[cX + cY * 3]) { posByChunks[cX + cY * 3] = []; } posByChunks[cX + cY * 3].push(a.x, a.y, 0, b.x, b.y, 0); count += 2; } } const posBuff = new Float32Array(count * 3); const thinBuff = new Float32Array(count); // fill by same thinkness thinBuff.fill(target.thickness.get(1)[0]); let nextIndices = 0; for (let i = 0; i < 9; i++) { if (!posByChunks[i]) { continue; } posBuff.set(posByChunks[i], nextIndices * 3); nextIndices += posByChunks[i].length / 3; indices[i] = nextIndices; } target.initialScale9Positions = posBuff; target.setPositions(posBuff); target.setThickness(thinBuff); target.invalidate(); return target; } private static restoreLineSegments(elem: LineElements): Vector3D[] { const positionAttributes = elem.positions; const indexAttributes = elem.indices; const count = elem._numElements || elem._numVertices; const posDim = positionAttributes.dimensions; const posStride = positionAttributes.stride; let indices: Uint16Array; let positions: Float32Array; let len: number; if (indexAttributes) { len = count * indexAttributes.dimensions; indices = indexAttributes.get(count, 0); positions = <any> positionAttributes.get(positionAttributes.count); } else { len = count; positions = <any> positionAttributes.get(count, 0); } const out: Vector3D[] = []; for (let i = 0; i < len; i += 6) { let index = indices ? indices[i] * posStride : i * posStride; out.push(new Vector3D( positions[index], positions[index + 1], posDim === 6 ? positions[index + 2] : 0, 0, )); index += (posDim == 6) ? 3 : 2; out.push(new Vector3D( positions[index], positions[index + 1], posDim === 6 ? positions[index + 2] : 0, 0, )); } return out; } public static updateScale9( elem: LineElements, originalRect: Rectangle, scaleX: number, scaleY: number, init: boolean = false, copy: boolean = false, ): LineElements { // todo: for now this only works for Float2Attributes. if (elem.scale9Indices.length !== 9) { throw 'ElementUtils: Error - triangleElement does not provide valid slice9Indices!'; } const offsets = elem.scale9Grid; const left = offsets.x - originalRect.x; const right = originalRect.right - offsets.right; const top = offsets.y - originalRect.y; const bottom = originalRect.bottom - offsets.bottom; const s_len = elem.scale9Indices.length; let innerWidth = originalRect.width * scaleX - (left + right); let innerHeight = originalRect.height * scaleY - (top + bottom); let cornerScaleX = 1; let cornerScaleY = 1; // reduce a overflow, when scale to small if (innerWidth < 0) { innerWidth = 0; cornerScaleX = originalRect.width * scaleX / (left + right); } if (innerHeight < 0) { innerHeight = 0; cornerScaleY = originalRect.height * scaleY / (top + bottom); } const innerScaleX = innerWidth / offsets.width; const innerScaleY = innerHeight / offsets.height; const stride = elem.positions.stride; const attrOffset = elem.positions.offset; const dim = elem.positions.dimensions; const positions = elem.positions.get(elem._numVertices); // todo: i had trouble when just cloning the positions // for now i just create the initialSlice9Positions by iterating the positions const initPos = elem.initialScale9Positions; const slice9Indices: number[] = elem.scale9Indices; const slice9Offsets_x = [ 0, left * cornerScaleX - left * innerScaleX, innerWidth - offsets.width * cornerScaleX, ]; const slice9Offsets_y = [ 0, top * cornerScaleY - top * innerScaleY, innerHeight - offsets.height * cornerScaleY, ]; // internal buffer iterator let attrindex = attrOffset; let vindex = 0; // iterating over the 9 chunks - keep in mind that we are constructing a 3x3 grid: for (let s = 0; s < s_len; s++) { const row = s / 3 | 0; const col = s % 3; // only need to x-scale if this is the middle column // if the innerWidth<=0 we can skip this complete column const scalex = col === 1 ? innerScaleX : cornerScaleX; // only need to y-scale if this is the middle row // if the innerHeight<=0 we can skip this complete row const scaley = row === 1 ? innerScaleY : cornerScaleY; // offsetx is different for each column const offsetx = slice9Offsets_x[col]; // offsety is different for each row const offsety = slice9Offsets_y[row]; // iterate the verts and apply the translation / scale // slice9Indices is vertices indeces, is not attribute indices while (vindex < slice9Indices[s]) { // start point const vxs = originalRect.x + (offsetx + (initPos[vindex * 3 + 0] - originalRect.x) * scalex) / scaleX; const vys = originalRect.y + (offsety + (initPos[vindex * 3 + 1] - originalRect.y) * scaley) / scaleY; // end point const vxe = originalRect.x + (offsetx + (initPos[vindex * 3 + 3] - originalRect.x) * scalex) / scaleX; const vye = originalRect.y + (offsety + (initPos[vindex * 3 + 4] - originalRect.y) * scaley) / scaleY; for (let i = 0; i < 2; i++) { let index = attrindex; // super complex, line has a lot of doubled vertices positions[index++] = vxs; positions[index++] = vys; dim === 6 && (positions[index++] = 0); positions[index++] = vxe; positions[index++] = vye; dim === 6 && (positions[index++] = 0); attrindex += stride; index = attrindex; positions[index++] = vxe; positions[index++] = vye; dim === 6 && (positions[index++] = 0); positions[index++] = vxs; positions[index++] = vys; dim === 6 && (positions[index++] = 0); attrindex += stride; } // we should include a stride, because buffer maybe be contecated // or XYZ instead of XY vindex += 2; } } elem.positions.invalidate(); elem.invalidate(); return elem; } }