segment/segmentHelper.tstypescript
/**
 * @module og/segment/SegmentHelper
 */

import { N, W, S, E } from "../quadTree/quadTree";

type IndexTypeArray = Uint32Array;
type IndexesTable = [IndexTypeArray[][], IndexTypeArray[][], IndexTypeArray[][], IndexTypeArray[][]];

function NewIndexesTypedArray(arr: number[]): IndexTypeArray {
    return new Uint32Array(arr);
}

function NewIndexesTypedArrayEmpty(size: number): IndexTypeArray {
    return new Uint32Array(size);
}

function createCenterBodyIndexes(size: number): IndexTypeArray {
    let indexes = [];

    let i0 = 1,
        j0 = 1;

    let i1 = 1,
        j1 = 1;

    let ind1 = 0,
        ind2 = 0,
        nr = 0;
    for (let i = i0; i < size - 1 - i1; i++) {
        for (let j = j0; j < size - j1; j++) {
            ind1 = i * size + j;
            nr = (i + 1) * size;
            ind2 = nr + j;
            indexes.push(ind1, ind2);
        }
        indexes.push(ind2, nr + j0);
    }
    indexes.push(indexes[indexes.length - 1], size * size - size);

    return NewIndexesTypedArray(indexes);
}

function createWestNeighborSkirt(size: number, deltaGr: number): IndexTypeArray {
    let indexes = [];
    const grCount = (size - 1) / deltaGr;
    const b = size * size - size;
    let k = 0;
    for (let i = 0; i < size - 2; i++) {
        if (i % grCount === 0) {
            k = i;
        }
        let rind = b - size * i - size + 1,
            lind = b - size * k;
        indexes.push(lind, rind);
    }

    if (deltaGr === size - 1) {
        indexes.push(size);
        indexes.push(0);
    }

    return NewIndexesTypedArray(indexes);
}

function createNorthNeighborSkirt(size: number, deltaGr: number): IndexTypeArray {
    let indexes = [];
    const grCount = (size - 1) / deltaGr;
    let k = 0;
    for (let i = 0; i < size - 2; i++) {
        if (i % grCount === 0) {
            k = i;
        }
        let rind = size + i + 1,
            lind = k;
        indexes.push(lind, rind);
    }

    if (deltaGr === size - 1) {
        indexes.push(size - 2);
        indexes.push(size - 1);
    }

    return NewIndexesTypedArray(indexes);
}

function createEastNeighborSkirt(size: number, deltaGr: number): IndexTypeArray {
    let indexes = [];
    const grCount = (size - 1) / deltaGr;
    let k = 0;
    for (let i = 0; i < size - 2; i++) {
        if (i % grCount === 0) {
            k = i;
        }
        let rind = size * (i + 1) + size - 2,
            lind = size + size * k - 1;
        indexes.push(lind, rind);
    }

    if (deltaGr === size - 1) {
        indexes.push(size * (size - 1) - 1);
        indexes.push(size * size - 1);
    }

    return NewIndexesTypedArray(indexes);
}

function createSouthNeighborSkirt(size: number, deltaGr: number): IndexTypeArray {
    let indexes = [];
    const grCount = (size - 1) / deltaGr;
    let k = 0;
    const rb = size * (size - 1) - 2;
    const lb = size * size - 1;
    for (let i = 0; i < size - 2; i++) {
        if (i % grCount === 0) {
            k = i;
        }
        let rind = rb - i,
            lind = lb - k;
        indexes.push(lind, rind);
    }

    if (deltaGr === size - 1) {
        indexes.push(size * size - size + 1);
    }
    indexes.push(size * size - size);

    return NewIndexesTypedArray(indexes);
}

function initIndexesBodySkirts(pow: number): IndexesTable {
    let table: IndexesTable = [[], [], [], []];

    for (let i = 0; i <= pow; i++) {
        let d = Math.pow(2, i),
            d1 = d + 1;

        table[N][i] = [];
        table[W][i] = [];
        table[S][i] = [];
        table[E][i] = [];

        for (let j = 0; j <= pow; j++) {
            let dd = Math.pow(2, j);
            table[W][i][j] = createWestNeighborSkirt(d1, dd);
            table[N][i][j] = createNorthNeighborSkirt(d1, dd);
            table[E][i][j] = createEastNeighborSkirt(d1, dd);
            table[S][i][j] = createSouthNeighborSkirt(d1, dd);
        }
    }
    return table;
}

function initIndexBodiesTable(pow: number): IndexTypeArray[] {
    let table = [];
    for (let i = 0; i <= pow; i++) {
        const d = Math.pow(2, i);
        table[i] = createCenterBodyIndexes(d + 1);
    }
    return table;
}

function createTextureCoords(size: number): Uint16Array {
    let texCoords = new Uint16Array((size + 1) * (size + 1) * 2);
    let k = 0;
    for (let i = 0; i <= size; i++) {
        for (let j = 0; j <= size; j++) {
            texCoords[k++] = (j / size) * 0xffff;
            texCoords[k++] = (i / size) * 0xffff;
        }
    }
    return texCoords;
}

/**
 * Triangles of a grid, listed per cell. The cell in a row and column (from the north-west corner) is
 * row * size + column, and its triangles are vertices[3 * start[cell]] up to vertices[3 * start[cell + 1]],
 * three vertex indexes each.
 */
