@splnlss/3d-story-viewer
v0.8.6
Published
A browser-based 3D map and story viewer built with Vite, Svelte, Three.js, and `3d-tiles-renderer`.
Readme
3D Story Viewer
A browser-based 3D map and story viewer built with Vite, Svelte, Three.js, and 3d-tiles-renderer.
The package exports:
Viewer: the main 3D viewer classViewerEvents: viewer lifecycle and transition event namescomputeElevation: helper for fetching elevation data
Requirements
- Node.js
- Yarn
- A browser with WebGL support
Install
yarn installRun Locally
yarn devThe dev server is configured by Vite to serve the app from src/ over HTTPS. Open the local URL printed by Vite.
Build
yarn buildBuild output is written to build/. The package entry points are configured in package.json:
@splnlss/3d-story-viewer->build/viewer.js@splnlss/3d-story-viewer/computeElevation->build/computeElevation.js
Preview Build
yarn previewBasic Usage
import { Viewer } from '@splnlss/3d-story-viewer';
const container = document.getElementById('app');
const viewer = new Viewer(container, {
cesiumToken: 'your-cesium-token',
googleMapsToken: 'your-google-maps-token',
});
viewer.loadEnv('/assets/viewer/neutral.hdr');
viewer.defineContentLayers([
{
id: 'example-layer',
name: 'Example Layer',
type: '3d-tiles',
labels: ['default'],
data: {
url: 'https://example.com/tileset.json',
},
options: {
renderOrder: 1,
},
},
]);
viewer.enableContentLayers(['default']);
viewer.alignCamera(-105.18896, 39.93, 9000);Geospatial Precision Contract
Longitude and latitude are WGS84 decimal degrees. Altitude is WGS84 ellipsoidal height in meters. Caller-supplied positions are accepted from -12,000 through 100,000 meters. Internal conversions do not clamp or range-limit derived heights, so orbiting above 100 km and changing view mode remain valid. ECEF positions and local east-north-up (ENU) translations are meters; camera pitch, yaw, and roll and local-model XYZ rotations are degrees. Local scale is positive and dimensionless. 3D Tiles and mesh longitude/latitude offsets are degrees, and their altitude offsets are ellipsoidal meters. Missing offset members default independently to zero, while an explicit zero is preserved.
Nonzero mesh offsets require options.offsetOrigin, an explicit WGS84
{ longitude, latitude, altitude } reference point for calculating the ECEF
translation. The GLB root is never assumed to be ECEF. The offset translates
the existing geometry; it does not georeference a local model. A missing or
invalid origin emits CONTENT_LAYER_ERROR and leaves the loaded mesh attached
without the offset.
Ungeoreferenced GLB models can instead use options.transform. Its anchor
is WGS84 longitude/latitude in decimal degrees and ellipsoidal altitude in
meters. translationENU is east/north/up in meters, rotationDegrees is a
model-local XYZ Euler rotation in degrees, and scale is positive and
dimensionless. The default [90, 0, 0] rotation maps glTF +Y Up to ENU +Up;
set an explicit rotation when the model's authored axes differ. A local
transform cannot be combined with legacy offset or offsetOrigin. Existing
ECEF GLBs without a transform keep their coordinates.
All accepted geospatial and camera values must be finite. Labels require
longitude, latitude, and altitude on every update. At an exact pole, ECEF to
geodetic conversion returns longitude 0 degrees because longitude is
mathematically undefined there. ECEF (0, 0, 0) has no geodetic solution and
is rejected.
Within this declared domain, the viewer adds less than 2 cm of numerical displacement. WGS84 reference conversions and round trips are bounded to 1 mm; centimeter ENU changes and locally rebased GeoJSON buffers through a 50 km feature extent are bounded to 5 mm. This numerical guarantee does not certify source survey accuracy, terrain geometry, asset georeferencing, vertical datum conversion, or screen-pixel measurement.
Migrating From 0.5.x
Version 0.6.0 requires all three label and GeoJSON coordinates. Camera targets,
labels, and Gaussian anchors reject non-finite or out-of-range inputs.
GeoJSON features exceeding 50 km from their local origin are rejected.
Update callers that relied on missing coordinates defaulting to zero, and
provide offsetOrigin for nonzero mesh offsets. Perimeters support Polygon
outer rings only; interior rings and MultiPolygon geometry remain outside this
precision change.
Gaussian Splat Layers
Gaussian splat content layers support ply, sog, spz, and rad data. Every
layer uses an explicit WGS84 anchor plus an optional local ENU transform:
viewer.defineContentLayers([
{
id: 'site-scan',
name: 'Site scan',
type: 'gaussian-splat',
labels: ['default'],
data: { url: 'https://assets.example.com/site.rad', format: 'rad' },
options: {
transform: {
anchor: { longitude: -105.1, latitude: 40.1, altitude: 1600 },
translationENU: [0, 0, 0],
rotationDegrees: [0, 0, 0],
scale: 1,
},
},
},
]);PLY, SOG, and SPZ files stream directly and report byte progress. RAD uses
Spark's paged loader: becoming ready means its metadata is valid and LOD
streaming can begin, while detail continues to load in response to the camera.
RAD hosts must support HTTP byte ranges and cross-origin GET/HEAD requests.
Expose Content-Length, Content-Range, and Accept-Ranges; relative RAD
sidecar files must use the same policy.
Local GLB Placement and Targeted Updates
const localBarn = {
id: 'barn', name: 'Barn', type: 'mesh', labels: ['barn'],
data: { url: 'https://assets.example.com/barn.glb' },
options: {
transform: {
anchor: { longitude: -105.2, latitude: 39.96, altitude: 1700 },
translationENU: [0, 0, 0], rotationDegrees: [90, 0, 0], scale: 1,
},
},
};
viewer.replaceContentLayer(localBarn.id, localBarn); // add or replace one layer
viewer.replaceContentLayer(localBarn.id, null); // remove one layerreplaceContentLayer preserves unrelated layer objects and resources. It
reports an invalid layer definition through CONTENT_LAYER_ERROR; an invalid
ID or a mismatched replacement ID throws synchronously. A failed replacement
leaves the previous layer in place. Existing defineContentLayers calls still
replace the full set while retaining the current enabled-label selection.
Migrating From 0.8.4
Version 0.8.5 adds optional local GLB placement and targeted content replacement. Existing mesh and 3D Tiles definitions keep their 0.8.4 coordinates and offsets. Callers adding local GLBs should supply a WGS84 anchor and review the glTF Y-up default in the scene. Transformed local GLBs render in the terrain depth pass with their authored glTF materials and the viewer's shared Draco/KTX2 decoders. Their render order is fixed to 0 so a catalog overlay order cannot make them show through terrain. GLBs without a transform retain the legacy layer-6 patch rendering and coordinate behavior.
Environment Variables
Viewer tokens can be passed directly in the constructor or provided through Vite environment variables:
VITE_CESIUM_TOKEN=...
VITE_GOOGLE_MAPS_TOKEN=...Demo App
src/index.html contains a simple demo that:
- creates a
Viewer - loads
public/assets/viewer/neutral.hdr - defines layers from
utils/mapLayers.js - enables the
defaultlayer group - registers number-pad shortcuts for sample camera positions and time frames
Useful Files
src/main.js: public module exportssrc/viewer/index.js: main viewer implementationsrc/viewer/ContentLayer/: supported content layer typesutils/mapLayers.js: example layer definitionsutils/elevation/: elevation utilitiespublic/assets/viewer/: static viewer assets
Camera translation speed
viewer.movementSpeed sets camera-relative translation along the WGS84 ECEF
basis in metres per second (default 100). viewer.boostMultiplier is the
dimensionless multiplier while the configured BOOST key is held (default 5).
Both are writable number fields. Consumers should supply finite, non-negative
values. Mouse zoom/orbit and scripted camera transitions are independent.
Changing viewer.keysEnabled clears held keys and movement flags; resuming
requires a fresh key press. This lets popovers and text inputs suspend movement
without leaving the camera moving or resuming from a key typed in the control.
