import { type Aabb, type Vec2 } from "./geometry.js"; /** * Deterministic spatial sampling — draw one or many positions from a geometric region under * explicit constraints, with an injected RNG, bounded attempts, and a structured pass/fail result. * * The three concerns stay separate on purpose (issue #940): a {@link SampleRegion} owns *where and * how* candidates are drawn (its distribution policy — area- vs radial-uniform, edge-biased, weighted * subregions); {@link SampleConstraints} own *which* candidates survive (include/exclude regions, * keep-out distance via disc/sphere exclusions, caller predicates, surface projection, minimum * separation); and {@link samplePoint}/{@link sampleBatch} own *how hard we try* (attempt budget and * fallback policy). Nothing silently accepts an invalid point — exhaustion is reported, not hidden. */ /** A 3D point `[x, y, z]` — the volume/surface counterpart to {@link Vec2}. */ export type Point3 = readonly [number, number, number]; /** A sampled point: 2D `[x, z]` or 3D `[x, y, z]`. Every point in one run shares its dimensionality. */ export type SamplePoint = readonly number[]; /** * A geometric region a sampler can draw from. Built-ins cover point sets, rect/box, circle/sphere, * annulus/shell, polygon, and weighted composites; {@link customRegion} wraps a caller-defined * sampler + bounds. `sample` draws one raw candidate under the region's own distribution policy; * `contains` is the membership test reused for include/exclude constraints and rejection. */ export interface SampleRegion

{ /** 2 for planar regions (`[x, z]`), 3 for volumetric/surface regions (`[x, y, z]`). */ readonly dimensions: 2 | 3; /** True when the region can never yield a point (degenerate size, empty set) — reported, not sampled. */ readonly isEmpty: boolean; /** Draw one raw candidate using `rng` (may still fail constraints). Consumes a fixed number of draws. */ sample(rng: () => number): P; /** Membership test on the XZ (or XYZ) footprint — powers include gates and exclude keep-outs. */ contains(point: P): boolean; } /** * Post-draw gates a candidate must clear. All are optional; an empty constraint set accepts every * in-region draw. `exclude` discs/spheres are the keep-out distance primitive ("no closer than R to * this center"); `accept` is a free-form caller predicate (biome, slope, ownership); `project` snaps * a candidate onto a surface/navmesh before validation and may reject by returning `null`. */ export interface SampleConstraints

{ /** Candidate must lie inside *every* listed region (intersection); omit for no inclusion gate. */ include?: readonly SampleRegion

[]; /** Candidate must lie outside *every* listed region — the keep-out/exclusion set. */ exclude?: readonly SampleRegion

[]; /** Extra caller acceptance test applied last; `false` rejects the candidate. */ accept?: (point: P) => boolean; /** Optional surface/navmesh projection applied before validation; `null` rejects the candidate. */ project?: (point: P) => P | null; /** Reject a candidate within this Euclidean distance of an already-accepted point in the same run. */ minSeparation?: number; } /** * What to return when the attempt budget is exhausted. `"none"` yields no point (honest failure); * `"last-candidate"` returns the final rejected draw (post-projection); `{ point }` returns a caller * fixed fallback (a hand-placed safe spot). Explicit, so a caller never mistakes a fallback for a hit. */ export type FallbackPolicy

= "none" | "last-candidate" | { readonly point: P; }; /** Why a sample ended: a clean hit, budget exhausted, or the region was degenerate. */ export type SampleReason = "accepted" | "exhausted" | "empty-region"; /** Structured single-sample outcome — never a bare point, so failure and fallback are visible. */ export interface SampleResult

{ /** True only for `"accepted"` — a genuine in-constraints hit (fallbacks are never `ok`). */ readonly ok: boolean; /** The accepted point, or the fallback point, or `null` when no fallback applied. */ readonly point: P | null; /** How many candidates were drawn (0 for an empty region). */ readonly attempts: number; /** Outcome classification. */ readonly reason: SampleReason; /** True when `point` came from the fallback policy rather than a passing draw. */ readonly usedFallback: boolean; } /** Inputs for a single {@link samplePoint} draw. */ export interface SamplePointOptions

{ region: SampleRegion

; /** Injected deterministic RNG in `[0, 1)`; same stream → same result. */ rng: () => number; constraints?: SampleConstraints

; /** Maximum candidates to draw before giving up. Default 30. Bounds the work. */ maxAttempts?: number; /** What to return on exhaustion. Default `"none"`. */ fallback?: FallbackPolicy

; /** Already-placed points the new point must respect `minSeparation` against (batch threads this). */ placed?: readonly P[]; } /** * Draw one point from `region` that clears every constraint, retrying up to `maxAttempts` times with * the injected `rng`, then apply the `fallback` policy. Returns a structured {@link SampleResult} — * a clean hit is `ok: true`; exhaustion or a degenerate region is reported with the attempt count and * an explicit fallback flag, never a silently-accepted bad point. * * @capability spatial-sample draw one deterministic position from a region under include/exclude/distance constraints */ export declare function samplePoint

(options: SamplePointOptions

): SampleResult

; /** Inputs for a {@link sampleBatch} draw of many spaced points. */ export interface SampleBatchOptions

{ region: SampleRegion

; rng: () => number; /** How many points to place. */ count: number; constraints?: SampleConstraints

; /** Attempt budget **per point**. Default 30. */ maxAttempts?: number; } /** Structured batch outcome: the placed points plus whether the full count was met. */ export interface SampleBatchResult

