Skip to content

createSceneStack

createSceneStack<F>(__namedParameters): object

Defined in: camera/scene-stack.ts:427

F

SceneStackDeps<F>

breadcrumb: () => object[]

object[]

climbToParent: () => Promise<void>

D7 — asks the source where zooming out of the current scene leads (SceneSource.parentOf, US070 / ADR-016 A24) and pushes the synthetic frame enter() would have pushed had the user arrived from above — built the same way enter() builds a real one, entryScale included. Never touches the scene or the camera itself in the ordinary case: the existing leave(), completely unmodified, is what actually swaps to the newly-pushed ancestor, on a later call once its own scale threshold is crossed (D7’s “lazy, one ancestor per zoom-out”).

  • anchored: the parent scene has an item standing for this one (anchorId), so the frame continues the footprint from that item’s box. If the anchor turns out to be missing (a data inconsistency), the camera is left untouched, enter()’s own failure guarantee.
  • unanchored: there is a parent but nothing in it to continue a footprint from, so this is a direct swap to it (the shape reset() uses), never a pushed frame.
  • none, or a rejection: nothing happens.

A no-op at any depth but 0, and at the root.

Promise<void>

enter: (anchorId, opts) => Promise<void>

approach: false is the zoom-triggered path: the gesture has already zoomed the anchor past the entry threshold, so the approach animation is skipped and the swap happens at the current camera — continuing the motion in progress rather than replacing it with a redundant one.

string

boolean

Promise<void>

enterSibling: (id, opts) => Promise<void>

Enters a sibling of the current scene — one drawn around it — via the parent: leave (at the current camera, so nothing moves) then an ordinary enter.

string

boolean

Promise<void>

enterThrough: (parent, id) => Promise<"none" | "entered" | "centred">

Double-clicking id, an item drawn as one of parent’s children in its place: enters parent where it is (nothing moves, the children already being where they will be), then the item itself when it has a scene of its own, or otherwise brings it to the middle of the view. So the scene you end in is the item’s, not whichever is left in the middle. Resolves to what happened.

PreviewParent

string

Promise<"none" | "entered" | "centred">

ghostSibling: (id) => RenderNode | null

A sibling drawn around the current scene that can be entered from here, by id.

string

RenderNode | null

isAnimating: () => boolean

boolean

isLocked: () => boolean

boolean

leave: (approach) => Promise<void>

The inverse of enter. approach: false is the zoom-out-triggered path — see scenes.ts’s spike doc comment (ported verbatim in spirit) for why the swap happens immediately rather than animating back up first.

boolean = true

Promise<void>

maybeAutoEnterOnZoom: () => void

void

maybeAutoLeaveOnZoom: () => void

void

maybeMoveAcross: () => void

Step 3 — moving sideways. Inside X, once the middle of the screen has left X’s footprint and sits on a sibling Y that is big enough to be inside, the current scene becomes Y: leave back to the parent and enter Y, both at the current camera. Each of those already keeps the picture unchanged, and Y’s children are usually already loaded (it was being revealed around X), so the move itself is invisible — only the scene the URL settles on changes.

void

refineIfCurrent: (focus, updated) => void

Exposed so the composition layer can merge a page that streamed in for the initial mount’s focus — loadSceneStreaming’s onUpdate can fire before this stack even exists yet (the stack’s own constructor needs the first-resolved scene as an argument), a race ensureChildScene’s own internal use of this never has to consider. The composition layer buffers at most the latest such update and applies it once the stack is constructed — see PixiExplorerView.tsx.

Merges a later loadSceneStreaming page into current — but only while focus’s scene is still the one on screen; if the user has since left it (drilled further, backed out, or the initial-mount race described on SceneStack.refineIfCurrent below), a late-arriving page is silently dropped rather than resurrecting a scene nobody is looking at. A renderNodes/ layout swap only — it never touches the camera, so a scene filling in behind the user never moves anything they’re looking at.

Uses layoutAppend, not toStackedScene’s ordinary layoutFor. A full relayout on every streamed page reshuffled and reshifted every already-visible dot each time (confirmed directly in a browser) — sunflowerLayoutAppend’s own doc comment has the reasoning. Streaming only ever targets a packed (non-neighbourhood) scene, which never has a focusId, so the incremental append — not radialFocusLayout — is always the right one here; a fixed scene’s positions are its own, so layoutAppend simply lays it out again.

F

ExplorerScene<F>

void

refreshCurrent: () => Promise<boolean>

US043 Step 4 — replaces the current scene with a fresh composition of the same focus, for a write/invalidation-driven refresh rather than a navigation. A full relayout via toStackedScene, not refineIfCurrent’s incremental append: a write can remove membership (a collection filter dropping an item, a set no longer owned), not just add to it, and sunflowerLayoutAppend only knows how to grow a layout, never shrink or re-decorate one already placed — reusing it here would leave stale positions/marks behind for anything that changed. Positions therefore are not guaranteed stable across a refresh the way a streamed page’s are; only the camera and stack frames are preserved (swapTo never touches either).

A no-op while a gesture-driven transition is in flight (never fights the camera mid-animation; the next invalidation notification or a subsequent settle catches it up) and a no-op if the scene the caller was refreshing is no longer current by the time the loader resolves (the user navigated away during the await) — both silent, matching enter/climbToParent’s own guarantee that a failure or a lost race leaves the camera exactly where it was. Returns whether a refreshed scene was actually applied, so the caller can decide whether to re-check its own selection.

Promise<boolean>

schedulePreviews: () => void

D1.2/D1.3, widened — starts loading the children of every visible drillable node past the prefetch threshold, biggest on screen first, at most MAX_PREVIEW_LOADS uncached at once. Cheap to call every frame (the caller does, after each draw): a no-op for anything already previewing or pending. Previews still on screen are refreshed in the cache’s LRU order, and marked so the cache’s limit never evicts one (PreviewHolder.onScreen).

void

transitionView: (now) => TransitionView

The cross-fades to draw for the current camera — see transition.ts. Every visible node big enough reveals its own children behind its own cloud (under), by its own size on screen; nothing fades because a different node is being zoomed into. Having entered this scene through a parent, that parent is drawn over it (over): the node entered dissolving, and its siblings — with any children they were already revealing — exactly as they were drawn before the swap. Every quantity is a function of on-screen size, which enter/leave keep continuous, so the swap itself changes nothing on screen.

number = ...

TransitionView

get current(): StackedScene<F>

StackedScene<F>

get depth(): number

number

boxById(id): ExplorerBox | undefined

string

ExplorerBox | undefined

jumpTo(focus): Promise<void>

Reconciles the camera against a focus this stack didn’t reach by its own enter/leave — a browser Back/Forward or a pasted deep link while the surface is already mounted (plan §1 “Still open: … the URL/history model” — an exact undo-stack reconstruction from an arbitrary history entry is not attempted; this jumps straight to the target and discards the in-flight zoom-out stack, same simplification reset already makes for the root case). A no-op if focus is already current.focus — the common case, since most URL changes this surface sees are its own settle-navigate calls landing back on themselves.

F | null

Promise<void>

nodeById(id): RenderNode | undefined

string

RenderNode | undefined

reset(): Promise<void>

Unwind every frame at once (the “back to the top”/root affordance). Re-fits the camera itself, same as the spike’s reset — swapping the scene without doing so would leave the camera in the abandoned scene’s coordinate space, which usually means nothing on screen.

Promise<void>