@zephytiju/prism-geovision-embed
v0.2.0
Published
Platform Prism geovision-embed micro-UI (component id "geovision-embed"): the agreed dossier-embed render component as a DISPATCHER — <GeovisionEmbed sourceFileRef viewId mode? interactionMode? engine? locale? width height /> resolves the geovision-view f
Readme
@zephytiju/prism-geovision-embed
Platform Prism geovision-embed micro-UI (component id "geovision-embed") — the render-slot
component of the CLOSED dossier-embed contract (GeoVision ↔ Dossier Editor coordination,
2026-09-22; the agreed channel summary lives in the GeoVision wiki docs below). The editor's chrome
(title / SRC FILE / VIEW ▾ / ↗) is the EDITOR's; this package owns ONLY the slot, and every internal
control (compact layer rail, video-like replay, …) stays inside it — nothing an embed does
navigates away.
The embed is a DISPATCHER: one invocation renders exactly ONE variant — never a stacked
dashboard. The saved view's previewKind selects it:
| previewKind | Renders | Chrome |
| --- | --- | --- |
| map | the Cesium engine map (default, engine="cesium"): the same zoom/rotate/tilt/pan camera as the globe viewport on an ION-FREE CesiumJS/Resium viewer — an SVG snapshot placeholder crossfading into the lazily-mounted engine with the video-like replay control on top | replay control only — no tools, no rail, no floatboxes, no fusion board, no view-mode cycling, no SAVE |
| map (engine="svg") | the lighter static SVG map (markers / heat / side coloring + video-like replay) | compact layer rail + replay control |
| relational | @zephytiju/prism-relation-fluxboard | none — rendered as-is |
| timeline | @zephytiju/prism-timeline-board | none — rendered as-is |
| stats | @zephytiju/prism-stats-panel | none — rendered as-is |
Whatever renders fills the slot's full width/height (a scoped fit rule widens the packages'
intrinsic standalone widths to the slot); unknown or missing previewKind renders the localized
UNSUPPORTED PREVIEW KIND frame.
The contract (implemented exactly)
<GeovisionEmbed sourceFileRef viewId mode? interactionMode? engine? resolutionScale? locale? width height />| Prop | Type | Default | Contract |
| --- | --- | --- | --- |
| sourceFileRef | string | — | stable ref addressing the saved geovision-view file (IFileEntry.read) |
| viewId | string | — | the saved view to render — ${sourceFileRef}#${previewKind}, exactly as describeViews lists it; a viewId addressing another file renders UNKNOWN VIEW, and one with an unknown/missing previewKind renders UNSUPPORTED PREVIEW KIND |
| mode | "static" \| "live" | "static" | v1: live is reserved and behaves as static (no live fetches) |
| interactionMode | "rich" \| "static" | "rich" | rich captures pointer events inside the slot for inline interactions; static renders a static frame (also forced below 240 px height) |
| engine | "cesium" \| "svg" | "cesium" | the MAP variant's render engine: the CesiumJS engine embed (same zoom/rotate as the globe) or the lighter static SVG variant. Ignored by the other preview kinds; the degradation paths always fall back to the SVG snapshot |
| resolutionScale | number | 0.75 | the Cesium engine's render resolution scale — the embed's GPU/retina footprint cap. Ignored by the "svg" engine |
| locale | "en" \| "zh-CN" | "en" | the embed renders in the editor's active locale (unknown locales fall back to en) |
| width / height | number \| string | "100%" / "100%" | slot size; below 240 px height the embed degrades to a static frame with no pointer capture |
GeovisionEmbed.describeViews(sourceFileRef) feeds the editor's VIEW ▾ dropdown: it reads +
validates the file and returns { viewId, name, previewKind }[] — ONE view per previewKind
(map / relational / timeline / stats), viewId = ${fileRef}#${previewKind} (the file
format is closed and carries no extension fields, so the per-kind views are addressed by
convention over the stable file ref; the name derives from the recorded provenance because the
format carries no name field). Fails closed to an empty list for unreadable or invalid files.
Agreed coordination record: task list https://applink.feishu.cn/client/todo/task_list?guid=33ec590f-7c59-4ace-b069-53c649fc55ab and the authoritative designs:
- https://qcnwge0wy4s0.feishu.cn/wiki/IZ55wbfHPiqag5kT5eGckDlJnXf
- https://qcnwge0wy4s0.feishu.cn/wiki/DZ78w8NneizBNhkhuE2cCyp5nRb
- https://qcnwge0wy4s0.feishu.cn/wiki/DOljwdYuRib4uIkbRaGcu1pNnSc
How it renders
- Resolve
{ sourceFileRef, viewId }viacreateFileEntryClient(useLatticeTransport()).read— the HOST supplies the transport; the embed installs NO executor of its own, so in static mode no live fetches occur beyond the host's own routes. - Validate fail-closed through the authoritative format
(
@zephytiju/lattice-common-bundle/geovisionView): structure AND digest. Failures render localized frames (not-found/invalid/unknown-view/unknown-preview). - Seed an isolated
<ChannelScope>—query-box.entity-resultscarries the snapshot projection,search-results.selected-entitythe workspace's selected entity — so every embed renders its own file and nothing leaks across embeds on the same page (the 2×2 demo grid runs four of them side by side). - Dispatch on the view's
previewKind— exactly ONE of:- map, Cesium engine (default) — the SAME engine/zoom/rotate as the globe viewport
(
cesium1.143.0 +resium1.24.0, identical to@zephytiju/prism-globe-viewport), wrapped in an EMBED-BUDGET architecture:- snapshot placeholder — the existing SVG mini-map renders from the same snapshot data the moment the embed mounts (zero Cesium cost — the instant visual; see the performance strategy below for the lazy handover);
- lazy mount — an
IntersectionObserveron the embed container boots the CesiumVieweronly while the slot is on screen (deferred off the observer's own path, so the WebGL boot never blocks the interaction that revealed it); once the viewer element exists the layers crossfade (SVG out, canvas in). Scrolling the slot out of view — or unmounting — unmounts the Resium<Viewer>, whose teardown destroys the WebGL context/workers; re-entry re-observes and boots again; - idle GPU —
requestRenderMode: true+maximumRenderTimeChange: Infinitykeep the render loop asleep when nothing changes (Cesium's camera input handlers request renders themselves on zoom/rotate/tilt/pan, which stay fully enabled — theScreenSpaceCameraControlleris untouched);resolutionScale0.75 (the embed prop) caps the raster footprint,scene.globe.tileCacheSize10 bounds the tile memory, every UI widget is disabled (animation/timeline/baseLayerPicker/geocoder/homeButton/ sceneModePicker/navigationHelpButton/fullscreenButton/infoBox/selectionIndicator) and the sky is off (skyBox: false,skyAtmosphere: false). CesiumJS 1.143 carries nocreditsDisplayconstructor option, so the credit strip (the only chrome Cesium still paints with everything else disabled) is suppressed by a rule scoped to the embed's stage; the imagery credit stays attached to the provider; - ion-free imagery + camera restore — the saved view's
baseLayer.urlTemplate(with its credit) feeds aUrlTemplateImageryProvider, falling back to theOpenStreetMapImageryProviderdefault — never a Cesium ion asset; terrain is the default ellipsoid. The camera is restored from the saved view'scamerastate{lon, lat, height}— the sameIGeoViewserialization the globe viewport saves; - markers + replay — entity markers derive from the snapshot (the same side-color
derivation the SVG mini-map paints: token-colored points + mono labels, the workspace's
selected entity emphasized). The video-like replay control overlays the canvas; each
scrub tick re-derives the marker positions with the SAME track math as the SVG variant
(linear interpolation between deterministic waypoints) and explicitly calls
scene.requestRender()— underrequestRenderModea scrub tick renders exactly one frame; - no other chrome — no tools, no layer rail, no floatboxes, no fusion board, no view-mode cycling, no SAVE: just basemap + markers + replay;
- map, SVG engine (
engine="svg") — the lighter static variant hosts opt into: the static equirectangular SVG mini-map filling the slot with- layers from the file — normal markers (overlay), kernel-density blobs per enabled heat
layer (simplified variant of the globe's heat kernel), side-color mapping applied to marker
fills/outlines (
solid/stripe/accent). Tag matchers apply to the item's streamtype(the kinds≡tags convention the recorded behavior uses for heat selections) because the format'sEntityResultItemdeclares onlyid/type/label/lat/lon; - the compact layer rail — the same click/long-press contract as the globe's rail at compact density (click toggles, long-press/right-click opens the anchored opacity options; changes are local to the surface, persisted only via the editor's block config);
- the VIDEO-LIKE replay control — play/pause + progress scrubbing entity positions across the snapshot's time-windowed timeline events (tracks interpolate linearly by time between deterministic waypoints derived from the events; display-only);
- layers from the file — normal markers (overlay), kernel-density blobs per enabled heat
layer (simplified variant of the globe's heat kernel), side-color mapping applied to marker
fills/outlines (
- relational / timeline / stats — the REAL packages mounted inside the scope, rendered as-is (their internal fetches flow through the host transport; in the demo the mock executor answers from the file's snapshot).
- map, Cesium engine (default) — the SAME engine/zoom/rotate as the globe viewport
(
Degradation path
engine="svg", interactionMode="static" and slots below MIN_RICH_HEIGHT (240 px) NEVER
mount Cesium: the map variant renders the SVG snapshot only (the SVG mini-map plus, on the
rich-SVG path, its rail/replay chrome), and static frames additionally capture no pointer events.
Off-screen rich Cesium embeds hold no GPU resources either — the observer gate is the second
half of the degradation story (visible ⇒ engine, invisible ⇒ destroyed).
Performance strategy (the Cesium engine in one paragraph)
Instant SVG from the snapshot (no engine cost at first paint) → the engine boots only when the
slot is actually visible (IntersectionObserver, boot deferred to an idle callback) → once booted
the render loop sleeps (requestRenderMode + maximumRenderTimeChange: Infinity; Cesium's own
camera handlers wake it for zoom/rotate/tilt/pan) → replay ticks and marker moves render exactly
one explicit scene.requestRender() frame each → the raster is capped (resolutionScale 0.75,
configurable) and tile memory bounded (tileCacheSize 10) → leaving the viewport destroys the
viewer outright (WebGL context + workers freed), and re-entry rebuilds it.
Peer dependencies / registry status
The three mounted component packages are REGISTRY PEERS (>=0.1.0) and are published on npm;
the dev surface pins the same registry specifiers as devDependencies (no vendored tarballs
since 0.2.0):
.npmrckeepslegacy-peer-deps=trueso npm never auto-installs the peers on top of the explicit dev pins;- typecheck/tests/demo run against the REAL packaged output (dist) of the siblings, exactly what
the registry serves — the jsdom run inlines them and stubs the
@xyflow/reactstylesheet (vitest.config.ts).
The Cesium engine comes from the REGISTRY as regular dependencies — cesium 1.143.0 +
resium 1.24.0 (the same versions and @cesium/* overrides as
@zephytiju/prism-globe-viewport), so hosts install the identical engine the globe runs. The
demo's vite config serves Cesium's static asset tree (Workers/Assets/ThirdParty/Widgets)
straight from node_modules via publicDir with CESIUM_BASE_URL="/" — nothing is bundled or
vendored, and no ion endpoint is configured anywhere.
Per the owner gate this package is NOT published or tagged from this work (the npm 0.1.0 is the
superseded stacked-dashboard build; 0.2.0 is the dispatcher fix, published only after owner
confirmation).
Development
npm install # registry peers pinned as devDependencies (see .npmrc) + cesium/resium
npm run typecheck # tsc (against the REAL cesium/resium types)
npm test # vitest (jsdom) — 56 tests; cesium/resium stubbed at the resolver level
npm run dev # vite demo (2×2 grid: map on the Cesium engine + relational/timeline/stats,
# plus the engine="svg" fallback panel; EN/中文, dark × light)
npm run build # emits dist/ with registry peer imports
npm run shot-demo # captures /tmp/guanlan-review/demo-geovision-embed-v3.pngTests follow the globe viewport's mocking strategy: jsdom cannot host the ~30MB WebGL
engine, so vitest.config.ts aliases cesium/resium to test/stubs/ — plain surface stubs
that record the Viewer constructor props, camera.setView calls, scene.requestRender counts,
the globe.tileCacheSize assignment and viewer destroys. The library build (tsc) still
typechecks against the real packages. test/setup.ts additionally installs an always-visible
IntersectionObserver default; the cesium-engine suite installs its own controllable mock to
drive enter/exit visibility transitions.
Source layout (each module ≤250 lines): src/GeovisionEmbed.tsx (contract + failure frames),
src/resolveView.ts (IFileEntry.read + fail-closed validation + previewKind resolution),
src/views.ts (view-id addressing: viewIdFor / previewKindOf / resolveViewKind),
src/seed.ts (ChannelScope seeding), src/EmbedBody.tsx (the DISPATCHER — one variant per
invocation), src/map/CesiumMapVariant.tsx (the map variant's engine dispatch: SVG-only
degradation paths + the Cesium engine surface — observer gate, ready-gate crossfade, replay
wiring), src/map/CesiumMarkersLayer.tsx (the Resium entity layer),
src/map/cesiumModel.ts (pure marker/camera/imagery derivations),
src/map/MapVariant.tsx (the SVG variant + its chrome), src/map/railState.ts (shared
initial-rail derivation), src/describeViews.ts, src/map/ (projection, replay math, heat
blobs, side coloring, tokens, MiniMap, ReplayControl), src/rail/LayerRailCompact.tsx,
src/locales/ (en + zh-CN with exact key parity; stringsForLocale falls back to en).
