Full Code of mapbox/martini for AI

main f9a7a96b583a cached
11 files
13.4 KB
4.5k tokens
10 symbols
1 requests
Download .txt
Repository: mapbox/martini
Branch: main
Commit: f9a7a96b583a
Files: 11
Total size: 13.4 KB

Directory structure:
gitextract_e46fi7nx/

├── .github/
│   └── workflows/
│       └── node.yml
├── .gitignore
├── LICENSE
├── README.md
├── bench.js
├── eslint.config.js
├── index.d.ts
├── index.js
├── package.json
└── test/
    ├── test.js
    └── util.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/node.yml
================================================
name: Node
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Setup Node
      uses: actions/setup-node@v4
      with:
        node-version: 20

    - name: Install dependencies
      run: npm ci

    - name: Run tests
      run: npm test


================================================
FILE: .gitignore
================================================
node_modules
yarn.lock
*.log
martini.js
martini.min.js


================================================
FILE: LICENSE
================================================
ISC License

Copyright (c) 2019, Mapbox

Permission to use, copy, modify, and/or distribute this software for any purpose
with or without fee is hereby granted, provided that the above copyright notice
and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
THIS SOFTWARE.


================================================
FILE: README.md
================================================
# MARTINI

[![Simply Awesome](https://img.shields.io/badge/simply-awesome-brightgreen.svg)](https://github.com/mourner/projects)

MARTINI stands for **Mapbox's Awesome Right-Triangulated Irregular Networks, Improved**.

It'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._

See 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).

Based on the paper ["Right-Triangulated Irregular Networks" by Will Evans et. al. (1997)](https://www.cs.ubc.ca/~will/papers/rtin.pdf).

![MARTINI terrain demo](martini.gif)

## Example

```js
import Martini from '@mapbox/martini';

// set up mesh generator for a certain 2^k+1 grid size
const martini = new Martini(257);

// generate RTIN hierarchy from terrain data (an array of size^2 length)
const tile = martini.createTile(terrain);

// get a mesh (vertices and triangles indices) for a 10m error
const mesh = tile.getMesh(10);
```

## Install

```bash
npm install @mapbox/martini
```

### Ports to other languages

- [pymartini](https://github.com/kylebarron/pymartini) (Python)


================================================
FILE: bench.js
================================================

import fs from 'fs';
import {PNG} from 'pngjs';
import Martini from './index.js';
import {mapboxTerrainToGrid} from './test/util.js';

const png = PNG.sync.read(fs.readFileSync('./test/fixtures/fuji.png'));

const terrain = mapboxTerrainToGrid(png);

console.time('init tileset');
const martini = new Martini(png.width + 1);
console.timeEnd('init tileset');

console.time('create tile');
const tile = martini.createTile(terrain);
console.timeEnd('create tile');

console.time('mesh');
const mesh = tile.getMesh(30);
console.timeEnd('mesh');

console.log(`vertices: ${mesh.vertices.length / 2}, triangles: ${mesh.triangles.length / 3}`);

console.time('20 meshes total');
for (let i = 0; i <= 20; i++) {
    console.time(`mesh ${i}`);
    tile.getMesh(i);
    console.timeEnd(`mesh ${i}`);
}
console.timeEnd('20 meshes total');


================================================
FILE: eslint.config.js
================================================
import config from "eslint-config-mourner";

export default [
    ...config,
    {
        rules: {
            'no-use-before-define': 0
        }
    }
];


================================================
FILE: index.d.ts
================================================
export default class Martini {
  constructor(gridSize?: number);
  createTile(terrain: ArrayLike<number>): Tile;
}

export class Tile {
  constructor(terrain: ArrayLike<number>, martini: Martini);
  update(): void;
  getMesh(maxError?: number): {
    vertices: Uint16Array;
    triangles: Uint32Array;
  };
}


================================================
FILE: index.js
================================================

export default class Martini {
    constructor(gridSize = 257) {
        this.gridSize = gridSize;
        const tileSize = gridSize - 1;
        if (tileSize & (tileSize - 1)) throw new Error(
            `Expected grid size to be 2^n+1, got ${gridSize}.`);

        this.numTriangles = tileSize * tileSize * 2 - 2;
        this.numParentTriangles = this.numTriangles - tileSize * tileSize;

        this.indices = new Uint32Array(this.gridSize * this.gridSize);

        // coordinates for all possible triangles in an RTIN tile
        this.coords = new Uint16Array(this.numTriangles * 4);

        // get triangle coordinates from its index in an implicit binary tree
        for (let i = 0; i < this.numTriangles; i++) {
            let id = i + 2;
            let ax = 0, ay = 0, bx = 0, by = 0, cx = 0, cy = 0;
            if (id & 1) {
                bx = by = cx = tileSize; // bottom-left triangle
            } else {
                ax = ay = cy = tileSize; // top-right triangle
            }
            while ((id >>= 1) > 1) {
                const mx = (ax + bx) >> 1;
                const my = (ay + by) >> 1;

                if (id & 1) { // left half
                    bx = ax; by = ay;
                    ax = cx; ay = cy;
                } else { // right half
                    ax = bx; ay = by;
                    bx = cx; by = cy;
                }
                cx = mx; cy = my;
            }
            const k = i * 4;
            this.coords[k + 0] = ax;
            this.coords[k + 1] = ay;
            this.coords[k + 2] = bx;
            this.coords[k + 3] = by;
        }
    }

    createTile(terrain) {
        return new Tile(terrain, this);
    }
}

class Tile {
    constructor(terrain, martini) {
        const size = martini.gridSize;
        if (terrain.length !== size * size) throw new Error(
            `Expected terrain data of length ${size * size} (${size} x ${size}), got ${terrain.length}.`);

        this.terrain = terrain;
        this.martini = martini;
        this.errors = new Float32Array(terrain.length);
        this.update();
    }

    update() {
        const {numTriangles, numParentTriangles, coords, gridSize: size} = this.martini;
        const {terrain, errors} = this;

        // iterate over all possible triangles, starting from the smallest level
        for (let i = numTriangles - 1; i >= 0; i--) {
            const k = i * 4;
            const ax = coords[k + 0];
            const ay = coords[k + 1];
            const bx = coords[k + 2];
            const by = coords[k + 3];
            const mx = (ax + bx) >> 1;
            const my = (ay + by) >> 1;
            const cx = mx + my - ay;
            const cy = my + ax - mx;

            // calculate error in the middle of the long edge of the triangle
            const interpolatedHeight = (terrain[ay * size + ax] + terrain[by * size + bx]) / 2;
            const middleIndex = my * size + mx;
            const middleError = Math.abs(interpolatedHeight - terrain[middleIndex]);

            errors[middleIndex] = Math.max(errors[middleIndex], middleError);

            if (i < numParentTriangles) { // bigger triangles; accumulate error with children
                const leftChildIndex = ((ay + cy) >> 1) * size + ((ax + cx) >> 1);
                const rightChildIndex = ((by + cy) >> 1) * size + ((bx + cx) >> 1);
                errors[middleIndex] = Math.max(errors[middleIndex], errors[leftChildIndex], errors[rightChildIndex]);
            }
        }
    }

    getMesh(maxError = 0) {
        const {gridSize: size, indices} = this.martini;
        const {errors} = this;
        let numVertices = 0;
        let numTriangles = 0;
        const max = size - 1;

        // use an index grid to keep track of vertices that were already used to avoid duplication
        indices.fill(0);

        // retrieve mesh in two stages that both traverse the error map:
        // - countElements: find used vertices (and assign each an index), and count triangles (for minimum allocation)
        // - processTriangle: fill the allocated vertices & triangles typed arrays

        function countElements(ax, ay, bx, by, cx, cy) {
            const mx = (ax + bx) >> 1;
            const my = (ay + by) >> 1;

            if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) {
                countElements(cx, cy, ax, ay, mx, my);
                countElements(bx, by, cx, cy, mx, my);
            } else {
                indices[ay * size + ax] = indices[ay * size + ax] || ++numVertices;
                indices[by * size + bx] = indices[by * size + bx] || ++numVertices;
                indices[cy * size + cx] = indices[cy * size + cx] || ++numVertices;
                numTriangles++;
            }
        }
        countElements(0, 0, max, max, max, 0);
        countElements(max, max, 0, 0, 0, max);

        const vertices = new Uint16Array(numVertices * 2);
        const triangles = new Uint32Array(numTriangles * 3);
        let triIndex = 0;

        function processTriangle(ax, ay, bx, by, cx, cy) {
            const mx = (ax + bx) >> 1;
            const my = (ay + by) >> 1;

            if (Math.abs(ax - cx) + Math.abs(ay - cy) > 1 && errors[my * size + mx] > maxError) {
                // triangle doesn't approximate the surface well enough; drill down further
                processTriangle(cx, cy, ax, ay, mx, my);
                processTriangle(bx, by, cx, cy, mx, my);

            } else {
                // add a triangle
                const a = indices[ay * size + ax] - 1;
                const b = indices[by * size + bx] - 1;
                const c = indices[cy * size + cx] - 1;

                vertices[2 * a] = ax;
                vertices[2 * a + 1] = ay;

                vertices[2 * b] = bx;
                vertices[2 * b + 1] = by;

                vertices[2 * c] = cx;
                vertices[2 * c + 1] = cy;

                triangles[triIndex++] = a;
                triangles[triIndex++] = b;
                triangles[triIndex++] = c;
            }
        }
        processTriangle(0, 0, max, max, max, 0);
        processTriangle(max, max, 0, 0, 0, max);

        return {vertices, triangles};
    }
}


================================================
FILE: package.json
================================================
{
  "name": "@mapbox/martini",
  "version": "0.2.0",
  "type": "module",
  "description": "A JavaScript library for real-time terrain mesh generation",
  "main": "index.js",
  "exports": "./index.js",
  "types": "index.d.ts",
  "scripts": {
    "pretest": "eslint index.js bench.js test",
    "test": "node test/test.js",
    "bench": "node bench.js",
    "prepublishOnly": "npm run test"
  },
  "keywords": [
    "terrain",
    "rtin",
    "mesh",
    "3d",
    "webgl"
  ],
  "author": "Vladimir Agafonkin",
  "license": "ISC",
  "devDependencies": {
    "eslint": "^9.31.0",
    "eslint-config-mourner": "^4.0.2",
    "pngjs": "^7.0.0"
  },
  "files": [
    "index.js",
    "index.d.ts"
  ],
  "eslintConfig": {
    "extends": "mourner",
    "rules": {
      "no-use-before-define": 0
    }
  }
}


================================================
FILE: test/test.js
================================================

import fs from 'fs';
import {PNG} from 'pngjs';
import test from 'node:test';
import assert from 'node:assert/strict';
import Martini from '../index.js';
import {mapboxTerrainToGrid} from './util.js';

const fuji = PNG.sync.read(fs.readFileSync('./test/fixtures/fuji.png'));
const terrain = mapboxTerrainToGrid(fuji);

test('generates a mesh', () => {
    const martini = new Martini(fuji.width + 1);
    const tile = martini.createTile(terrain);
    const mesh = tile.getMesh(500);

    assert.deepEqual([
        320, 64, 256, 128, 320, 128, 384, 128, 256, 0, 288, 160, 256, 192, 288, 192, 320, 192, 304, 176, 256, 256, 288,
        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,
        256, 384, 256, 320, 320, 320, 256, 512, 512, 512, 128, 448, 192, 384, 192, 128, 384, 256, 512, 256, 384, 0,
        512, 128, 256, 64, 192, 0, 256, 64, 128, 32, 96, 0, 128, 32, 64, 16, 48, 0, 64, 0, 32
    ], Array.from(mesh.vertices), 'correct vertices');

    assert.deepEqual([
        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,
        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,
        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,
        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,
        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,
        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,
        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,
        37, 37, 16, 39, 41, 37, 39, 38, 35, 32, 32, 10, 36, 38, 32, 36
    ], Array.from(mesh.triangles), 'correct triangles');
});


================================================
FILE: test/util.js
================================================

export function mapboxTerrainToGrid(png) {
    const gridSize = png.width + 1;
    const terrain = new Float32Array(gridSize * gridSize);

    const tileSize = png.width;

    // decode terrain values
    for (let y = 0; y < tileSize; y++) {
        for (let x = 0; x < tileSize; x++) {
            const k = (y * tileSize + x) * 4;
            const r = png.data[k + 0];
            const g = png.data[k + 1];
            const b = png.data[k + 2];
            terrain[y * gridSize + x] = (r * 256 * 256 + g * 256.0 + b) / 10.0 - 10000.0;
        }
    }
    // backfill right and bottom borders
    for (let x = 0; x < gridSize - 1; x++) {
        terrain[gridSize * (gridSize - 1) + x] = terrain[gridSize * (gridSize - 2) + x];
    }
    for (let y = 0; y < gridSize; y++) {
        terrain[gridSize * y + gridSize - 1] = terrain[gridSize * y + gridSize - 2];
    }

    return terrain;
}
Download .txt
gitextract_e46fi7nx/

├── .github/
│   └── workflows/
│       └── node.yml
├── .gitignore
├── LICENSE
├── README.md
├── bench.js
├── eslint.config.js
├── index.d.ts
├── index.js
├── package.json
└── test/
    ├── test.js
    └── util.js
Download .txt
SYMBOL INDEX (10 symbols across 3 files)

FILE: index.d.ts
  class Martini (line 1) | class Martini {
  class Tile (line 6) | class Tile {

FILE: index.js
  class Martini (line 2) | class Martini {
    method constructor (line 3) | constructor(gridSize = 257) {
    method createTile (line 47) | createTile(terrain) {
  class Tile (line 52) | class Tile {
    method constructor (line 53) | constructor(terrain, martini) {
    method update (line 64) | update() {
    method getMesh (line 95) | getMesh(maxError = 0) {

FILE: test/util.js
  function mapboxTerrainToGrid (line 2) | function mapboxTerrainToGrid(png) {
Condensed preview — 11 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (15K chars).
[
  {
    "path": ".github/workflows/node.yml",
    "chars": 331,
    "preview": "name: Node\non: [push, pull_request]\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n    - name: Checkout\n      uses:"
  },
  {
    "path": ".gitignore",
    "chars": 55,
    "preview": "node_modules\nyarn.lock\n*.log\nmartini.js\nmartini.min.js\n"
  },
  {
    "path": "LICENSE",
    "chars": 738,
    "preview": "ISC License\n\nCopyright (c) 2019, Mapbox\n\nPermission to use, copy, modify, and/or distribute this software for any purpos"
  },
  {
    "path": "README.md",
    "chars": 1353,
    "preview": "# MARTINI\n\n[![Simply Awesome](https://img.shields.io/badge/simply-awesome-brightgreen.svg)](https://github.com/mourner/p"
  },
  {
    "path": "bench.js",
    "chars": 828,
    "preview": "\nimport fs from 'fs';\nimport {PNG} from 'pngjs';\nimport Martini from './index.js';\nimport {mapboxTerrainToGrid} from './"
  },
  {
    "path": "eslint.config.js",
    "chars": 157,
    "preview": "import config from \"eslint-config-mourner\";\n\nexport default [\n    ...config,\n    {\n        rules: {\n            'no-use-"
  },
  {
    "path": "index.d.ts",
    "chars": 309,
    "preview": "export default class Martini {\n  constructor(gridSize?: number);\n  createTile(terrain: ArrayLike<number>): Tile;\n}\n\nexpo"
  },
  {
    "path": "index.js",
    "chars": 6246,
    "preview": "\nexport default class Martini {\n    constructor(gridSize = 257) {\n        this.gridSize = gridSize;\n        const tileSi"
  },
  {
    "path": "package.json",
    "chars": 800,
    "preview": "{\n  \"name\": \"@mapbox/martini\",\n  \"version\": \"0.2.0\",\n  \"type\": \"module\",\n  \"description\": \"A JavaScript library for real"
  },
  {
    "path": "test/test.js",
    "chars": 2003,
    "preview": "\nimport fs from 'fs';\nimport {PNG} from 'pngjs';\nimport test from 'node:test';\nimport assert from 'node:assert/strict';\n"
  },
  {
    "path": "test/util.js",
    "chars": 893,
    "preview": "\nexport function mapboxTerrainToGrid(png) {\n    const gridSize = png.width + 1;\n    const terrain = new Float32Array(gri"
  }
]

About this extraction

This page contains the full source code of the mapbox/martini GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 11 files (13.4 KB), approximately 4.5k tokens, and a symbol index with 10 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!