{ /** Accepted points in deterministic draw order. */ readonly points: readonly P[]; /** The requested count. */ readonly requested: number; /** How many were actually placed (`<= requested`). */ readonly placed: number; /** True when `placed === requested`. */ readonly complete: boolean; /** Total candidates drawn across all points. */ readonly attempts: number; } /** * Place up to `count` points from `region`, each clearing the shared constraints and honoring * `minSeparation` against every point already placed in this batch (via a spatial hash, not an * O(n²) scan). Order is deterministic for a given `rng` stream. Batches never silently under-fill: * the result reports `placed`, `complete`, and the total attempt count. * * @capability spatial-sample-batch place many deterministic spaced points from a region with minimum separation */ export declare function sampleBatch

(options: SampleBatchOptions

): SampleBatchResult

; /** Fill policy for a circle/ring: `"area"` = area-uniform (even density); `"radial"` = radius-uniform (clumps toward center). */ export type RadialDistribution = "area" | "radial"; /** Fill policy for a sphere/shell: `"volume"` = volume-uniform (even density); `"radial"` = radius-uniform. */ export type VolumeDistribution = "volume" | "radial"; /** Fill policy for a rect/box: `"uniform"` = even coverage; `"edge"` = biased to a boundary band. */ export type AreaDistribution = "uniform" | "edge"; /** A weighted member of a {@link weightedRegion} composite. */ export interface WeightedRegionEntry

{ region: SampleRegion

; /** Relative selection weight; non-positive entries are never chosen. */ weight: number; } /** * A fixed set of candidate points sampled by (optionally weighted) selection — the "spawn point * table" region. `contains` is exact membership. Empty when the set is empty. * * @capability spatial-region-point-set sample from a fixed, optionally weighted candidate point table */ export declare function pointSetRegion(points: readonly Vec2[], options?: { weights?: readonly number[]; }): SampleRegion; /** * An axis-aligned rectangle on the XZ plane. `"uniform"` spreads evenly (draws x then z); * `"edge"` biases onto a boundary band of `edgeThickness` meters (perimeter-weighted side, then * inward depth) — the two are never conflated, the caller opts in. */ export declare function rectRegion(area: Aabb, options?: { distribution?: AreaDistribution; edgeThickness?: number; }): SampleRegion; /** * A filled circle. `"area"` is area-uniform (√-corrected radius, even density); `"radial"` is * radius-uniform (clumps toward the center). Angle is drawn first, then radius — matching the common * `angle = rng()·2π; r = rng()·R` idiom so migrations keep their stream. */ export declare function discRegion(center: Vec2, radius: number, options?: { distribution?: RadialDistribution; }): SampleRegion; /** * A ring (annulus) between `innerRadius` and `outerRadius`. `"area"` fills the band with even * density; `"radial"` spreads uniformly in radius. Angle first, then radius. */ export declare function annulusRegion(center: Vec2, innerRadius: number, outerRadius: number, options?: { distribution?: RadialDistribution; }): SampleRegion; /** * An arbitrary closed polygon on the XZ plane. Candidates are drawn uniformly from the polygon's * bounding box and gated by point-in-polygon `contains`, so a run's draw count stays fixed (the * sampler's attempt budget bounds the rejection, not a hidden inner loop). * * @capability spatial-region-polygon sample within an arbitrary closed polygon on the XZ plane */ export declare function polygonRegion(polygon: readonly Vec2[]): SampleRegion; /** * A composite that first picks one member by weight, then delegates to its sampler — the * "weighted subregions" distribution policy. `contains` is true when any member contains the point. * * @capability spatial-region-weighted compose sub-regions behind a weighted selection policy */ export declare function weightedRegion

(entries: readonly WeightedRegionEntry

[]): SampleRegion

; /** * Wrap a caller-defined sampler and bounds test as a {@link SampleRegion} — the escape hatch for * regions the built-ins do not cover (a navmesh cell, a heightfield mask, a spline tube). * * @capability spatial-region-custom wrap a caller-defined sampler and bounds test as a region */ export declare function customRegion

(spec: { dimensions: 2 | 3; sample: (rng: () => number) => P; contains: (point: P) => boolean; isEmpty?: boolean; }): SampleRegion

; /** * An axis-aligned box `[min..max]` in 3D. Uniform density; draws x, y, z in order. * * @capability spatial-region-box sample uniformly within an axis-aligned 3D box */ export declare function boxRegion(min: Point3, max: Point3): SampleRegion; /** * A filled ball. `"volume"` is volume-uniform (∛-corrected radius); `"radial"` is radius-uniform. * Direction is drawn first (two draws), then radius. * * @capability spatial-region-sphere sample within a filled 3D ball, volume- or radius-uniform */ export declare function sphereRegion(center: Point3, radius: number, options?: { distribution?: VolumeDistribution; }): SampleRegion; /** * A spherical shell between `innerRadius` and `outerRadius`. `"volume"` fills the shell with even * density; `"radial"` spreads uniformly in radius. Direction first, then radius. * * @capability spatial-region-shell sample within a 3D spherical shell, volume- or radius-uniform */ export declare function shellRegion(center: Point3, innerRadius: number, outerRadius: number, options?: { distribution?: VolumeDistribution; }): SampleRegion; /** Inputs for {@link sampleStratified}: a grid over `area` with one jittered point per cell. */ export interface StratifiedOptions { area: Aabb; rng: () => number; /** Grid columns (X) and rows (Z). */ cols: number; rows: number; /** Cell jitter 0..1 — 0 is a rigid lattice, 1 fills the whole cell. Default 1. */ jitter?: number; } /** * Stratified 2D sampling: one jittered point per grid cell over `area`, in deterministic row-major * order. Unlike pure rejection sampling this guarantees even coverage (no clumps, no gaps) — the * distinct "stratified" distribution policy, kept separate from area-uniform draws. * * @capability spatial-sample-stratified evenly cover an area with one jittered point per grid cell, deterministic */ export declare function sampleStratified(options: StratifiedOptions): Vec2[];