export interface CellTriangles {
    start: Uint32Array;
    vertices: Uint32Array;
}

/**
 * Lists the triangles of a triangle strip over a grid of (size + 1) x (size + 1) vertices per cell they cover.
 * A triangle is listed in every cell of its bounding box.
 * @param strip - Vertex indexes of the triangle strip.
 * @param size - Grid size, cells per side.
 */
export function createCellTriangles(strip: ArrayLike<number>, size: number): CellTriangles {
    const d = size + 1,
        start = new Uint32Array(size * size + 1);
    let vertices = new Uint32Array(0);

    // First count the triangles of every cell, then write them.
    for (let pass = 0; pass < 2; pass++) {
        const next = pass === 0 ? start : start.slice();
        for (let i = 0; i + 2 < strip.length; i++) {
            const a = strip[i],
                b = strip[i + 1],
                c = strip[i + 2];
            // The steps that join the parts of the strip repeat a vertex and draw nothing
            if (a === b || b === c || a === c) {
                continue;
            }
            const ar = (a / d) | 0,
                br = (b / d) | 0,
                cr = (c / d) | 0,
                ac = a - ar * d,
                bc = b - br * d,
                cc = c - cr * d,
                col0 = Math.min(ac, bc, cc),
                col1 = Math.max(ac, bc, cc),
                row1 = Math.max(ar, br, cr);
            for (let row = Math.min(ar, br, cr); row < row1; row++) {
                for (let col = col0; col < col1; col++) {
                    const cell = row * size + col;
                    if (pass === 0) {
                        start[cell + 1]++;
                    } else {
                        const k = next[cell]++ * 3;
                        vertices[k] = a;
                        vertices[k + 1] = b;
                        vertices[k + 2] = c;
                    }
                }
            }
        }
        if (pass === 0) {
            for (let cell = 0; cell < size * size; cell++) {
                start[cell + 1] += start[cell];
            }
            vertices = new Uint32Array(start[size * size] * 3);
        }
    }

    return { start, vertices };
}

/**
 * One slot per grid size and neighbour sizes, filled on first use by getCellTriangles.
 */
function createCellTrianglesTable(pow: number): (CellTriangles | undefined)[] {
    return new Array((pow + 1) ** 5).fill(undefined);
}

class SegmentHelper {
    protected _maxGridSize: number;
    public centerIndexesTable: IndexTypeArray[];
    public skirtsIndexesTable: IndexesTable;
    protected _cellTriangles: (CellTriangles | undefined)[];

    constructor(maxGridSize: number = 0) {
        this._maxGridSize = maxGridSize;
        this.centerIndexesTable = initIndexBodiesTable(this._maxGridSize);
        this.skirtsIndexesTable = initIndexesBodySkirts(this._maxGridSize);
        this._cellTriangles = createCellTrianglesTable(this._maxGridSize);
    }

    public get maxGridSize(): number {
        return this._maxGridSize;
    }

    public init() {
        this.centerIndexesTable = initIndexBodiesTable(this._maxGridSize);
        this.skirtsIndexesTable = initIndexesBodySkirts(this._maxGridSize);
        this._cellTriangles = createCellTrianglesTable(this._maxGridSize);
    }

    public setMaxGridSize(gridSize: number) {
        this._maxGridSize = gridSize;
        this.init();
    }

    public createSegmentIndexes(size: number, sidesSizes: [number, number, number, number]) {
        if (size) {
            let c = this.centerIndexesTable[size],
                w = this.skirtsIndexesTable[W][size][sidesSizes[W]],
                n = this.skirtsIndexesTable[N][size][sidesSizes[N]],
                e = this.skirtsIndexesTable[E][size][sidesSizes[E]],
                s = this.skirtsIndexesTable[S][size][sidesSizes[S]];

            let indexes = NewIndexesTypedArrayEmpty(c.length + w.length + n.length + e.length + s.length);

            indexes.set(c, 0);
            indexes.set(w, c.length);
            indexes.set(n, c.length + w.length);
            indexes.set(e, c.length + w.length + n.length);
            indexes.set(s, c.length + w.length + n.length + e.length);

            return indexes;
        } else {
            return NewIndexesTypedArray([0, 2, 1, 3]);
        }
    }

    /**
     * The triangles drawn in every cell of a segment grid, built from the indexes createSegmentIndexes gives the GPU.
     * @param size - Grid size, log2.
     * @param sidesSizes - Neighbour grid sizes, log2.
     */
    public getCellTriangles(size: number, sidesSizes: [number, number, number, number]): CellTriangles {
        const t = this._maxGridSize + 1,
            key = (((size * t + sidesSizes[0]) * t + sidesSizes[1]) * t + sidesSizes[2]) * t + sidesSizes[3];
        let cells = this._cellTriangles[key];
        if (!cells) {
            cells = this._cellTriangles[key] = createCellTriangles(
                this.createSegmentIndexes(size, sidesSizes),
                1 << size
            );
        }
        return cells;
    }

    public initTextureCoordsTable(pow: number): Uint16Array[] {
        let table = [];
        for (let i = 0; i <= pow; i++) {
            const d = Math.pow(2, i);
            table[i] = createTextureCoords(d);
        }
        return table;
    }
}

let instance = new SegmentHelper();

export function getInstance() {
    return instance;
}