# mapbox

Mapbox GL JS integration — Three.js custom layer and coordinate projection utilities.

```typescript
import {
    CustomLayer,
    projectGeometriesToMapboxCoords,
    projectGeometryToMapboxCoords,
    projectPositionsToMapboxMercators,
} from '@yaoc1996/js-tools/mapbox'
```

---

## `class CustomLayer`

A Mapbox GL JS custom layer that embeds a Three.js scene inside a Mapbox map. Extends `EventDispatcher` and implements `CustomLayerInterface`.

```typescript
const layer = new CustomLayer({
    id: 'my-3d-layer',
    anchor: [lng, lat],
    renderingMode: '3d',
})

layer.addEventListener('beforerender', ({ renderer, scene, camera }) => {
    // update uniforms, animations, etc.
})

map.addLayer(layer)
layer.add(myMesh)
```

### Constructor

| Name | Type | Description |
|------|------|-------------|
| `options.id` | `string` | Mapbox layer id |
| `options.anchor` | `LngLatLike` | Geographic origin for the Three.js world space |
| `options.renderingMode` | `"2d" \| "3d"` | Mapbox rendering mode |

### Methods

#### `add(object: Object3D) → void`

Adds an object to the internal Three.js scene.

#### `remove(object: Object3D) → void`

Removes an object from the internal Three.js scene.

### Properties

| Name | Type | Description |
|------|------|-------------|
| `id` | `string` | Layer id |
| `renderingMode` | `"2d" \| "3d"` | Rendering mode |
| `anchor` | `LngLat` | Geographic anchor converted from constructor input |
| `worldAnchor` | `MercatorCoordinate` | Mercator coordinate of the anchor |
| `map` | `Map \| null` | The Mapbox map instance after `onAdd`, `null` otherwise |

### Events

| Event | Payload | When |
|-------|---------|------|
| `beforerender` | `{ renderer, scene, camera }` | Before the Three.js scene is rendered each frame |
| `afterrender` | `{ renderer, scene, camera }` | After the Three.js scene is rendered each frame |

### Notes

- Three.js objects added via `add` should use world coordinates relative to `worldAnchor`.
- The renderer is created in `onAdd` and disposed in `onRemove`.

---

## Utilities

---

## `projectGeometryToMapboxCoords(geometry, anchor, options?) → Promise<void>`

Reprojects the `position` buffer of a `BufferGeometry` from a source CRS into Mapbox mercator world space, updating the geometry in place.

### Parameters

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `geometry` | `BufferGeometry` | — | Geometry whose `position` attribute to reproject |
| `anchor` | `MercatorCoordinate` | — | World-space origin (output of `CustomLayer.worldAnchor`) |
| `options.useWorker` | `boolean` | `true` | Run projection in a Web Worker |
| `options.crs` | `ProjectionDefinition` | — | Source CRS; omit for WGS84 input |
| `options.offset` | `[number, number, number]` | `[0,0,0]` | XYZ offset applied before projection |

### Example

```typescript
import { projectGeometryToMapboxCoords } from '@yaoc1996/js-tools/mapbox'

await projectGeometryToMapboxCoords(geometry, layer.worldAnchor, {
    crs: proj4.defs('EPSG:3857'),
    offset: [originX, originY, originZ],
})
```

### Notes

- Throws `Error` if the `position` attribute array is not a `Float32Array`.
- Calls `computeBoundingBox()` and `computeBoundingSphere()` after updating positions.
- With `useWorker: true` (default), projection runs off the main thread via a Comlink-wrapped worker.

---

## `projectGeometriesToMapboxCoords(geometries, anchor, options?) → Promise<void>`

Same as `projectGeometryToMapboxCoords` but accepts an array of geometries and projects them all in a single worker round-trip.

### Parameters

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `geometries` | `BufferGeometry[]` | — | Geometries to reproject in place |
| `anchor` | `MercatorCoordinate` | — | World-space origin (output of `CustomLayer.worldAnchor`) |
| `options.useWorker` | `boolean` | `true` | Run projection in a Web Worker |
| `options.crs` | `ProjectionDefinition` | — | Source CRS; omit for WGS84 input |
| `options.offset` | `[number, number, number]` | `[0,0,0]` | XYZ offset applied before projection |

### Notes

- Single function call → single worker spawn, one batched message, one terminate.
- Throws if any geometry's `position` attribute is not a `Float32Array`.
- `projectGeometryToMapboxCoords` is a thin wrapper around this function with a single geometry.

---

## `projectPositionsToMapboxMercators(positions, worldAnchor, offset?, crs?, out?) → Float32Array`

Converts a flat `Float32Array` of XYZ positions from a source CRS into Mapbox mercator world space relative to a given anchor. Single-pass with inlined Web Mercator math — no per-vertex object allocations.

### Parameters

| Name | Type | Default | Description |
|------|------|---------|-------------|
| `positions` | `Float32Array` | — | Interleaved XYZ positions |
| `worldAnchor` | `MercatorCoordinate` | — | World-space origin |
| `offset` | `[number, number, number]` | `[0, 0, 0]` | XYZ offset applied before projection |
| `crs` | `ProjectionDefinition` | — | Source CRS; omit for WGS84 input |
| `out` | `Float32Array` | — | Optional output buffer; if provided, the function writes into it and returns it (must be at least `positions.length` long). Pass `positions` itself to project in place. |

### Returns

`Float32Array` — reprojected positions in mercator world space. Same instance as `out` if provided, otherwise a newly allocated array.

### Example

```typescript
import { projectPositionsToMapboxMercators } from '@yaoc1996/js-tools/mapbox'

const mercatorPositions = projectPositionsToMapboxMercators(
    rawPositions,
    layer.worldAnchor,
    [originX, originY, originZ],
    proj4.defs('EPSG:3857'),
)
```

### Notes

- Used internally by `projectGeometryToMapboxCoords` for the `useWorker: false` path and by the Mapbox web worker (which passes its input buffer as `out` for zero-allocation projection).
- When `crs` is provided, the y-component is scaled by `crs.to_meter` (defaults to `1.0`).
- `meterInMercatorCoordinateUnits()` is read once from `worldAnchor` and applied uniformly to all vertex elevations. For continent-scale geometries this can drift a few percent at the extremes versus a per-vertex computation.
- Latitudes outside ±85.0511° are clamped to mercator's valid range.

---

## Types

```typescript
type MapboxWorkerApi = {
    projectGeometries(params: {
        positions: ArrayBufferLike[];
        worldAnchor: MercatorCoordinate;
        offset?: [number, number, number];
        crs?: ProjectionDefinition;
    }): {
        positions: ArrayBuffer[];
    };
};
```

The worker is wrapped via [Comlink](https://github.com/GoogleChromeLabs/comlink). For advanced use cases, `createMapboxWorker()` returns `{ api, terminate }` — call `api.projectGeometries(...)` directly and `terminate()` when done.
