import type { Aabb, Vec2 } from "./geometry.js"; import { type TerrainField } from "./terrain.js"; /** A sculpt operation kind: heightfield brushes plus the surface-paint brush. */ export type TerraformMode = "raise" | "lower" | "smooth" | "flatten" | "noise" | "ramp" | "paint"; /** How a brush's strength fades from its center to its rim. */ export type TerraformFalloff = "smooth" | "linear" | "none"; /** A brush footprint: a round disc or an axis-aligned square. */ export type TerraformShape = "circle" | "square"; /** Clamp bounds applied to the resulting terrain height after a heightfield edit. */ export interface TerraformHeightLimit { min?: number; max?: number; } /** A single sculpt stamp: which brush, where, and its shaping parameters. */ export interface TerraformEdit { mode: TerraformMode; center: Vec2; radius: number; strength?: number; /** Flatten target height, or ramp start height when `to` is set (defaults to sampled ground). */ target?: number; /** Ramp end point; required for `mode: "ramp"`, ignored otherwise. */ to?: Vec2; /** Ramp end height (defaults to sampled ground at `to`). */ targetTo?: number; surface?: string; falloff?: TerraformFalloff; shape?: TerraformShape; /** Noise brush frequency (world units), higher = finer detail. */ frequency?: number; /** Noise brush seed — same seed reproduces the same roughening. */ seed?: number; /** Clamp final terrain height into this range after the edit. */ heightLimit?: TerraformHeightLimit; } export interface EditableTerrainConfig { bounds: Aabb; base?: TerrainField; cellSize?: number; } /** * One material layer in a terrain's reorderable stack: a palette `surface` id (drives the base * color) plus its render parameters. Array order is the stack order — lower index paints under * higher. `roughness`/`tiling`/`triplanar`/`tint`/`opacity` are carried as data so a runtime game * reads them straight off the snapshot. */ export interface TerrainMaterialLayer { id: string; surface: string; /** Surface micro-roughness 0..1 (0 = mirror, 1 = matte). */ roughness?: number; /** World units per texture repeat. */ tiling?: number; /** Project the material on all three axes to hide stretching on cliffs. */ triplanar?: boolean; /** Authored hex color replacing the default surface palette color in editor and runtime. */ tint?: string; /** Layer master opacity 0..1 — scales this layer's blend weight everywhere. */ opacity?: number; /** * Material library name (e.g. `"foliage/grass"`) whose scanned PBR color map paints this layer, * as authored. The editor resolves it into {@link TerrainMaterialLayer.materialUrl} so the runtime * needs no asset-library lookup. */ material?: string; /** Resolved URL of the layer's scanned color map, written by the editor from {@link TerrainMaterialLayer.material}. */ materialUrl?: string; } export interface TerraformSnapshot { bounds: Aabb; cellSize: number; cols: number; rows: number; offsets: readonly number[]; /** Dominant surface id per cell — the single-layer fast path and backward-compatible channel. */ surfaces: readonly (string | null)[]; /** Reorderable material stack; absent on pre-2.0 documents (derive with {@link migrateTerrainSnapshot}). */ layers?: readonly TerrainMaterialLayer[]; /** * Per-cell per-layer blend weights, flat `cols*rows*layers.length` (cell-major, then layer), * each cell summing to ≈1. Lazy: absent until a blend is painted, so single-layer terrains stay * compact. When absent, blending falls back to the hard {@link TerraformSnapshot.surfaces} id. */ weights?: readonly number[]; } /** * A compact record of the vertices a sculpt stroke touched: parallel `indices`/`before`/`after` * arrays into the offset grid. Storing one of these per stroke keeps undo history small — the * whole terrain document is never copied. */ export interface TerraformDelta { indices: readonly number[]; before: readonly number[]; after: readonly number[]; } /** Reports each changed vertex during a recorded edit: grid index, prior offset, new offset. */ export type TerraformDeltaRecorder = (index: number, before: number, after: number) => void; /** * A compact record of the surface-material cells a paint stroke touched: parallel * `indices`/`before`/`after` arrays into the per-cell surface grid. One per stroke keeps paint * undo history small. */ export interface SurfaceDelta { indices: readonly number[]; before: readonly (string | null)[]; after: readonly (string | null)[]; } /** Reports each changed cell during a recorded paint: grid index, prior surface id, new surface id. */ export type SurfaceDeltaRecorder = (index: number, before: string | null, after: string | null) => void; /** * A compact record of the blend weights a blend-paint stroke touched: parallel `indices`/`before`/ * `after` arrays into the flat `weights` grid (`cell*layerCount + layer`). `layerCount` guards the * delta against being replayed after the layer stack changed shape. */ export interface WeightDelta { layerCount: number; indices: readonly number[]; before: readonly number[]; after: readonly number[]; } /** Reports each changed weight slot during a recorded blend: flat index, prior weight, new weight. */ export type WeightDeltaRecorder = (index: number, before: number, after: number) => void; /** A height/slope predicate for auto-painting a surface layer (e.g. rock on steep slopes, snow up high). */ export interface TerrainSurfaceRule { surface: string | null; /** Minimum slope (rise/run) a cell must have to be painted. */ minSlope?: number; maxSlope?: number; minHeight?: number; maxHeight?: number; } export interface EditableTerrain extends TerrainField { readonly cols: number; readonly rows: number; readonly cellSize: number; /** World-space extent of the cell grid; cell `(gx, gz)` spans `cols`/`rows` equal slices of it. */ readonly gridBounds: Aabb; apply(edit: TerraformEdit): number; /** Apply an edit while reporting every touched vertex to `record` — the seam strokes record deltas through. */ applyRecording(edit: TerraformEdit, record: TerraformDeltaRecorder): number; /** Apply a single edit and return the compact delta it produced. */ editDelta(edit: TerraformEdit): TerraformDelta; /** Re-apply a delta's `after` offsets (redo). */ applyDelta(delta: TerraformDelta): void; /** Restore a delta's `before` offsets (undo). */ revertDelta(delta: TerraformDelta): void; /** Paint a surface brush while reporting every changed cell to `record` — surface strokes record deltas through. */ paintRecording(edit: TerraformEdit, record: SurfaceDeltaRecorder): number; /** Paint a surface brush and return the compact delta it produced. */ paintDelta(edit: TerraformEdit): SurfaceDelta; /** Fill every cell with one surface (or `null` to clear) and return the compact delta. */ fillSurfaceDelta(surface: string | null): SurfaceDelta; /** Paint a surface into every cell matching a height/slope rule and return the compact delta. */ autoPaintDelta(rule: TerrainSurfaceRule): SurfaceDelta; /** Re-apply a surface delta's `after` ids (redo). */ applySurfaceDelta(delta: SurfaceDelta): void; /** Restore a surface delta's `before` ids (undo). */ revertSurfaceDelta(delta: SurfaceDelta): void; /** The current material layer stack (lower index paints under higher). */ readonly layers: readonly TerrainMaterialLayer[]; /** Replace the material stack, retaining weights by layer id and normalizing when painted layers are removed. */ setLayers(layers: readonly TerrainMaterialLayer[]): void; /** * Paint the active `edit.surface` layer's blend weight into the brush footprint, pushing the * cell toward that layer while renormalizing the rest. Returns the compact delta it produced; * empty if `edit.surface` is not one of the current {@link layers}. */ blendPaintDelta(edit: TerraformEdit): WeightDelta; /** Blend-paint while reporting every changed weight slot to `record` — blend strokes record through it. */ blendRecording(edit: TerraformEdit, record: WeightDeltaRecorder): number; /** Re-apply a weight delta's `after` values (redo). */ applyWeightDelta(delta: WeightDelta): void; /** Restore a weight delta's `before` values (undo). */ revertWeightDelta(delta: WeightDelta): void; /** The per-layer blend weights under a world point (parallel to {@link layers}); empty when unblended. */ weightsAt(x: number, z: number): number[]; heightOffsetAt(x: number, z: number): number; surfaceAt(x: number, z: number): string | null; reset(): void; snapshot(): TerraformSnapshot; restore(snapshot: TerraformSnapshot): void; } /** Smooth/linear/hard brush weight for a sample `distance` from the brush center. */ export declare function brushWeight(distance: number, radius: number, falloff: TerraformFalloff): number; export declare function createEditableTerrain(config: EditableTerrainConfig): EditableTerrain; /** * Accumulates a whole drag — many brush stamps — into one compact {@link TerraformDelta}. Keeps * each vertex's first `before` and latest `after`, so undo replays the stroke as a single step * even though the pointer fired dozens of moves. */ export interface TerraformStroke { stamp(edit: TerraformEdit): number; delta(): TerraformDelta; isEmpty(): boolean; } /** Opens a stroke recorder over `terrain`; stamp edits into it, then read one net delta. */ export declare function beginTerraformStroke(terrain: Pick): TerraformStroke; /** * Accumulates a whole paint drag — many surface stamps — into one compact {@link SurfaceDelta}. * Keeps each cell's first `before` and latest `after`, so undo replays the paint as a single step. */ export interface SurfaceStroke { stamp(edit: TerraformEdit): number; delta(): SurfaceDelta; isEmpty(): boolean; } /** Opens a paint-stroke recorder over `terrain`; stamp paint edits into it, then read one net delta. */ export declare function beginSurfaceStroke(terrain: Pick): SurfaceStroke; /** Returns a new snapshot with a surface delta's `after` ids applied (copy-on-write). */ export declare function applySurfaceDeltaToSnapshot(snapshot: TerraformSnapshot, delta: SurfaceDelta): TerraformSnapshot; /** Returns a new snapshot with a surface delta's `before` ids restored (copy-on-write undo). */ export declare function revertSurfaceDeltaFromSnapshot(snapshot: TerraformSnapshot, delta: SurfaceDelta): TerraformSnapshot; /** * Upgrades a pre-2.0 snapshot in place-safe (copy-on-write) form: derives a {@link TerrainMaterialLayer} * stack from the distinct painted surfaces (first-seen order, default params) when none exists. * Leaves the lazy `weights` buffer absent — a single-layer terrain stays compact until blended. * Idempotent: a snapshot that already carries `layers` is returned unchanged. */ export declare function migrateTerrainSnapshot(snapshot: TerraformSnapshot): TerraformSnapshot; /** * Returns a new snapshot with a weight delta's `after` values applied (materializing weights first). * @internal — undo/redo plumbing behind the `blendTerrain` command. */ export declare function applyWeightDeltaToSnapshot(snapshot: TerraformSnapshot, delta: WeightDelta): TerraformSnapshot; /** * Returns a new snapshot with a weight delta's `before` values restored (copy-on-write undo). * @internal — undo/redo plumbing behind the `blendTerrain` command. */ export declare function revertWeightDeltaFromSnapshot(snapshot: TerraformSnapshot, delta: WeightDelta): TerraformSnapshot; /** * Accumulates a whole blend-paint drag into one compact {@link WeightDelta}. Keeps each weight * slot's first `before` and latest `after`, so undo replays the blend as a single step. */ export interface BlendStroke { stamp(edit: TerraformEdit): number; delta(): WeightDelta; isEmpty(): boolean; } /** * Opens a blend-stroke recorder over `terrain`; stamp blend edits into it, then read one net delta. * @internal — the stroke recorder behind the blend brush and the `blendTerrain` command. */ export declare function beginBlendStroke(terrain: Pick): BlendStroke; /** * The world-space {@link Aabb} spanning the vertices a stroke's `indices` touched (indices into the * `(cols+1)×(rows+1)` offset grid), expanded by `margin` meters. Drives partial preview-mesh * rebuilds — only the dirty submesh re-samples instead of the whole heightfield. Null if empty. * @internal — the editor's preview-mesh rebuild uses this; not a game-facing primitive. */ export declare function dirtyBoundsFromIndices(snapshot: TerraformSnapshot, indices: readonly number[], margin?: number): Aabb | null; /** A fresh, unedited terrain snapshot sized to `bounds`/`cellSize` — the seed for a new sculpt document. */ export declare function createTerrainSnapshot(config: EditableTerrainConfig): TerraformSnapshot; /** Rebuilds a live {@link EditableTerrain} from a snapshot, layered over `base` ground. */ export declare function editableTerrainFromSnapshot(snapshot: TerraformSnapshot, base?: TerrainField): EditableTerrain; /** Returns a new snapshot with a delta's `after` offsets applied (copy-on-write — inputs untouched). */ export declare function applyDeltaToSnapshot(snapshot: TerraformSnapshot, delta: TerraformDelta): TerraformSnapshot; /** Returns a new snapshot with a delta's `before` offsets restored (copy-on-write undo). */ export declare function revertDeltaFromSnapshot(snapshot: TerraformSnapshot, delta: TerraformDelta): TerraformSnapshot; export interface TerraformBrushConfig { radius?: number; strength?: number; falloff?: TerraformFalloff; surface?: string; } export interface TerraformBrush { raise(center: Vec2): number; lower(center: Vec2): number; flatten(center: Vec2, target?: number): number; paint(center: Vec2, surface?: string): number; setRadius(radius: number): void; setStrength(strength: number): void; config(): Required> & { surface: string | undefined; }; /** The retunable brush settings; the terrain keeps its own `snapshot()`. */ snapshot(): TerraformBrushState; restore(next: TerraformBrushState): void; } /** Plain JSON state of a {@link TerraformBrush}. */ export interface TerraformBrushState { radius: number; strength: number; } export declare function createTerraformBrush(terrain: Pick, config?: TerraformBrushConfig): TerraformBrush;