[
  {
    "path": ".github/workflows/node.yml",
    "content": "name: Node\non: [push, pull_request]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n    - name: Checkout\n      uses: actions/checkout@v4\n\n    - name: Setup Node\n      uses: actions/setup-node@v4\n      with:\n        node-version: 20\n\n    - name: Install dependencies\n      run: npm ci\n\n    - name: Run tests\n      run: npm test\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules\nyarn.lock\n*.log\nmartini.js\nmartini.min.js\n"
  },
  {
    "path": "LICENSE",
    "content": "ISC License\n\nCopyright (c) 2019, Mapbox\n\nPermission to use, copy, modify, and/or distribute this software for any purpose\nwith or without fee is hereby granted, provided that the above copyright notice\nand this permission notice appear in all copies.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND\nFITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS\nOF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER\nTORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF\nTHIS SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# MARTINI\n\n[![Simply Awesome](https://img.shields.io/badge/simply-awesome-brightgreen.svg)](https://github.com/mourner/projects)\n\nMARTINI stands for **Mapbox's Awesome Right-Triangulated Irregular Networks, Improved**.\n\nIt's an experimental JavaScript library for **real-time terrain mesh generation** from height data. Given a (2<sup>k</sup>+1) × (2<sup>k</sup>+1) terrain grid, it generates a hierarchy of triangular meshes of varying level of detail in milliseconds. _A work in progress._\n\nSee the algorithm in action and read more about how it works in [this interactive Observable notebook](https://observablehq.com/@mourner/martin-real-time-rtin-terrain-mesh).\n\nBased on the paper [\"Right-Triangulated Irregular Networks\" by Will Evans et. al. (1997)](https://www.cs.ubc.ca/~will/papers/rtin.pdf).\n\n![MARTINI terrain demo](martini.gif)\n\n## Example\n\n```js\nimport Martini from '@mapbox/martini';\n\n// set up mesh generator for a certain 2^k+1 grid size\nconst martini = new Martini(257);\n\n// generate RTIN hierarchy from terrain data (an array of size^2 length)\nconst tile = martini.createTile(terrain);\n\n// get a mesh (vertices and triangles indices) for a 10m error\nconst mesh = tile.getMesh(10);\n```\n\n## Install\n\n```bash\nnpm install @mapbox/martini\n```\n\n### Ports to other languages\n\n- [pymartini](https://github.com/kylebarron/pymartini) (Python)\n"
  },
  {
    "path": "bench.js",
    "content": "\nimport fs from 'fs';\nimport {PNG} from 'pngjs';\nimport Martini from './index.js';\nimport {mapboxTerrainToGrid} from './test/util.js';\n\nconst png = PNG.sync.read(fs.readFileSync('./test/fixtures/fuji.png'));\n\nconst terrain = mapboxTerrainToGrid(png);\n\nconsole.time('init tileset');\nconst martini = new Martini(png.width + 1);\nconsole.timeEnd('init tileset');\n\nconsole.time('create tile');\nconst tile = martini.createTile(terrain);\nconsole.timeEnd('create tile');\n\nconsole.time('mesh');\nconst mesh = tile.getMesh(30);\nconsole.timeEnd('mesh');\n\nconsole.log(`vertices: ${mesh.vertices.length / 2}, triangles: ${mesh.triangles.length / 3}`);\n\nconsole.time('20 meshes total');\nfor (let i = 0; i <= 20; i++) {\n    console.time(`mesh ${i}`);\n    tile.getMesh(i);\n    console.timeEnd(`mesh ${i}`);\n}\nconsole.timeEnd('20 meshes total');\n"
  },
  {
    "path": "eslint.config.js",
    "content": "import config from \"eslint-config-mourner\";\n\nexport default [\n    ...config,\n    {\n        rules: {\n            'no-use-before-define': 0\n        }\n    }\n];\n"
  },
  {
    "path": "index.d.ts",
    "content": "export default class Martini {\n  constructor(gridSize?: number);\n  createTile(terrain: ArrayLike<number>): Tile;\n}\n\nexport class Tile {\n  constructor(terrain: ArrayLike<number>, martini: Martini);\n  update(): void;\n  getMesh(maxError?: number): {\n    vertices: Uint16Array;\n    triangles: Uint32Array;\n  };\n}\n"
  },
  {
    "path": "index.js",
    "content": "\nexport default class Martini {\n    constructor(gridSize = 257) {\n        this.gridSize = gridSize;\n        const tileSize = gridSize - 1;\n        if (tileSize & (tileSize - 1)) throw new Error(\n            `Expected grid size to be 2^n+1, got ${gridSize}.`);\n\n        this.numTriangles = tileSize * tileSize * 2 - 2;\n        this.numParentTriangles = this.numTriangles - tileSize * tileSize;\n\n        this.indices = new Uint32Array(this.gridSize * this.gridSize);\n\n        // coordinates for all possible triangles in an RTIN tile\n        this.coords = new Uint16Array(this.numTriangles * 4);\n\n        // get triangle coordinates from its index in an implicit binary tree\n        for (let i = 0; i < this.numTriangles; i++) {\n            let id = i + 2;\n            let ax = 0, ay = 0, bx = 0, by = 0, cx = 0, cy = 0;\n            if (id & 1) {\n                bx = by = cx = tileSize; // bottom-left triangle\n            } else {\n                ax = ay = cy = tileSize; // top-right triangle\n            }\n            while ((id >>= 1) > 1) {\n                const mx = (ax + bx) >> 1;\n                const my = (ay + by) >> 1;\n\n                if (id & 1) { // left half\n                    bx = ax; by = ay;\n                    ax = cx; ay = cy;\n                } else { // right half\n                    ax = bx; ay = by;\n                    bx = cx; by = cy;\n                }\n                cx = mx; cy = my;\n            }\n            const k = i * 4;\n            this.coords[k + 0] = ax;\n            this.coords[k + 1] = ay;\n            this.coords[k + 2] = bx;\n            this.coords[k + 3] = by;\n        }\n    }\n\n    createTile(terrain) {\n        return new Tile(terrain, this);\n    }\n}\n\nclass Tile {\n    constructor(terrain, martini) {\n        const size = martini.gridSize;\n        if (terrain.length !== size * size) throw new Error(\n            `Expected terrain data of length ${size * size} (${size} x ${size}), got ${terrain.length}.`);\n\n        this.terrain = terrain;\n        this.martini = martini;\n        this.errors = new Float32Array(terrain.length);\n        this.update();\n    }\n\n    update() {\n        const {numTriangles, numParentTriangles, coords, gridSize: size} = this.martini;\n        const {terrain, errors} = this;\n\n        // iterate over all possible triangles, starting from the smallest level\n        for (let i = numTriangles - 1; i >= 0; i--) {\n            const k = i * 4;\n            const ax = coords[k + 0];\n            const ay = coords[k + 1];\n            const bx = coords[k + 2];\n            const by = coords[k + 3];\n            const mx = (ax + bx) >> 1;\n            const my = (ay + by) >> 1;\n            const cx = mx + my - ay;\n            const cy = my + ax - mx;\n\n            // calculate error in the middle of the long edge of the triangle\n            const interpolatedHeight = (terrain[ay * size + ax] + terrain[by * size + bx]) / 2;\n            const middleIndex = my * size + mx;\n            const middleError = Math.abs(interpolatedHeight - terrain[middleIndex]);\n\n            errors[middleIndex] = Math.max(errors[middleIndex], middleError);\n\n            if (i < numParentTriangles) { // bigger triangles; accumulate error with children\n                const leftChildIndex = ((ay + cy) >> 1) * size + ((ax + cx) >> 1);\n                const rightChildIndex = ((by + cy) >> 1) * size + ((bx + cx) >> 1);\n                errors[middleIndex] = Math.max(errors[middleIndex], errors[leftChildIndex], errors[rightChildIndex]);\n            }\n        }\n    }\n\n    getMesh(maxError = 0) {\n        const {gridSize: size, indices} = this.martini;\n        const {errors} = this;\n        let numVertices = 0;\n        let numTriangles = 0;\n        const max = size - 1;\n\n        // use an index grid to keep track of vertices that were already used to avoid duplication\n        indices.fill(0);\n\n        // retrieve mesh in two stages that both traverse the error map:\n        // - countElements: find used vertices (and assign each an index), and count triangles (for minimum allocation)\n        // - processTriangle: fill the allocated vertices & triangles typed arrays\n\n        function countElements(ax, ay, bx, by, cx, cy) {\n            const mx = (ax + bx) >> 1;\n            const my = (ay + by) >> 1;\n\n            if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) {\n                countElements(cx, cy, ax, ay, mx, my);\n                countElements(bx, by, cx, cy, mx, my);\n            } else {\n                indices[ay * size + ax] = indices[ay * size + ax] || ++numVertices;\n                indices[by * size + bx] = indices[by * size + bx] || ++numVertices;\n                indices[cy * size + cx] = indices[cy * size + cx] || ++numVertices;\n                numTriangles++;\n            }\n        }\n        countElements(0, 0, max, max, max, 0);\n        countElements(max, max, 0, 0, 0, max);\n\n        const vertices = new Uint16Array(numVertices * 2);\n        const triangles = new Uint32Array(numTriangles * 3);\n        let triIndex = 0;\n\n        function processTriangle(ax, ay, bx, by, cx, cy) {\n            const mx = (ax + bx) >> 1;\n            const my = (ay + by) >> 1;\n\n            if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) {\n                // triangle doesn't approximate the surface well enough; drill down further\n                processTriangle(cx, cy, ax, ay, mx, my);\n                processTriangle(bx, by, cx, cy, mx, my);\n\n            } else {\n                // add a triangle\n                const a = indices[ay * size + ax] - 1;\n                const b = indices[by * size + bx] - 1;\n                const c = indices[cy * size + cx] - 1;\n\n                vertices[2 * a] = ax;\n                vertices[2 * a + 1] = ay;\n\n                vertices[2 * b] = bx;\n                vertices[2 * b + 1] = by;\n\n                vertices[2 * c] = cx;\n                vertices[2 * c + 1] = cy;\n\n                triangles[triIndex++] = a;\n                triangles[triIndex++] = b;\n                triangles[triIndex++] = c;\n            }\n        }\n        processTriangle(0, 0, max, max, max, 0);\n        processTriangle(max, max, 0, 0, 0, max);\n\n        return {vertices, triangles};\n    }\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"@mapbox/martini\",\n  \"version\": \"0.2.0\",\n  \"type\": \"module\",\n  \"description\": \"A JavaScript library for real-time terrain mesh generation\",\n  \"main\": \"index.js\",\n  \"exports\": \"./index.js\",\n  \"types\": \"index.d.ts\",\n  \"scripts\": {\n    \"pretest\": \"eslint index.js bench.js test\",\n    \"test\": \"node test/test.js\",\n    \"bench\": \"node bench.js\",\n    \"prepublishOnly\": \"npm run test\"\n  },\n  \"keywords\": [\n    \"terrain\",\n    \"rtin\",\n    \"mesh\",\n    \"3d\",\n    \"webgl\"\n  ],\n  \"author\": \"Vladimir Agafonkin\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"eslint\": \"^9.31.0\",\n    \"eslint-config-mourner\": \"^4.0.2\",\n    \"pngjs\": \"^7.0.0\"\n  },\n  \"files\": [\n    \"index.js\",\n    \"index.d.ts\"\n  ],\n  \"eslintConfig\": {\n    \"extends\": \"mourner\",\n    \"rules\": {\n      \"no-use-before-define\": 0\n    }\n  }\n}\n"
  },
  {
    "path": "test/test.js",
    "content": "\nimport fs from 'fs';\nimport {PNG} from 'pngjs';\nimport test from 'node:test';\nimport assert from 'node:assert/strict';\nimport Martini from '../index.js';\nimport {mapboxTerrainToGrid} from './util.js';\n\nconst fuji = PNG.sync.read(fs.readFileSync('./test/fixtures/fuji.png'));\nconst terrain = mapboxTerrainToGrid(fuji);\n\ntest('generates a mesh', () => {\n    const martini = new Martini(fuji.width + 1);\n    const tile = martini.createTile(terrain);\n    const mesh = tile.getMesh(500);\n\n    assert.deepEqual([\n        320, 64, 256, 128, 320, 128, 384, 128, 256, 0, 288, 160, 256, 192, 288, 192, 320, 192, 304, 176, 256, 256, 288,\n        224, 352, 160, 320, 160, 512, 0, 384, 0, 128, 128, 128, 0, 64, 64, 64, 0, 0, 0, 32, 32, 192, 192, 384, 384, 512,\n        256, 384, 256, 320, 320, 320, 256, 512, 512, 512, 128, 448, 192, 384, 192, 128, 384, 256, 512, 256, 384, 0,\n        512, 128, 256, 64, 192, 0, 256, 64, 128, 32, 96, 0, 128, 32, 64, 16, 48, 0, 64, 0, 32\n    ], Array.from(mesh.vertices), 'correct vertices');\n\n    assert.deepEqual([\n        0, 1, 2, 3, 0, 2, 4, 1, 0, 5, 6, 7, 7, 8, 9, 5, 7, 9, 1, 6, 5, 6, 10, 11, 11, 8, 7, 6, 11, 7, 12, 2, 13, 8, 12,\n        13, 3, 2, 12, 2, 1, 5, 13, 5, 9, 8, 13, 9, 2, 5, 13, 3, 14, 15, 15, 4, 0, 3, 15, 0, 16, 4, 17, 18, 17, 19, 19,\n        20, 21, 18, 19, 21, 16, 17, 18, 1, 16, 22, 22, 10, 6, 1, 22, 6, 4, 16, 1, 23, 24, 25, 26, 25, 27, 10, 26, 27,\n        23, 25, 26, 28, 24, 23, 29, 3, 30, 24, 29, 30, 14, 3, 29, 8, 25, 31, 31, 3, 12, 8, 31, 12, 27, 8, 11, 10, 27,\n        11, 25, 8, 27, 25, 24, 30, 30, 3, 31, 25, 30, 31, 32, 33, 34, 10, 32, 34, 35, 33, 32, 33, 28, 23, 34, 23, 26,\n        10, 34, 26, 33, 23, 34, 36, 16, 37, 38, 36, 37, 36, 10, 22, 16, 36, 22, 39, 18, 40, 41, 39, 40, 16, 18, 39, 42,\n        21, 43, 44, 42, 43, 18, 21, 42, 21, 20, 45, 45, 44, 43, 21, 45, 43, 44, 41, 40, 40, 18, 42, 44, 40, 42, 41, 38,\n        37, 37, 16, 39, 41, 37, 39, 38, 35, 32, 32, 10, 36, 38, 32, 36\n    ], Array.from(mesh.triangles), 'correct triangles');\n});\n"
  },
  {
    "path": "test/util.js",
    "content": "\nexport function mapboxTerrainToGrid(png) {\n    const gridSize = png.width + 1;\n    const terrain = new Float32Array(gridSize * gridSize);\n\n    const tileSize = png.width;\n\n    // decode terrain values\n    for (let y = 0; y < tileSize; y++) {\n        for (let x = 0; x < tileSize; x++) {\n            const k = (y * tileSize + x) * 4;\n            const r = png.data[k + 0];\n            const g = png.data[k + 1];\n            const b = png.data[k + 2];\n            terrain[y * gridSize + x] = (r * 256 * 256 + g * 256.0 + b) / 10.0 - 10000.0;\n        }\n    }\n    // backfill right and bottom borders\n    for (let x = 0; x < gridSize - 1; x++) {\n        terrain[gridSize * (gridSize - 1) + x] = terrain[gridSize * (gridSize - 2) + x];\n    }\n    for (let y = 0; y < gridSize; y++) {\n        terrain[gridSize * y + gridSize - 1] = terrain[gridSize * y + gridSize - 2];\n    }\n\n    return terrain;\n}\n"
  }
]