@splatwalk/core 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eric Eisaman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # @splatwalk/core
2
+
3
+ The SplatWalk WASM core as a single, versioned, binary-friendly package. It bundles:
4
+
5
+ - `wasm_splatwalk.js` + `wasm_splatwalk_bg.wasm` — the wasm-bindgen glue and binary.
6
+ - `wasm_splatwalk.d.ts` — hand-authored TypeScript declarations with the real
7
+ settings and result shapes (the generated wasm-bindgen `.d.ts` types every
8
+ argument and result as `any`).
9
+ - `floor` subpath — a framework-agnostic FAST NAV floor module (`buildFastFloorMesh`,
10
+ `extractFloorFieldWithRecovery`, `trimStrayFloorCells`, the dense-floor seed/region
11
+ estimators, the recovery ladder, and the canonical `FAST_NAV_PRESET`). No Babylon
12
+ or bundler dependency.
13
+
14
+ This package targets binary-only and non-Babylon integrators. The reference
15
+ TypeScript bridge and Babylon UI live in the main SplatWalk repository.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install @splatwalk/core
21
+ ```
22
+
23
+ MIT-licensed and free forever, including for commercial and proprietary use.
24
+ See [`LICENSING.md`](https://github.com/EricEisaman/splatwalk/blob/main/LICENSING.md).
25
+
26
+ ## Use the binary directly
27
+
28
+ ```ts
29
+ import init, {
30
+ init_splatwalk,
31
+ build_walkable_ground_field,
32
+ build_room_floor_mesh,
33
+ mesh_to_glb,
34
+ } from '@splatwalk/core';
35
+
36
+ await init(); // always init before calling named exports
37
+ init_splatwalk();
38
+
39
+ const field = build_walkable_ground_field(splatBytes, settings);
40
+ if (field.api_version !== 2) throw new Error('stale SplatWalk binary');
41
+
42
+ // One-call room floor (binary-side equivalent of the FAST NAV floor path):
43
+ const floor = build_room_floor_mesh(splatBytes, { ...settings, emit_glb: true });
44
+ const glb = floor.glb ?? mesh_to_glb(floor.mesh.vertices, floor.mesh.indices);
45
+ ```
46
+
47
+ `semver` and `capabilities` on every result let you tolerate additive change
48
+ instead of hard-failing on a version bump; `api_version` stays the hard gate.
49
+
50
+ ## Use the framework-agnostic floor module
51
+
52
+ ```ts
53
+ import {
54
+ FAST_NAV_PRESET,
55
+ extractFloorFieldWithRecovery,
56
+ resolveRecovery,
57
+ } from '@splatwalk/core/floor';
58
+
59
+ const result = await extractFloorFieldWithRecovery({
60
+ bytes: splatBytes,
61
+ // Inject the binary's field builder so the module stays engine-agnostic:
62
+ buildField: async (b, s) => build_walkable_ground_field(b, s),
63
+ baseSettings: { ...FAST_NAV_PRESET, rotation, flip_y, collision_seed },
64
+ seed: collision_seed,
65
+ recovery: resolveRecovery(),
66
+ log: (m) => console.log(m),
67
+ });
68
+ ```
69
+
70
+ See the [Integration Guide](https://github.com/EricEisaman/splatwalk/blob/main/docs/INTEGRATION.md)
71
+ for a task-oriented walkthrough, and `docs/wasm-api.md` in the main repository for
72
+ the full settings reference, the coordinate + winding contract, and the progress
73
+ line protocol.
package/floor.d.ts ADDED
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Framework-agnostic FAST NAV floor logic.
3
+ *
4
+ * This module contains the room-floor extraction math that does NOT depend on
5
+ * Babylon.js, the viewer, or any web-worker glue. It operates purely on the
6
+ * {@link WalkableGroundFieldResult} returned by the WASM core plus plain
7
+ * {@link MeshSettings}, so binary-only and non-Babylon integrators can reuse it
8
+ * without pulling in a 3D engine. The Babylon orchestration (the viewer, the nav
9
+ * worker, spawn handling) lives in {@link "@/navigation/fastNav"}, which
10
+ * re-exports everything here for backwards compatibility.
11
+ *
12
+ * The only imports are type-only, so nothing in this module ties a consumer to a
13
+ * specific runtime or bundler.
14
+ */
15
+ import type { MeshSettings, WalkableGroundFieldResult } from './wasm_splatwalk';
16
+ /** A single human-readable progress line, optionally tagged with `[INFO]`, `[WAIT]`, `[WARN]`, `[SUCCESS]`. */
17
+ export type FastNavLogger = (message: string) => void;
18
+ /**
19
+ * Canonical FAST NAV floor-field preset.
20
+ *
21
+ * These are the conservative reconstruction + 2.5D floor-field settings the
22
+ * reference FAST NAV path uses to extract a room floor. Binary-only integrators
23
+ * can pass this straight to `build_walkable_ground_field` / `build_room_floor_mesh`
24
+ * (merged with their own per-scene `rotation`, `flip_y`, `collision_seed`, and
25
+ * optional `region_min`/`region_max`) instead of reverse-engineering the values.
26
+ *
27
+ * It intentionally omits per-scene fields (`rotation`, `flip_y`, `collision_seed`,
28
+ * `region_*`) so callers supply those for their own splat orientation and seed.
29
+ *
30
+ * Keep this in sync with the Rust `fast_nav_preset_json()` in
31
+ * `wasm-splatwalk/src/lib.rs`, which the WASM core exports via `fast_nav_preset()`
32
+ * and bakes into `build_room_floor_mesh`.
33
+ */
34
+ export declare const FAST_NAV_PRESET: Readonly<MeshSettings>;
35
+ /**
36
+ * Default vertical tolerance (meters) within which a floor corner height is snapped
37
+ * to the dominant floor plane, so a flat floor triangulates as a single flat surface
38
+ * instead of a noisy set of stepped quads that Recast fragments into islands.
39
+ */
40
+ export declare const DEFAULT_FLOOR_FLATTEN_TOLERANCE = 0.12;
41
+ /** Default ceiling on total navmesh voxel columns when auto-sizing the cell size. */
42
+ export declare const DEFAULT_MAX_NAV_CELLS = 1000000;
43
+ /**
44
+ * Auto-size the Recast cell size (`cs`) for a floor/collider mesh of the given
45
+ * horizontal extent.
46
+ *
47
+ * Follows the standard Recast guideline that `cs` belongs in
48
+ * `[agentRadius / 3, agentRadius / 2]`, and within that window picks the FINEST
49
+ * cell size whose grid (`width/cs * depth/cs`) still fits under `maxCells`. This
50
+ * keeps coverage of a large scene complete (the grid is bounded by a cell budget
51
+ * instead of a fixed small `cs` that either truncates the area or explodes the
52
+ * voxel count) while never going finer/coarser than the agent radius warrants.
53
+ *
54
+ * `agentRadiusM` is the agent radius in metres; with the gaming-standard 0.5 m
55
+ * agent this yields `cs` in `[0.167, 0.25]`.
56
+ */
57
+ export declare function autoNavCellSize(widthM: number, depthM: number, agentRadiusM: number, maxCells?: number): number;
58
+ /** Why floor-field extraction failed (used to drive adaptive recovery). */
59
+ export type FastNavFloorReason = 'no_component' | 'too_small' | 'empty_mesh';
60
+ /** Diagnostic payload attached to a {@link FastNavFloorError}. */
61
+ export interface FastNavFloorDiagnostics {
62
+ /** Largest usable floor area found, in square meters (if known). */
63
+ readonly area?: number;
64
+ /** Number of connected floor components considered. */
65
+ readonly components?: number;
66
+ /** Per-state cell counts from the walkable ground field. */
67
+ readonly stateCounts?: Record<string, number>;
68
+ }
69
+ /**
70
+ * Typed error thrown by {@link buildFastFloorMesh} when the floor field does not
71
+ * yield a usable room floor. The {@link reason} lets the recovery loop decide
72
+ * whether to escalate extraction parameters and retry.
73
+ */
74
+ export declare class FastNavFloorError extends Error {
75
+ readonly reason: FastNavFloorReason;
76
+ readonly area?: number;
77
+ readonly components?: number;
78
+ readonly stateCounts?: Record<string, number>;
79
+ constructor(reason: FastNavFloorReason, message: string, diagnostics?: FastNavFloorDiagnostics);
80
+ }
81
+ /** A single attempt in the adaptive floor-field recovery ladder. */
82
+ export interface FastNavRecoveryStep {
83
+ /** Human-readable label surfaced in the logs (e.g. `relaxed`, `coarse`). */
84
+ readonly label: string;
85
+ /** Partial {@link MeshSettings} merged over the base fast-field settings for this attempt. */
86
+ readonly settings: Partial<MeshSettings>;
87
+ /** Minimum accepted floor area (m^2) for this attempt to count as success. */
88
+ readonly minRoomFloorArea: number;
89
+ /** Optional per-step override for stray-floater trimming (see {@link trimStrayFloorCells}). */
90
+ readonly strayTrim?: StrayTrimOptions;
91
+ }
92
+ /** Configurable, ordered floor-field recovery ladder. */
93
+ export interface FastNavRecoveryConfig {
94
+ /** Attempts tried in order; the first one that yields a usable floor wins. */
95
+ readonly steps: readonly FastNavRecoveryStep[];
96
+ }
97
+ /**
98
+ * Default, built-in recovery ladder. It first escalates extraction parameters
99
+ * (coarser cells, lower density threshold, higher variance tolerance, higher
100
+ * voxel target, lower confidence) and only relaxes the room-area gate on later
101
+ * steps as a last resort. Integrators can override any/all of this.
102
+ */
103
+ export declare const DEFAULT_FAST_NAV_RECOVERY: FastNavRecoveryConfig;
104
+ /**
105
+ * Resolve a (possibly partial/omitted) recovery config to a concrete one,
106
+ * falling back to {@link DEFAULT_FAST_NAV_RECOVERY} when no steps are supplied.
107
+ * This is what makes adaptive recovery on-by-default for every caller.
108
+ */
109
+ export declare function resolveRecovery(partial?: Partial<FastNavRecoveryConfig>): FastNavRecoveryConfig;
110
+ /**
111
+ * Tuning for {@link trimStrayFloorCells}. All optional; sensible defaults make it
112
+ * a no-op on clean scenes and only trim a small number of peripheral strays.
113
+ */
114
+ export interface StrayTrimOptions {
115
+ /** Master switch. Defaults to `true` (built-in, on by default). */
116
+ readonly enabled?: boolean;
117
+ /**
118
+ * Max vertical distance (meters) a cell may sit from the median floor height to
119
+ * still count as real floor. Cells beyond this are treated as stray floaters.
120
+ * Defaults to `0.5`.
121
+ */
122
+ readonly heightTolerance?: number;
123
+ /**
124
+ * Safety cap: if trimming would drop more than this fraction of the floor, the
125
+ * spread is considered structural (e.g. a genuine multi-level floor) and nothing
126
+ * is trimmed. Defaults to `0.3` ("small numbers" of strays only).
127
+ */
128
+ readonly maxStrayFraction?: number;
129
+ /** Never trim the floor below this many cells. Defaults to `16`. */
130
+ readonly minKeepCells?: number;
131
+ }
132
+ /** Result of {@link trimStrayFloorCells}. */
133
+ export interface StrayTrimResult {
134
+ /** The retained floor cell indices (the dense, contiguous core). */
135
+ readonly cells: number[];
136
+ /** Cells dropped because their height was a floater-like outlier. */
137
+ readonly droppedHeightOutliers: number;
138
+ /** Cells dropped because they were spatially-isolated peripheral specks. */
139
+ readonly droppedPeripheral: number;
140
+ /** Median floor height used as the reference plane. */
141
+ readonly medianHeight: number;
142
+ /** Whether any cells were dropped. */
143
+ readonly changed: boolean;
144
+ }
145
+ /**
146
+ * Ignore a small number of stray peripheral splats/cells in a detected floor.
147
+ *
148
+ * Large, floater-heavy scans leave scattered cells at outlier heights inside an
149
+ * otherwise-flat floor component; triangulating them creates vertical cliffs that
150
+ * Recast splits into tiny fragments. This helper drops height outliers (relative
151
+ * to the median floor plane) and any spatially-isolated specks, keeping the
152
+ * largest contiguous core. It is deliberately conservative: if the would-be
153
+ * removals exceed `maxStrayFraction`, the spread is treated as structural and the
154
+ * input is returned unchanged, so clean and legitimately multi-level floors are
155
+ * untouched.
156
+ */
157
+ export declare function trimStrayFloorCells(field: WalkableGroundFieldResult, cells: number[], options?: StrayTrimOptions): StrayTrimResult;
158
+ /** Tuning for {@link estimateDenseFloorSeed}. */
159
+ export interface DenseSeedOptions {
160
+ /** Master switch. Defaults to `true` (built-in, on by default). */
161
+ readonly enabled?: boolean;
162
+ /** Height histogram bin size (meters) used to find the dense floor band. Defaults to `0.25`. */
163
+ readonly heightBin?: number;
164
+ /**
165
+ * Keep only cells at/above this density percentile when locating the dense
166
+ * floor (0..1). Higher = stricter, ignores more sparse strays. Defaults to `0.6`.
167
+ */
168
+ readonly densityPercentile?: number;
169
+ /**
170
+ * Minimum horizontal/vertical move (meters) from the current seed before a
171
+ * re-seed + field rebuild is worthwhile. Defaults to `2.0`.
172
+ */
173
+ readonly reseedThreshold?: number;
174
+ }
175
+ /**
176
+ * Estimate a collision seed located in the DENSE floor area of the scene, so the
177
+ * pipeline anchors on the real room floor instead of a sparse floater plane below
178
+ * it (the common failure on large, floater-heavy scans). Returns `fallbackSeed`
179
+ * when there isn't enough signal to be confident.
180
+ */
181
+ export declare function estimateDenseFloorSeed(field: WalkableGroundFieldResult, fallbackSeed: number[], options?: DenseSeedOptions): number[];
182
+ /**
183
+ * Estimate an adapted default region (oriented-space AABB) around the dense floor:
184
+ * generous in XZ (covers the whole floor band) but tightly clamped in Y to the
185
+ * floor plus walkable headroom. This excludes deep stray floaters below/above the
186
+ * real floor so floor detection is no longer dragged off the dense area. Returns
187
+ * `null` when there isn't enough signal.
188
+ */
189
+ export declare function estimateDenseFloorRegion(field: WalkableGroundFieldResult, options?: DenseSeedOptions): {
190
+ min: number[];
191
+ max: number[];
192
+ } | null;
193
+ export interface FastFloorMesh {
194
+ positions: Float32Array;
195
+ indices: Uint32Array;
196
+ selectedCellCount: number;
197
+ acceptedCellCount: number;
198
+ obstacleCellCount: number;
199
+ rejectedCellCount: number;
200
+ selectedArea: number;
201
+ centroid: [number, number, number];
202
+ componentCount: number;
203
+ fallbackUsed: boolean;
204
+ seedDistance: number;
205
+ /** Number of enclosed void/low-confidence cells bridged back into the floor. */
206
+ bridgedCellCount: number;
207
+ }
208
+ /**
209
+ * Build a planar floor mesh from a walkable ground field by selecting the best
210
+ * connected floor component near the seed. Throws a typed {@link FastNavFloorError}
211
+ * when no usable floor of at least `minRoomFloorArea` square meters is found, so
212
+ * callers can escalate extraction parameters and retry. A small number of stray
213
+ * peripheral cells are ignored via {@link trimStrayFloorCells} (configurable).
214
+ */
215
+ export declare function buildFastFloorMesh(field: WalkableGroundFieldResult, seed: number[] | null, minRoomFloorArea: number, log: FastNavLogger, _strayTrim?: StrayTrimOptions, floorFlattenTolerance?: number): FastFloorMesh;
216
+ /** Builds a walkable ground field from splat bytes + settings (the WASM core call). */
217
+ export type WalkableGroundFieldBuilder = (bytes: Uint8Array, settings: MeshSettings) => Promise<WalkableGroundFieldResult>;
218
+ /** Arguments for {@link extractFloorFieldWithRecovery}. */
219
+ export interface ExtractFloorFieldArgs {
220
+ /** Raw, already-decompressed splat bytes. */
221
+ readonly bytes: Uint8Array;
222
+ /**
223
+ * Field builder used for each attempt. Inject the WASM core's
224
+ * `build_walkable_ground_field` (via the bridge) so this module stays
225
+ * framework-agnostic and does not import the bridge/worker itself.
226
+ */
227
+ readonly buildField: WalkableGroundFieldBuilder;
228
+ /** Base fast-field {@link MeshSettings} that each recovery step is merged over. */
229
+ readonly baseSettings: MeshSettings;
230
+ /** Carve seed in oriented space; snapped to the detected floor plane per attempt. */
231
+ readonly seed: number[];
232
+ /** Resolved recovery ladder (use {@link resolveRecovery} to fill defaults). */
233
+ readonly recovery: FastNavRecoveryConfig;
234
+ /** Default stray-floater trimming for steps that don't specify their own. */
235
+ readonly strayTrim?: StrayTrimOptions;
236
+ /** Density-aware re-seeding to anchor on the dense floor (on by default). */
237
+ readonly denseSeed?: DenseSeedOptions;
238
+ /** Progress sink. */
239
+ readonly log: FastNavLogger;
240
+ }
241
+ /** Successful result of {@link extractFloorFieldWithRecovery}. */
242
+ export interface ExtractFloorFieldResult {
243
+ readonly field: WalkableGroundFieldResult;
244
+ readonly floorMesh: FastFloorMesh;
245
+ /** Seed snapped to the floor plane of the winning attempt. */
246
+ readonly effectiveSeed: number[];
247
+ /** Label of the recovery step that produced the floor. */
248
+ readonly stepLabel: string;
249
+ }
250
+ /**
251
+ * Run floor-field extraction with the built-in adaptive recovery ladder: for each
252
+ * step, merge its settings over `baseSettings`, build the walkable ground field,
253
+ * snap the seed to the detected floor plane, then build the floor mesh with that
254
+ * step's `minRoomFloorArea`. On a {@link FastNavFloorError} (including an empty
255
+ * mesh) it logs and escalates to the next step. The first success wins; if every
256
+ * step fails it throws an aggregated {@link FastNavFloorError}.
257
+ */
258
+ export declare function extractFloorFieldWithRecovery(args: ExtractFloorFieldArgs): Promise<ExtractFloorFieldResult>;