import { RefObject } from 'react'; /** * How expensive the thing you are about to mount is. * * The heavier it is, the more free frame time the page has to show before it * is worth starting. Shared by {@link useSafeToMount} and `useSceneGate`. */ type MountCost = "light" | "normal" | "heavy"; /** * The thresholds each cost maps to. * * These replaced three separate numbers — minimum headroom, consecutive clean * frames, and a CPU core floor. Every one of them was a question a developer * had no way to answer, and the catalogue proved it: of ten call sites, six * passed the identical override and four passed nothing. That is not tuning, * it is people working around a default. "How expensive is this?" is a * question the person writing the component can actually answer. */ interface MountCostThresholds { /** Spare frame time a frame must show to count as clean, in ms. */ readonly headroomMs: number; /** How many clean frames in a row before the gate opens. */ readonly cleanFrames: number; /** Below this core count the gate never opens. */ readonly minCores: number; } /** * What each cost resolves to. Frozen, and readable for the same reason * `POINTER_INTENT_SENSITIVITY` is: a devtools panel or a docs demo * explaining why a gate is still closed needs the actual numbers, and the * alternative is every such surface keeping a copy that drifts. */ declare const SAFE_TO_MOUNT_COST: Readonly>; interface UseSafeToMountOptions { /** * How expensive the thing being mounted is. Default `"normal"`. * * - `"light"` — a small canvas, a handful of animated elements * - `"normal"` — most components * - `"heavy"` — a full 3D scene, post-processing, a large particle system */ cost?: MountCost; } /** * Returns `true` once the page has enough spare frame time to absorb an * expensive mount. * * ```tsx * const ready = useSafeToMount({ cost: "heavy" }); * return ready ? : ; * ``` * * It starts `false` and only ever flips to `true`. Something that unmounted * itself the moment it made the page slow would oscillate forever, so the gate * is one-way. * * On a machine with too few CPU cores for the given cost it stays `false` and * stops watching. Cores cannot improve while the page is open, so that is the * one condition that ends the story early. */ declare function useSafeToMount({ cost, }?: UseSafeToMountOptions): boolean; /** Where a heavy scene is in its life. */ type SceneState = /** Too far from the viewport to be worth existing. */ "dormant" /** Close enough to matter, waiting for the page to be able to afford it. */ | "warming" /** Running, full quality. */ | "active" /** Running, reduced quality — the device or the frame rate cannot take more. */ | "constrained" /** * Started, currently off screen. Still alive, drawing nothing. * * Scrolling past a scene must not destroy it. Rebuilding a WebGL context and * re-uploading its textures costs far more than leaving it mounted and * paused, so once a scene has started it never returns to `dormant`. */ | "idle" /** * The graphics context was taken away and a replacement is being built. * * Still `mounted` — the rebuild happens by remounting the canvas under a new * `generation`, so hiding it here would prevent the very thing that fixes it. */ | "recovering" /** Not running at all. Show a still image instead. */ | "poster"; interface UseSceneGateOptions { /** Shown in devtools. No effect on behaviour. */ label?: string; /** How expensive the scene is. Default `"heavy"`. */ cost?: MountCost; /** How far before the viewport to start warming, in px. Default 200. */ preload?: number; } interface SceneGate { /** Attach to the element that holds the scene. */ ref: RefObject; state: SceneState; /** * Should the scene exist? True for `active`, `constrained` and `idle` — * `idle` included, because a scene that is merely off screen should be * paused, not torn down. */ mounted: boolean; /** How much scene to build. Only meaningful while `mounted`. */ quality: "full" | "reduced"; /** * Put this on the ``. It changes when the graphics context is * lost, which is what makes React throw away the dead tree and build a * working one — the rebuild React is already good at. */ generation: number; /** * Why it is in this state, in a sentence. Written for a support thread and * a devtools row, not for a machine — do not branch on it. */ reason: string; } /** * Decide whether a heavy scene should exist, and how much of it. * * ```tsx * const scene = useSceneGate({ label: "hero" }); * * return ( *
* {scene.mounted ? ( * * * * ) : ( * * )} *
* ); * ``` * * It answers three questions the page cannot answer for itself: is this scene * close enough to matter, can the page afford to start it, and once running, * is anything going wrong that reducing quality would actually fix. * * That last word matters. **Reduced quality is only a fix for rendering being * slow.** If a third-party script is blocking the main thread, halving the * particle count makes the page uglier and just as slow — so this gate reads * the pressure classifier and only responds to `"render"`. Nothing else * degrades the scene. * * ## What it does not do * * It decides; it does not apply. Nothing here touches a renderer, a device * pixel ratio or a post-processing pass — you own what `"full"` and * `"reduced"` look like. The renderer adapter that applies these decisions to * a real three.js scene is a separate piece. * * It composes `useSafeToMount`, `useAdaptiveQuality` and `useFramePressure` * rather than reimplementing any of them, and all three still work on their * own. It replaced `useLazyScene` outright: that hook carried a second, * differently-behaved answer to "is the page healthy enough to mount", which * is the one shape this codebase cannot afford to keep duplicating. */ declare function useSceneGate({ label, cost, preload, }?: UseSceneGateOptions): SceneGate; export { type MountCost as M, type SceneState as S, type UseSafeToMountOptions as U, type MountCostThresholds as a, SAFE_TO_MOUNT_COST as b, type SceneGate as c, type UseSceneGateOptions as d, useSceneGate as e, useSafeToMount as u };