@zendir/ui 0.2.21 → 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/CHANGELOG.md +183 -1
- package/README.md +70 -28
- package/dist/index.d.ts +1 -1
- package/dist/index.js +51 -42
- package/dist/index.js.map +1 -1
- package/dist/react/3d/CesiumCaptureSource.d.ts +1 -1
- package/dist/react/3d/CesiumCaptureSource.js +1 -1
- package/dist/react/3d/CesiumCaptureSource.js.map +1 -1
- package/dist/react/3d/ZenSpace3D.js +1253 -0
- package/dist/react/3d/ZenSpace3D.js.map +1 -0
- package/dist/react/3d/ZenSpace3DCesium.js +579 -0
- package/dist/react/3d/ZenSpace3DCesium.js.map +1 -0
- package/dist/react/3d/ZenSpace3DTypes.d.ts +28 -1
- package/dist/react/3d/ZenSpace3DUtils.d.ts +17 -173
- package/dist/react/3d/ZenSpace3DUtils.js +20 -1
- package/dist/react/3d/ZenSpace3DUtils.js.map +1 -1
- package/dist/react/3d/index.d.ts +6 -12
- package/dist/react/3d/threeLoader.js +18 -0
- package/dist/react/3d/threeLoader.js.map +1 -0
- package/dist/react/astro/MonitoringIcon.js +1 -1
- package/dist/react/astro/MonitoringIcon.js.map +1 -1
- package/dist/react/astro/SimulationControls.js +2 -2
- package/dist/react/astro/SimulationControls.js.map +1 -1
- package/dist/react/astro/UnifiedTimeline.js +4 -4
- package/dist/react/astro/UnifiedTimeline.js.map +1 -1
- package/dist/react/charts/GroundTrackMap.d.ts +2 -15
- package/dist/react/charts/GroundTrackMap.js +1 -1
- package/dist/react/charts/GroundTrackMap.js.map +1 -1
- package/dist/react/charts/unified/AstroChart.js +34 -13
- package/dist/react/charts/unified/AstroChart.js.map +1 -1
- package/dist/react/chatgpt/AppCard.d.ts +0 -4
- package/dist/react/chatgpt/index.d.ts +0 -19
- package/dist/react/context/SpatialSelectionContext.d.ts +40 -0
- package/dist/react/context/SpatialSelectionContext.js +10 -0
- package/dist/react/context/SpatialSelectionContext.js.map +1 -0
- package/dist/react/context/index.d.ts +2 -0
- package/dist/react/core/{DataTable.d.ts → data/DataTable.d.ts} +1 -1
- package/dist/react/core/{DataTable.js → data/DataTable.js} +4 -4
- package/dist/react/core/data/DataTable.js.map +1 -0
- package/dist/react/core/{DataValue.d.ts → data/DataValue.d.ts} +2 -2
- package/dist/react/core/{DataValue.js → data/DataValue.js} +2 -2
- package/dist/react/core/data/DataValue.js.map +1 -0
- package/dist/react/core/{propertyConfig.d.ts → data/propertyConfig.d.ts} +2 -2
- package/dist/react/core/data/propertyConfig.js.map +1 -0
- package/dist/react/core/{AstroIcon.js → display/AstroIcon.js} +1 -1
- package/dist/react/core/display/AstroIcon.js.map +1 -0
- package/dist/react/core/{Badge.d.ts → display/Badge.d.ts} +1 -1
- package/dist/react/core/{Badge.js → display/Badge.js} +2 -2
- package/dist/react/core/display/Badge.js.map +1 -0
- package/dist/react/core/{CardHeader.d.ts → display/CardHeader.d.ts} +1 -1
- package/dist/react/core/{CardHeader.js → display/CardHeader.js} +2 -2
- package/dist/react/core/display/CardHeader.js.map +1 -0
- package/dist/react/core/{Container.d.ts → display/Container.d.ts} +1 -1
- package/dist/react/core/{Container.js → display/Container.js} +3 -3
- package/dist/react/core/display/Container.js.map +1 -0
- package/dist/react/core/{CopyButton.js → display/CopyButton.js} +1 -1
- package/dist/react/core/display/CopyButton.js.map +1 -0
- package/dist/react/core/{GlassCard.d.ts → display/GlassCard.d.ts} +1 -1
- package/dist/react/core/{GlassCard.js → display/GlassCard.js} +2 -2
- package/dist/react/core/display/GlassCard.js.map +1 -0
- package/dist/react/core/{HeaderIconWithStatus.d.ts → display/HeaderIconWithStatus.d.ts} +1 -1
- package/dist/react/core/{HeaderIconWithStatus.js → display/HeaderIconWithStatus.js} +1 -1
- package/dist/react/core/display/HeaderIconWithStatus.js.map +1 -0
- package/dist/react/core/{Icon.d.ts → display/Icon.d.ts} +1 -1
- package/dist/react/core/{Icon.js → display/Icon.js} +1 -1
- package/dist/react/core/display/Icon.js.map +1 -0
- package/dist/react/core/{Typography.d.ts → display/Typography.d.ts} +13 -4
- package/dist/react/core/{Typography.js → display/Typography.js} +1 -1
- package/dist/react/core/display/Typography.js.map +1 -0
- package/dist/react/core/{ConfirmDialog.js → feedback/ConfirmDialog.js} +1 -1
- package/dist/react/core/feedback/ConfirmDialog.js.map +1 -0
- package/dist/react/core/{Dialog.js → feedback/Dialog.js} +2 -2
- package/dist/react/core/feedback/Dialog.js.map +1 -0
- package/dist/react/core/{Toast.js → feedback/Toast.js} +3 -3
- package/dist/react/core/feedback/Toast.js.map +1 -0
- package/dist/react/core/index.d.ts +85 -85
- package/dist/react/core/{Button.js → inputs/Button.js} +2 -2
- package/dist/react/core/inputs/Button.js.map +1 -0
- package/dist/react/core/{Checkbox.js → inputs/Checkbox.js} +2 -2
- package/dist/react/core/inputs/Checkbox.js.map +1 -0
- package/dist/react/core/{Input.d.ts → inputs/Input.d.ts} +1 -1
- package/dist/react/core/{Input.js → inputs/Input.js} +3 -3
- package/dist/react/core/inputs/Input.js.map +1 -0
- package/dist/react/core/{LimitsBar.js → inputs/LimitsBar.js} +1 -1
- package/dist/react/core/inputs/LimitsBar.js.map +1 -0
- package/dist/react/core/{NumberInput.d.ts → inputs/NumberInput.d.ts} +2 -2
- package/dist/react/core/{NumberInput.js → inputs/NumberInput.js} +3 -3
- package/dist/react/core/inputs/NumberInput.js.map +1 -0
- package/dist/react/core/{PinInput.js → inputs/PinInput.js} +2 -2
- package/dist/react/core/inputs/PinInput.js.map +1 -0
- package/dist/react/core/{Select.js → inputs/Select.js} +3 -3
- package/dist/react/core/inputs/Select.js.map +1 -0
- package/dist/react/core/{Toggle.js → inputs/Toggle.js} +2 -2
- package/dist/react/core/inputs/Toggle.js.map +1 -0
- package/dist/react/core/{AppBar.d.ts → navigation/AppBar.d.ts} +1 -1
- package/dist/react/core/{AppBar.js → navigation/AppBar.js} +7 -7
- package/dist/react/core/navigation/AppBar.js.map +1 -0
- package/dist/react/core/{Pagination.js → navigation/Pagination.js} +2 -2
- package/dist/react/core/navigation/Pagination.js.map +1 -0
- package/dist/react/core/{SideNav.d.ts → navigation/SideNav.d.ts} +1 -1
- package/dist/react/core/{SideNav.js → navigation/SideNav.js} +3 -3
- package/dist/react/core/navigation/SideNav.js.map +1 -0
- package/dist/react/core/{Tabs.js → navigation/Tabs.js} +2 -2
- package/dist/react/core/navigation/Tabs.js.map +1 -0
- package/dist/react/core/{Popover.js → overlays/Popover.js} +1 -1
- package/dist/react/core/overlays/Popover.js.map +1 -0
- package/dist/react/core/{SidePanel.js → overlays/SidePanel.js} +3 -3
- package/dist/react/core/overlays/SidePanel.js.map +1 -0
- package/dist/react/core/{Tooltip.js → overlays/Tooltip.js} +2 -2
- package/dist/react/core/overlays/Tooltip.js.map +1 -0
- package/dist/react/core/{ActivityPlanner.js → widgets/ActivityPlanner.js} +1 -1
- package/dist/react/core/widgets/ActivityPlanner.js.map +1 -0
- package/dist/react/core/{Capture.js → widgets/Capture.js} +3 -3
- package/dist/react/core/widgets/Capture.js.map +1 -0
- package/dist/react/core/{ChatPanel.d.ts → widgets/ChatPanel.d.ts} +1 -1
- package/dist/react/core/{ChatPanel.js → widgets/ChatPanel.js} +2 -2
- package/dist/react/core/widgets/ChatPanel.js.map +1 -0
- package/dist/react/core/{ColorPickerPanel.d.ts → widgets/ColorPickerPanel.d.ts} +1 -1
- package/dist/react/core/{ColorPickerPanel.js → widgets/ColorPickerPanel.js} +3 -3
- package/dist/react/core/widgets/ColorPickerPanel.js.map +1 -0
- package/dist/react/core/{CommandBuilder.js → widgets/CommandBuilder.js} +1 -1
- package/dist/react/core/widgets/CommandBuilder.js.map +1 -0
- package/dist/react/core/{ConnectionForm.d.ts → widgets/ConnectionForm.d.ts} +1 -1
- package/dist/react/core/{ConnectionForm.js → widgets/ConnectionForm.js} +2 -2
- package/dist/react/core/widgets/ConnectionForm.js.map +1 -0
- package/dist/react/core/{FileExplorer.js → widgets/FileExplorer.js} +2 -2
- package/dist/react/core/widgets/FileExplorer.js.map +1 -0
- package/dist/react/core/{HexViewer.js → widgets/HexViewer.js} +1 -1
- package/dist/react/core/widgets/HexViewer.js.map +1 -0
- package/dist/react/core/{ImageGallery.d.ts → widgets/ImageGallery.d.ts} +1 -1
- package/dist/react/core/{ImageGallery.js → widgets/ImageGallery.js} +3 -3
- package/dist/react/core/widgets/ImageGallery.js.map +1 -0
- package/dist/react/core/{LogViewer.d.ts → widgets/LogViewer.d.ts} +13 -3
- package/dist/react/core/{LogViewer.js → widgets/LogViewer.js} +28 -8
- package/dist/react/core/widgets/LogViewer.js.map +1 -0
- package/dist/react/core/{MessageStream.d.ts → widgets/MessageStream.d.ts} +2 -2
- package/dist/react/core/{MessageStream.js → widgets/MessageStream.js} +4 -4
- package/dist/react/core/widgets/MessageStream.js.map +1 -0
- package/dist/react/core/{MissionCalendar.js → widgets/MissionCalendar.js} +2 -2
- package/dist/react/core/widgets/MissionCalendar.js.map +1 -0
- package/dist/react/core/{PacketViewer.js → widgets/PacketViewer.js} +1 -1
- package/dist/react/core/widgets/PacketViewer.js.map +1 -0
- package/dist/react/core/widgets/capture-placeholder.png.js.map +1 -0
- package/dist/react/hooks/index.d.ts +9 -11
- package/dist/react/hooks/useAccessWindows.d.ts +15 -19
- package/dist/react/hooks/useGroundTrackHistory.d.ts +34 -0
- package/dist/react/hooks/useSimulationScene.d.ts +141 -0
- package/dist/react/hooks/useSimulationScene.js +401 -0
- package/dist/react/hooks/useSimulationScene.js.map +1 -0
- package/dist/react/hooks/useZendirSession.d.ts +44 -69
- package/dist/react/index.d.ts +7 -3
- package/dist/react/panels/LayerControlPanel.d.ts +54 -0
- package/dist/react/panels/LayerControlPanel.js +184 -0
- package/dist/react/panels/LayerControlPanel.js.map +1 -0
- package/dist/react/panels/ObjectInventoryPanel.d.ts +57 -0
- package/dist/react/panels/ObjectInventoryPanel.js +261 -0
- package/dist/react/panels/ObjectInventoryPanel.js.map +1 -0
- package/dist/react/panels/index.d.ts +15 -0
- package/dist/react/theme/ThemeProvider.d.ts +2 -0
- package/dist/react/theme/ThemeProvider.js +50 -72
- package/dist/react/theme/ThemeProvider.js.map +1 -1
- package/dist/react/types.d.ts +32 -3
- package/dist/react/types.js.map +1 -1
- package/dist/react.js +51 -42
- package/dist/react.js.map +1 -1
- package/dist/shaders/atmosphere.frag.js +5 -0
- package/dist/shaders/atmosphere.frag.js.map +1 -0
- package/dist/shaders/atmosphere.vert.js +5 -0
- package/dist/shaders/atmosphere.vert.js.map +1 -0
- package/dist/shaders/stars.frag.js +5 -0
- package/dist/shaders/stars.frag.js.map +1 -0
- package/dist/shaders/stars.vert.js +5 -0
- package/dist/shaders/stars.vert.js.map +1 -0
- package/dist/style.css +6 -4
- package/dist/tokens/css-vars.d.ts +91 -0
- package/dist/tokens/css-vars.js +228 -0
- package/dist/tokens/css-vars.js.map +1 -0
- package/dist/tokens/index.d.ts +71 -18
- package/dist/tokens/index.js +206 -97
- package/dist/tokens/index.js.map +1 -1
- package/dist/tokens/tokens.css +50 -50
- package/package.json +26 -22
- package/sdk-stub.js +10 -5
- package/dist/react/3d/EarthViewer.d.ts +0 -46
- package/dist/react/3d/SolarSystemViewer.d.ts +0 -43
- package/dist/react/chatgpt/ChatGPTCard.d.ts +0 -6
- package/dist/react/core/ActivityPlanner.js.map +0 -1
- package/dist/react/core/AppBar.js.map +0 -1
- package/dist/react/core/AstroIcon.js.map +0 -1
- package/dist/react/core/Badge.js.map +0 -1
- package/dist/react/core/Button.js.map +0 -1
- package/dist/react/core/Capture.js.map +0 -1
- package/dist/react/core/CardHeader.js.map +0 -1
- package/dist/react/core/ChatPanel.js.map +0 -1
- package/dist/react/core/Checkbox.js.map +0 -1
- package/dist/react/core/ColorPickerPanel.js.map +0 -1
- package/dist/react/core/CommandBuilder.js.map +0 -1
- package/dist/react/core/ConfirmDialog.js.map +0 -1
- package/dist/react/core/ConnectionForm.js.map +0 -1
- package/dist/react/core/Container.js.map +0 -1
- package/dist/react/core/CopyButton.js.map +0 -1
- package/dist/react/core/DataTable.js.map +0 -1
- package/dist/react/core/DataValue.js.map +0 -1
- package/dist/react/core/Dialog.js.map +0 -1
- package/dist/react/core/FileExplorer.js.map +0 -1
- package/dist/react/core/GlassCard.js.map +0 -1
- package/dist/react/core/HeaderIconWithStatus.js.map +0 -1
- package/dist/react/core/HexViewer.js.map +0 -1
- package/dist/react/core/Icon.js.map +0 -1
- package/dist/react/core/ImageGallery.js.map +0 -1
- package/dist/react/core/Input.js.map +0 -1
- package/dist/react/core/LimitsBar.js.map +0 -1
- package/dist/react/core/LogViewer.js.map +0 -1
- package/dist/react/core/MessageStream.js.map +0 -1
- package/dist/react/core/MissionCalendar.js.map +0 -1
- package/dist/react/core/NumberInput.js.map +0 -1
- package/dist/react/core/PacketViewer.js.map +0 -1
- package/dist/react/core/Pagination.js.map +0 -1
- package/dist/react/core/PinInput.js.map +0 -1
- package/dist/react/core/Popover.js.map +0 -1
- package/dist/react/core/Select.js.map +0 -1
- package/dist/react/core/SideNav.js.map +0 -1
- package/dist/react/core/SidePanel.js.map +0 -1
- package/dist/react/core/Tabs.js.map +0 -1
- package/dist/react/core/Toast.js.map +0 -1
- package/dist/react/core/Toggle.js.map +0 -1
- package/dist/react/core/Tooltip.js.map +0 -1
- package/dist/react/core/Typography.js.map +0 -1
- package/dist/react/core/capture-placeholder.png.js.map +0 -1
- package/dist/react/core/propertyConfig.js.map +0 -1
- package/dist/react/hooks/useSimulationTime.d.ts +0 -61
- package/dist/react/hooks/useSpacecraftPosition.d.ts +0 -50
- package/dist/react/hooks/useTelemetry.d.ts +0 -55
- package/dist/types.d.ts +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- /package/dist/react/core/{propertyConfig.js → data/propertyConfig.js} +0 -0
- /package/dist/react/core/{AstroIcon.d.ts → display/AstroIcon.d.ts} +0 -0
- /package/dist/react/core/{CopyButton.d.ts → display/CopyButton.d.ts} +0 -0
- /package/dist/react/core/{ConfirmDialog.d.ts → feedback/ConfirmDialog.d.ts} +0 -0
- /package/dist/react/core/{Dialog.d.ts → feedback/Dialog.d.ts} +0 -0
- /package/dist/react/core/{Toast.d.ts → feedback/Toast.d.ts} +0 -0
- /package/dist/react/core/{Button.d.ts → inputs/Button.d.ts} +0 -0
- /package/dist/react/core/{Checkbox.d.ts → inputs/Checkbox.d.ts} +0 -0
- /package/dist/react/core/{LimitsBar.d.ts → inputs/LimitsBar.d.ts} +0 -0
- /package/dist/react/core/{PinInput.d.ts → inputs/PinInput.d.ts} +0 -0
- /package/dist/react/core/{Select.d.ts → inputs/Select.d.ts} +0 -0
- /package/dist/react/core/{Toggle.d.ts → inputs/Toggle.d.ts} +0 -0
- /package/dist/react/core/{Pagination.d.ts → navigation/Pagination.d.ts} +0 -0
- /package/dist/react/core/{Tabs.d.ts → navigation/Tabs.d.ts} +0 -0
- /package/dist/react/core/{Popover.d.ts → overlays/Popover.d.ts} +0 -0
- /package/dist/react/core/{SidePanel.d.ts → overlays/SidePanel.d.ts} +0 -0
- /package/dist/react/core/{Tooltip.d.ts → overlays/Tooltip.d.ts} +0 -0
- /package/dist/react/core/{ActivityPlanner.d.ts → widgets/ActivityPlanner.d.ts} +0 -0
- /package/dist/react/core/{Capture.d.ts → widgets/Capture.d.ts} +0 -0
- /package/dist/react/core/{CommandBuilder.d.ts → widgets/CommandBuilder.d.ts} +0 -0
- /package/dist/react/core/{FileExplorer.d.ts → widgets/FileExplorer.d.ts} +0 -0
- /package/dist/react/core/{HexViewer.d.ts → widgets/HexViewer.d.ts} +0 -0
- /package/dist/react/core/{MissionCalendar.d.ts → widgets/MissionCalendar.d.ts} +0 -0
- /package/dist/react/core/{PacketViewer.d.ts → widgets/PacketViewer.d.ts} +0 -0
- /package/dist/react/core/{capture-placeholder.png.js → widgets/capture-placeholder.png.js} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CesiumCaptureSource.js","sources":["../../../src/react/3d/CesiumCaptureSource.ts"],"sourcesContent":["/**\n * @zendir/ui - CesiumJS Capture Source\n *\n * Factory function that creates a headless (hidden DOM) CesiumJS viewer\n * for rendering satellite-viewpoint globe images. Designed to plug into\n * the Capture component's `imageSource` prop.\n *\n * The viewer is created once and reused across captures. Camera position,\n * orientation, FOV, and resolution are applied per-request from CaptureArgs.\n *\n * Requires `cesium` as an optional peer dependency. Tree-shakes away when\n * Cesium is not installed (dynamic import).\n *\n * @example\n * ```ts\n * import { createCesiumCaptureSource } from '@zendir/ui/react/3d';\n *\n * const source = await createCesiumCaptureSource({ accessToken: '...' });\n * <Capture imageSource={source.capture} />\n *\n * // Cleanup on unmount:\n * source.destroy();\n * ```\n */\n\nimport type { CaptureArgs } from '../core/Capture';\nimport { EARTH_RADIUS_KM, DEG_TO_RAD, RAD_TO_DEG } from './ZenSpace3DUtils';\n\n/** Lazy import avoids circular deps; returns bundled placeholder PNG on capture failure */\nasync function loadPlaceholderFallback(): Promise<Uint8Array> {\n const { loadCapturePlaceholderImage } = await import('../core/Capture');\n return loadCapturePlaceholderImage();\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface CesiumCaptureSourceOptions {\n /**\n * Container element to host the hidden Cesium viewer.\n * If not provided, one is auto-created and appended to document.body.\n */\n container?: HTMLElement;\n /** Cesium Ion default access token for imagery/terrain tiles */\n accessToken?: string;\n /**\n * Imagery provider URL template. Falls back to Cesium Ion default\n * when not specified and an accessToken is provided.\n */\n imageryUrl?: string;\n /** Enable terrain (requires Cesium Ion token). Default: false */\n terrain?: boolean;\n /** Maximum cached tile count (higher = more memory, fewer re-downloads). Default: 100 */\n maximumCachedTiles?: number;\n /**\n * Called during capture while waiting for tiles to load.\n * Receives elapsed milliseconds so the caller can show a progress indicator.\n */\n onTileLoadProgress?: (elapsedMs: number) => void;\n /** Max milliseconds to wait for tile loading per capture. Default: 10000 */\n tileLoadTimeoutMs?: number;\n /**\n * Use OpenStreetMap raster tiles (no Cesium Ion token required).\n * Strongly recommended for local dev and Storybook; default is true.\n * Set false to keep the Viewer default imagery (typically Cesium Ion / Bing — needs `accessToken`).\n */\n useOpenStreetMapImagery?: boolean;\n}\n\nexport interface CesiumCaptureSourceHandle {\n /** Render a globe image from the given camera parameters. Pass as Capture's imageSource. */\n capture: (args: CaptureArgs) => Promise<Uint8Array>;\n /** Tear down the Cesium viewer, remove the hidden container, release WebGL context. */\n destroy: () => void;\n /** Whether the viewer is initialized and ready to render. */\n readonly ready: boolean;\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\n/**\n * Convert ECEF meters (Unreal / game-engine format) to SDK's ECI-like km format.\n *\n * ECEF (Earth-Centered Earth-Fixed): X→0°N 0°E, Y→0°N 90°E, Z→North Pole (meters)\n * SDK (ECI-like): X/Z→equatorial plane, Y→North Pole (km)\n *\n * Axis mapping: SDK.x = ECEF.x / 1000\n * SDK.y = ECEF.z / 1000 (ECEF Z=north → SDK Y=north)\n * SDK.z = ECEF.y / 1000\n *\n * Export this so callers (e.g. MQTT handlers) can normalize at the boundary\n * before passing positions to CaptureArgs.\n */\nexport function ecefMetersToSdkPosition(\n ecef: [number, number, number],\n): [number, number, number] {\n const [ex, ey, ez] = ecef;\n return [ex / 1000, ez / 1000, ey / 1000];\n}\n\n/**\n * Convert world-frame Euler XYZ rotation + ECEF position to Cesium HPR (degrees).\n *\n * The game engine sends the camera's absolute orientation as Euler 1-2-3\n * (XYZ intrinsic, right-handed, X=right Y=forward Z=up) in the ECEF world frame.\n * Cesium's heading/pitch/roll are relative to the local ENU (East-North-Up) frame\n * at the camera's geographic position, so we must:\n *\n * 1. Build the rotation matrix R from Euler XYZ (camera axes in ECEF)\n * 2. Extract the camera's forward (+Y) and up (+Z) vectors in ECEF\n * 3. Compute the ECEF→ENU rotation matrix at the camera's lat/lon\n * 4. Transform forward/up into the ENU frame\n * 5. Extract heading, pitch, roll from those ENU vectors\n *\n * @param ecefPos Camera position in ECEF meters [x, y, z]\n * @param eulerDeg Euler XYZ rotation in degrees [rx, ry, rz]\n * @returns [heading, pitch, roll] in degrees (Cesium convention)\n */\nexport function ecefEulerXyzToHpr(\n ecefPos: [number, number, number],\n eulerDeg: [number, number, number],\n): [number, number, number] {\n const [px, py, pz] = ecefPos;\n\n // ── Lat/Lon from ECEF position (geocentric, sufficient for orientation) ──\n const rHoriz = Math.sqrt(px * px + py * py);\n const lat = Math.atan2(pz, rHoriz);\n const lon = Math.atan2(py, px);\n const slat = Math.sin(lat), clat = Math.cos(lat);\n const slon = Math.sin(lon), clon = Math.cos(lon);\n\n // ── Build camera rotation matrix from Euler XYZ ──\n // R_game = Rz(rz) · Ry(ry) · Rx(rx) (fixed-frame / extrinsic equivalent)\n // Columns of R_game are the camera's body axes expressed in ECEF.\n const rx = eulerDeg[0] * DEG_TO_RAD;\n const ry = eulerDeg[1] * DEG_TO_RAD;\n const rz = eulerDeg[2] * DEG_TO_RAD;\n const cx = Math.cos(rx), sx = Math.sin(rx);\n const cy = Math.cos(ry), sy = Math.sin(ry);\n const cz = Math.cos(rz), sz = Math.sin(rz);\n\n // Column 1 = camera forward (+Y body axis) in ECEF\n const fX = sx * sy * cz - cx * sz;\n const fY = sx * sy * sz + cx * cz;\n const fZ = sx * cy;\n\n // Column 2 = camera up (+Z body axis) in ECEF\n const uX = cx * sy * cz + sx * sz;\n const uY = cx * sy * sz - sx * cz;\n const uZ = cx * cy;\n\n // ── Transform forward and up from ECEF → ENU ──\n // R_enu rows: East = [-slon, clon, 0 ]\n // North = [-slat*clon, -slat*slon, clat]\n // Up = [ clat*clon, clat*slon, slat]\n const fE = -slon * fX + clon * fY;\n const fN = -slat * clon * fX - slat * slon * fY + clat * fZ;\n const fU = clat * clon * fX + clat * slon * fY + slat * fZ;\n\n const uE = -slon * uX + clon * uY;\n const uN = -slat * clon * uX - slat * slon * uY + clat * uZ;\n const uU = clat * clon * uX + clat * slon * uY + slat * uZ;\n\n // ── Extract heading and pitch from forward vector in ENU ──\n const horizLen = Math.sqrt(fE * fE + fN * fN);\n // heading = CW from North: atan2(East component, North component)\n const heading = Math.atan2(fE, fN) * RAD_TO_DEG;\n // pitch = elevation from horizontal (negative = looking down)\n const pitch = Math.atan2(fU, horizLen) * RAD_TO_DEG;\n\n // ── Extract roll from up vector ──\n // Special case: looking straight up or down (gimbal lock for heading)\n if (horizLen < 1e-9) {\n const roll = Math.atan2(uE, uN) * RAD_TO_DEG;\n return [heading, pitch, roll];\n }\n\n // default_right = normalize( forward_enu × [0,0,1] ) — horizontal perp to forward\n // forward × Z_enu = [fE,fN,fU] × [0,0,1] = [fN, -fE, 0]\n const drLen = Math.sqrt(fN * fN + fE * fE); // = horizLen\n const drE = fN / drLen;\n const drN = -fE / drLen;\n // drU = 0\n\n // default_up = normalize( default_right × forward )\n // dr=[drE,drN,0], f=[fE,fN,fU] (normalize f for a unit cross product)\n const fLen = Math.sqrt(fE * fE + fN * fN + fU * fU);\n const fnE = fE / fLen, fnN = fN / fLen, fnU = fU / fLen;\n const duE = drN * fnU; // drN*fnU - 0*fnN\n const duN = -drE * fnU; // 0*fnE - drE*fnU\n const duU = drE * fnN - drN * fnE;\n\n // roll = atan2( up·default_right, up·default_up )\n const roll = Math.atan2(\n uE * drE + uN * drN, // up · default_right (drU=0)\n uE * duE + uN * duN + uU * duU, // up · default_up\n ) * RAD_TO_DEG;\n\n return [heading, pitch, roll];\n}\n\n/**\n * Convert local-frame Euler XYZ intrinsic rotation (right-handed, degrees)\n * to Cesium heading/pitch/roll (degrees).\n *\n * Use this when the rotation is already relative to a local frame aligned with\n * ENU (e.g. manual test inputs). For world-frame ECEF rotations from Unreal,\n * use `ecefEulerXyzToHpr` instead — it accounts for the ENU frame at the\n * camera's geographic position.\n *\n * Euler XYZ: rotation[0] around X, [1] around Y, [2] around Z.\n * Decomposed into Cesium HPR via the rotation matrix R = Rz·Ry·Rx.\n */\nexport function eulerXyzToHpr(\n eulerDeg: [number, number, number],\n): [number, number, number] {\n const rx = eulerDeg[0] * DEG_TO_RAD;\n const ry = eulerDeg[1] * DEG_TO_RAD;\n const rz = eulerDeg[2] * DEG_TO_RAD;\n\n const cx = Math.cos(rx), sx = Math.sin(rx);\n const cy = Math.cos(ry), sy = Math.sin(ry);\n const cz = Math.cos(rz), sz = Math.sin(rz);\n\n // R = Rz(rz) · Ry(ry) · Rx(rx)\n const R02 = cx * sy * cz + sx * sz;\n const R00 = cy * cz;\n const R01 = sx * sy * cz - cx * sz;\n const R12 = cx * sy * sz - sx * cz;\n const R22 = cx * cy;\n\n const heading = Math.atan2(R01, R00) * RAD_TO_DEG;\n const pitch = Math.asin(Math.max(-1, Math.min(1, -R02))) * RAD_TO_DEG;\n const roll = Math.atan2(-R12, R22) * RAD_TO_DEG;\n\n return [heading, pitch, roll];\n}\n\n/**\n * Convert CaptureArgs position [x, y, z] (km from Earth center, ECI-like)\n * to Cesium Cartesian3 (WGS84 meters).\n *\n * The Y axis points to the North Pole, X/Z define the equatorial plane\n * (matching the ZenSpace3D rendering convention after scale correction).\n *\n * Callers sending ECEF meter coordinates (e.g. Unreal Engine) should convert\n * with `ecefMetersToSdkPosition()` before passing to CaptureArgs.\n */\nfunction positionToCartesian3(Cesium: any, position: [number, number, number]): any {\n const [x, y, z] = position;\n const r = Math.sqrt(x * x + y * y + z * z);\n\n // Guard against zero-length position (would cause NaN in acos)\n if (r < 1) {\n return Cesium.Cartesian3.fromDegrees(0, 0, 400_000);\n }\n\n // Extract direction → lat/lon (independent of distance scale)\n const lat = 90 - Math.acos(Math.min(1, Math.max(-1, y / r))) * RAD_TO_DEG;\n const lon = Math.atan2(z, -x) * RAD_TO_DEG - 180;\n\n // Altitude is distance above Earth surface in km; Cesium needs meters\n const altKm = r - EARTH_RADIUS_KM;\n return Cesium.Cartesian3.fromDegrees(lon, lat, altKm * 1000);\n}\n\n/**\n * Compute the optical field of view from physical sensor parameters.\n *\n * Real satellite cameras derive FOV from:\n * FOV = 2 × atan( (sensor_size / 2) / focal_length )\n * where sensor_size = resolution × pixel_pitch (mm).\n *\n * Returns the FOV in degrees, or null if the parameters are insufficient\n * (zero focal_length, zero pixel_pitch, or zero resolution).\n */\nfunction computeOpticalFov(focalLengthMm: number, pixelPitchMm: number, resolutionPx: number): number | null {\n if (focalLengthMm <= 0 || pixelPitchMm <= 0 || resolutionPx <= 0) return null;\n const sensorSizeMm = resolutionPx * pixelPitchMm;\n return 2 * Math.atan(sensorSizeMm / (2 * focalLengthMm)) * RAD_TO_DEG;\n}\n\n/**\n * Apply grayscale filter to canvas pixel data in-place.\n * Uses luminance weights (ITU-R BT.709).\n */\nfunction applyGrayscale(ctx: CanvasRenderingContext2D, width: number, height: number): void {\n const imageData = ctx.getImageData(0, 0, width, height);\n const data = imageData.data;\n for (let i = 0; i < data.length; i += 4) {\n const lum = 0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2];\n data[i] = data[i + 1] = data[i + 2] = lum;\n }\n ctx.putImageData(imageData, 0, 0);\n}\n\n// ─── Depth-of-Field Simulation ────────────────────────────────────────────────\n\n/**\n * Estimate the nadir subject distance in meters from the camera position.\n *\n * The SDK position is in ECI-like coordinates (km). Camera altitude above\n * the Earth surface gives the approximate distance to the ground plane\n * directly below (nadir). For oblique views the actual distance is larger,\n * but nadir is the dominant case for satellite imagery.\n */\nfunction estimateSubjectDistanceM(position: [number, number, number]): number {\n const [x, y, z] = position;\n const rKm = Math.sqrt(x * x + y * y + z * z);\n const altKm = Math.max(0, rKm - EARTH_RADIUS_KM);\n return altKm * 1000;\n}\n\n/**\n * Compute depth-of-field blur radius in CSS pixels using the thin lens model.\n *\n * Thin lens circle of confusion (CoC) diameter:\n * c = |f²/N × (S - D) / (D × (S - f))|\n *\n * where:\n * f = focal length (mm)\n * N = f-number (aperture)\n * S = focusing distance (mm) — the plane in sharp focus\n * D = subject distance (mm) — actual distance to the scene\n *\n * The CoC diameter is converted to pixels via pixel_pitch, then halved to\n * get a blur radius. The result is clamped by the sensor's maximum\n * acceptable CoC (args.coc) to model physical diffraction limits.\n *\n * Returns 0 when the scene is in acceptable focus (blur < 0.5px).\n * Exported for unit testing.\n */\nexport function computeDofBlurPx(args: CaptureArgs, subjectDistanceM: number): number {\n const fMm = args.focal_length;\n const N = args.aperture;\n const sMm = args.focusing_distance * 1000; // meters → mm\n const dMm = subjectDistanceM * 1000; // meters → mm\n const ppMm = args.pixel_pitch;\n\n if (N <= 0 || fMm <= 0 || dMm <= 0 || ppMm <= 0 || sMm <= fMm) return 0;\n\n // Subject is essentially at the focus plane — no blur needed\n if (Math.abs(sMm - dMm) < 1) return 0;\n\n // Thin lens CoC diameter in mm\n // c = |f²/N × (S - D) / (D × (S - f))|\n const cocDiameterMm = Math.abs(\n (fMm * fMm / N) * (sMm - dMm) / (dMm * (sMm - fMm)),\n );\n\n // Note: args.coc is the sensor's acceptable-focus criterion (not a render clamp).\n // We render the FULL optical blur — the sensor CoC only defines what humans\n // consider \"in focus\" for DoF range calculations, not the actual blur disc.\n\n // Convert diameter → pixel radius\n const radiusPx = (cocDiameterMm / ppMm) / 2;\n\n // Below half a pixel is imperceptible; cap at 1/4 of image to prevent whiteout\n if (radiusPx < 0.5) return 0;\n return Math.min(radiusPx, args.resolution / 4);\n}\n\n/**\n * Apply Gaussian depth-of-field blur to a 2D canvas context.\n * Uses the native CSS `filter: blur()` for GPU-accelerated performance.\n *\n * The blur is applied by:\n * 1. Drawing the current canvas content onto a temp canvas with blur filter\n * 2. Copying the blurred result back\n *\n * This simulates the optical defocus that occurs when the subject distance\n * doesn't match the focusing distance — exactly what happens when a\n * satellite camera's focus plane is set incorrectly.\n */\nfunction applyDofBlur(ctx: CanvasRenderingContext2D, width: number, height: number, radiusPx: number): void {\n const tmp = document.createElement('canvas');\n tmp.width = width;\n tmp.height = height;\n const tmpCtx = tmp.getContext('2d')!;\n\n // CSS filter blur uses Gaussian sigma ≈ radius; the visual result closely\n // matches a real optical defocus disc for moderate blur amounts.\n tmpCtx.filter = `blur(${radiusPx}px)`;\n tmpCtx.drawImage(ctx.canvas, 0, 0);\n\n // Copy blurred result back, replacing the original sharp image\n ctx.clearRect(0, 0, width, height);\n ctx.drawImage(tmp, 0, 0);\n}\n\n/**\n * Convert a data-URL string to a Uint8Array of the underlying binary data.\n */\nfunction dataUrlToBytes(dataUrl: string): Uint8Array {\n const base64 = dataUrl.split(',')[1];\n const binaryStr = atob(base64);\n const bytes = new Uint8Array(binaryStr.length);\n for (let i = 0; i < binaryStr.length; i++) {\n bytes[i] = binaryStr.charCodeAt(i);\n }\n return bytes;\n}\n\n// ─── Factory ──────────────────────────────────────────────────────────────────\n\n/**\n * Creates a reusable CesiumJS capture source backed by a hidden viewer.\n *\n * The viewer is initialized once with `preserveDrawingBuffer: true` so\n * canvas pixels survive after `scene.render()`. All Cesium UI widgets\n * are disabled. The container is positioned off-screen (1x1 px) and\n * resized to the requested resolution on each capture.\n *\n * @param options - Configuration for the hidden viewer\n * @returns Handle with `capture(args)`, `destroy()`, and `ready` flag\n */\nexport async function createCesiumCaptureSource(\n options: CesiumCaptureSourceOptions = {}\n): Promise<CesiumCaptureSourceHandle> {\n // Cesium is untyped when loaded via dynamic import in some TS configs\n const Cesium: any = await import('cesium');\n\n // Set default access token if provided\n if (options.accessToken) {\n Cesium.Ion.defaultAccessToken = options.accessToken;\n }\n\n // Create or reuse a hidden container.\n // CRITICAL: The container must remain fully GPU-rendered for WebGL readback.\n // - `left:-9999px` → GPU skips rendering (off-viewport optimisation)\n // - `visibility:hidden` → some browsers throttle WebGL compositing\n // - `display:none` → element has no layout, canvas is 0x0\n // Solution: keep it on-screen at 0,0 but use `clip-path: inset(100%)` to\n // hide it visually. The element participates in layout AND the GPU renders\n // the WebGL context, but nothing is painted to the screen.\n let container = options.container ?? null;\n let ownsContainer = false;\n if (!container) {\n container = document.createElement('div');\n container.style.cssText =\n 'position:fixed;left:0;top:0;width:1px;height:1px;overflow:hidden;' +\n 'clip-path:inset(100%);pointer-events:none;z-index:-1;';\n document.body.appendChild(container);\n ownsContainer = true;\n }\n\n // Create a minimal Cesium Viewer with no UI chrome\n const viewer: any = new Cesium.Viewer(container, {\n baseLayerPicker: false,\n fullscreenButton: false,\n vrButton: false,\n geocoder: false,\n homeButton: false,\n sceneModePicker: false,\n timeline: false,\n navigationHelpButton: false,\n animation: false,\n scene3DOnly: true,\n // Only render when we explicitly call scene.render()\n useDefaultRenderLoop: false,\n requestRenderMode: true,\n contextOptions: {\n webgl: { preserveDrawingBuffer: true },\n },\n });\n\n // Globe settings for clean captures:\n // - Disable dynamic lighting so the globe is fully illuminated regardless of time-of-day\n // - Remove skybox/moon chrome for clean satellite imagery output\n viewer.scene.globe.enableLighting = false;\n viewer.scene.backgroundColor = Cesium.Color.BLACK;\n viewer.scene.skyBox = undefined as any;\n viewer.scene.moon = undefined as any;\n\n // Raster imagery that does not require a Cesium Ion token (Storybook / local dev).\n // Default Ion/Bing layers often fail silently without a token → black frames.\n // We try multiple OSM-compatible tile sources for reliability.\n const useOsm = options.useOpenStreetMapImagery !== false;\n if (useOsm) {\n viewer.imageryLayers.removeAll();\n\n // ESRI World Imagery — real satellite/aerial photography. No token required\n // for reasonable usage. Far more realistic than Carto basemaps for a\n // satellite-camera simulator (shows actual land, ocean, clouds).\n // Falls back to Carto Voyager (visible colors) if ESRI is unreachable.\n const tileUrl = options.imageryUrl\n || 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}';\n\n viewer.imageryLayers.addImageryProvider(\n new Cesium.UrlTemplateImageryProvider({\n url: tileUrl,\n credit: '© Esri, Maxar, Earthstar Geographics',\n maximumLevel: 19,\n }),\n );\n }\n\n // Enable terrain if requested\n if (options.terrain && options.accessToken) {\n try {\n viewer.scene.terrainProvider = await Cesium.CesiumTerrainProvider.fromIonAssetId(1);\n } catch {\n // Fall back to ellipsoid terrain silently\n }\n }\n\n let destroyed = false;\n\n const capture = async (args: CaptureArgs): Promise<Uint8Array> => {\n if (destroyed) return new Uint8Array(0);\n\n try {\n // Determine render resolution (halve for sample/preview mode)\n const res = args.sample ? Math.max(64, Math.round(args.resolution / 2)) : args.resolution;\n\n // Resize the container to the requested resolution.\n // The sequence is critical for correct WebGL framebuffer dimensions:\n // 1. Set CSS dimensions on the container\n // 2. Force a synchronous reflow so clientWidth/clientHeight update\n // 3. Call viewer.resize() — Cesium reads clientWidth/clientHeight\n // 4. Yield an rAF so the GPU/GL viewport picks up the new canvas size\n // Getting this order wrong causes Cesium to render at stale dimensions\n // (e.g. 1×1) and produce distorted/stretched imagery.\n container!.style.width = `${res}px`;\n container!.style.height = `${res}px`;\n\n // Force synchronous reflow — reading a layout property forces the browser\n // to recalculate layout immediately, ensuring clientWidth/clientHeight\n // reflect the new CSS dimensions before Cesium reads them.\n void container!.clientWidth;\n\n viewer.resize();\n\n // Yield two animation frames: the first lets the browser commit the layout\n // change to the compositor; the second ensures the GL viewport/framebuffer\n // is fully allocated at the new dimensions before we render.\n await new Promise<void>((r) => requestAnimationFrame(() => r()));\n await new Promise<void>((r) => requestAnimationFrame(() => r()));\n\n // Resize again after frames — catches edge cases where the canvas element\n // inside the viewer didn't update dimensions during the first resize.\n viewer.resize();\n\n // Position the camera using CaptureArgs scene coordinates\n const cameraPosition = positionToCartesian3(Cesium, args.position);\n const [heading, pitch, roll] = args.rotation;\n\n viewer.camera.setView({\n destination: cameraPosition,\n orientation: {\n heading: Cesium.Math.toRadians(heading),\n pitch: Cesium.Math.toRadians(pitch),\n roll: Cesium.Math.toRadians(roll),\n },\n });\n\n // Resolve effective FOV — prefer the explicit fov arg, but fall back to\n // the optically-computed value from focal_length + pixel_pitch + resolution\n // when fov is invalid (≤0 or ≥180). This ensures realistic captures even\n // when the caller only provides physical optics parameters.\n const opticalFov = computeOpticalFov(args.focal_length, args.pixel_pitch, args.resolution);\n let effectiveFov = args.fov;\n\n if (effectiveFov <= 0 || effectiveFov >= 180) {\n // Explicit FOV is invalid — use the optics-derived value if available\n if (opticalFov !== null && opticalFov > 0 && opticalFov < 180) {\n effectiveFov = opticalFov;\n if (import.meta.env.DEV) {\n console.debug(\n `[CesiumCapture] fov=${args.fov}° invalid, using optics-derived FOV ` +\n `= ${opticalFov.toFixed(2)}° (f=${args.focal_length}mm, pitch=${args.pixel_pitch}mm, ` +\n `res=${args.resolution}px)`\n );\n }\n }\n } else if (import.meta.env.DEV && opticalFov !== null) {\n // Both are valid — warn when they disagree significantly (>5% relative error).\n // This catches configuration mistakes where the optics don't match the stated FOV.\n const relError = Math.abs(effectiveFov - opticalFov) / opticalFov;\n if (relError > 0.05) {\n console.warn(\n `[CesiumCapture] Explicit fov=${effectiveFov}° differs from optics-derived ` +\n `FOV=${opticalFov.toFixed(2)}° by ${(relError * 100).toFixed(1)}%. ` +\n `Check focal_length=${args.focal_length}mm / pixel_pitch=${args.pixel_pitch}mm.`\n );\n }\n }\n\n if (effectiveFov > 0 && effectiveFov < 180) {\n (viewer.camera.frustum as any).fov = effectiveFov * DEG_TO_RAD;\n }\n\n // Render in a loop until imagery tiles finish loading.\n // Cesium streams tiles over the network, so the first render is often blank.\n // We poll globe.tilesLoaded (true when all visible tiles are resident),\n // up to a timeout to avoid hanging on slow networks.\n const maxWaitMs = options.tileLoadTimeoutMs ?? 10_000;\n const pollIntervalMs = 200;\n const startMs = performance.now();\n const deadline = startMs + maxWaitMs;\n\n // Initial render — forces tile requests to be dispatched\n viewer.scene.requestRender();\n viewer.scene.forceRender();\n\n // Poll until tiles are loaded or timeout. A first render can trigger tile\n // downloads; subsequent forceRender() calls rasterize newly arrived tiles.\n let tileWaitLoops = 0;\n while (!viewer.scene.globe.tilesLoaded && performance.now() < deadline) {\n tileWaitLoops++;\n options.onTileLoadProgress?.(performance.now() - startMs);\n await new Promise<void>((r) => setTimeout(r, pollIntervalMs));\n viewer.scene.requestRender();\n viewer.scene.forceRender();\n }\n\n // Three more forced frames after tiles report ready:\n // 1st: GPU uploads decoded texture data from newly arrived tiles\n // 2nd: composites all layers into the draw buffer\n // 3rd: ensures the preserveDrawingBuffer content is stable for readback\n viewer.scene.requestRender();\n viewer.scene.forceRender();\n await new Promise<void>((r) => requestAnimationFrame(() => r()));\n viewer.scene.requestRender();\n viewer.scene.forceRender();\n await new Promise<void>((r) => requestAnimationFrame(() => r()));\n viewer.scene.forceRender();\n\n const canvas = viewer.scene.canvas as HTMLCanvasElement;\n\n if (import.meta.env.DEV) {\n const waitMs = Math.round(performance.now() - startMs);\n const tilesOk = viewer.scene.globe.tilesLoaded;\n console.debug(\n `[CesiumCapture] ${res}x${res} canvas=${canvas.width}x${canvas.height} ` +\n `tiles=${tilesOk ? 'ok' : 'TIMEOUT'} loops=${tileWaitLoops} wait=${waitMs}ms`\n );\n }\n\n // Verify the canvas actually has non-black content (dev-only diagnostic).\n // Guard both getContext result and readPixels existence — Cesium's canvas\n // may expose webgl/webgl2 or neither depending on context options.\n if (import.meta.env.DEV && canvas.width > 0 && canvas.height > 0) {\n try {\n const sampleCtx = canvas.getContext('webgl2') || canvas.getContext('webgl');\n if (sampleCtx && typeof (sampleCtx as any).readPixels === 'function') {\n const gl = sampleCtx as WebGLRenderingContext;\n const px = new Uint8Array(4);\n gl.readPixels(\n Math.floor(canvas.width / 2), Math.floor(canvas.height / 2),\n 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px,\n );\n if (px[0] === 0 && px[1] === 0 && px[2] === 0) {\n console.warn(\n `[CesiumCapture] Center pixel is black — tiles may not have loaded. ` +\n `Check network for OSM tile 403/CORS errors.`,\n );\n }\n }\n } catch {\n // Non-critical diagnostic — don't let it crash the capture pipeline\n }\n }\n\n // Resolve output format — default to PNG when omitted\n const fmt = args.format === 'jpeg' ? 'image/jpeg' : 'image/png';\n const jpegQuality = args.format === 'jpeg' ? (args.jpeg_quality ?? 0.92) : undefined;\n\n // ── Post-processing pipeline ──────────────────────────────────────\n // Order matters: DoF blur → monochromatic → encode.\n // All post-processing happens on an offscreen 2D canvas to avoid\n // double-reading the WebGL draw buffer.\n\n // Compute DoF blur eagerly so we know whether an offscreen pass is needed\n let dofBlurRadius = 0;\n if (args.focusing_distance > 0 && args.aperture > 0) {\n const subjectDist = estimateSubjectDistanceM(args.position);\n dofBlurRadius = computeDofBlurPx(args, subjectDist);\n }\n\n const needsOffscreen = dofBlurRadius > 0 || args.monochromatic;\n\n if (needsOffscreen) {\n const offscreen = document.createElement('canvas');\n offscreen.width = canvas.width;\n offscreen.height = canvas.height;\n const ctx = offscreen.getContext('2d')!;\n ctx.drawImage(canvas, 0, 0);\n\n // 1. Depth-of-field blur: simulate optical defocus from the thin lens model\n if (dofBlurRadius > 0) {\n applyDofBlur(ctx, offscreen.width, offscreen.height, dofBlurRadius);\n if (import.meta.env.DEV) {\n const subjectDist = estimateSubjectDistanceM(args.position);\n console.debug(\n `[CesiumCapture] DoF blur: radius=${dofBlurRadius.toFixed(1)}px ` +\n `(subject=${(subjectDist / 1000).toFixed(0)}km, focus=${args.focusing_distance}m, ` +\n `f/${args.aperture}, f=${args.focal_length}mm)`\n );\n }\n }\n\n // 2. Monochromatic: BT.709 luminance grayscale\n if (args.monochromatic) {\n applyGrayscale(ctx, offscreen.width, offscreen.height);\n }\n\n const dataUrl = jpegQuality !== undefined\n ? offscreen.toDataURL(fmt, jpegQuality)\n : offscreen.toDataURL(fmt);\n return dataUrlToBytes(dataUrl);\n }\n\n const dataUrl = jpegQuality !== undefined\n ? canvas.toDataURL(fmt, jpegQuality)\n : canvas.toDataURL(fmt);\n return dataUrlToBytes(dataUrl);\n } catch (err) {\n // Log the actual error in dev so we can diagnose rendering failures\n if (import.meta.env.DEV) {\n console.error('[CesiumCapture] Capture failed:', err);\n }\n return loadPlaceholderFallback();\n }\n };\n\n const destroy = () => {\n if (destroyed) return;\n destroyed = true;\n try {\n viewer.destroy();\n } catch {\n // Viewer may already be destroyed\n }\n if (ownsContainer && container && container.parentNode) {\n container.parentNode.removeChild(container);\n }\n };\n\n return {\n capture,\n destroy,\n get ready() {\n return !destroyed;\n },\n };\n}\n"],"names":["roll","dataUrl"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"CesiumCaptureSource.js","sources":["../../../src/react/3d/CesiumCaptureSource.ts"],"sourcesContent":["/**\n * @zendir/ui - CesiumJS Capture Source\n *\n * Factory function that creates a headless (hidden DOM) CesiumJS viewer\n * for rendering satellite-viewpoint globe images. Designed to plug into\n * the Capture component's `imageSource` prop.\n *\n * The viewer is created once and reused across captures. Camera position,\n * orientation, FOV, and resolution are applied per-request from CaptureArgs.\n *\n * Requires `cesium` as an optional peer dependency. Tree-shakes away when\n * Cesium is not installed (dynamic import).\n *\n * @example\n * ```ts\n * import { createCesiumCaptureSource } from '@zendir/ui/react/3d';\n *\n * const source = await createCesiumCaptureSource({ accessToken: '...' });\n * <Capture imageSource={source.capture} />\n *\n * // Cleanup on unmount:\n * source.destroy();\n * ```\n */\n\nimport type { CaptureArgs } from '../core/widgets/Capture';\nimport { EARTH_RADIUS_KM, DEG_TO_RAD, RAD_TO_DEG } from './ZenSpace3DUtils';\n\n/** Lazy import avoids circular deps; returns bundled placeholder PNG on capture failure */\nasync function loadPlaceholderFallback(): Promise<Uint8Array> {\n const { loadCapturePlaceholderImage } = await import('../core/widgets/Capture');\n return loadCapturePlaceholderImage();\n}\n\n// ─── Types ────────────────────────────────────────────────────────────────────\n\nexport interface CesiumCaptureSourceOptions {\n /**\n * Container element to host the hidden Cesium viewer.\n * If not provided, one is auto-created and appended to document.body.\n */\n container?: HTMLElement;\n /** Cesium Ion default access token for imagery/terrain tiles */\n accessToken?: string;\n /**\n * Imagery provider URL template. Falls back to Cesium Ion default\n * when not specified and an accessToken is provided.\n */\n imageryUrl?: string;\n /** Enable terrain (requires Cesium Ion token). Default: false */\n terrain?: boolean;\n /** Maximum cached tile count (higher = more memory, fewer re-downloads). Default: 100 */\n maximumCachedTiles?: number;\n /**\n * Called during capture while waiting for tiles to load.\n * Receives elapsed milliseconds so the caller can show a progress indicator.\n */\n onTileLoadProgress?: (elapsedMs: number) => void;\n /** Max milliseconds to wait for tile loading per capture. Default: 10000 */\n tileLoadTimeoutMs?: number;\n /**\n * Use OpenStreetMap raster tiles (no Cesium Ion token required).\n * Strongly recommended for local dev and Storybook; default is true.\n * Set false to keep the Viewer default imagery (typically Cesium Ion / Bing — needs `accessToken`).\n */\n useOpenStreetMapImagery?: boolean;\n}\n\nexport interface CesiumCaptureSourceHandle {\n /** Render a globe image from the given camera parameters. Pass as Capture's imageSource. */\n capture: (args: CaptureArgs) => Promise<Uint8Array>;\n /** Tear down the Cesium viewer, remove the hidden container, release WebGL context. */\n destroy: () => void;\n /** Whether the viewer is initialized and ready to render. */\n readonly ready: boolean;\n}\n\n// ─── Helpers ──────────────────────────────────────────────────────────────────\n\n/**\n * Convert ECEF meters (Unreal / game-engine format) to SDK's ECI-like km format.\n *\n * ECEF (Earth-Centered Earth-Fixed): X→0°N 0°E, Y→0°N 90°E, Z→North Pole (meters)\n * SDK (ECI-like): X/Z→equatorial plane, Y→North Pole (km)\n *\n * Axis mapping: SDK.x = ECEF.x / 1000\n * SDK.y = ECEF.z / 1000 (ECEF Z=north → SDK Y=north)\n * SDK.z = ECEF.y / 1000\n *\n * Export this so callers (e.g. MQTT handlers) can normalize at the boundary\n * before passing positions to CaptureArgs.\n */\nexport function ecefMetersToSdkPosition(\n ecef: [number, number, number],\n): [number, number, number] {\n const [ex, ey, ez] = ecef;\n return [ex / 1000, ez / 1000, ey / 1000];\n}\n\n/**\n * Convert world-frame Euler XYZ rotation + ECEF position to Cesium HPR (degrees).\n *\n * The game engine sends the camera's absolute orientation as Euler 1-2-3\n * (XYZ intrinsic, right-handed, X=right Y=forward Z=up) in the ECEF world frame.\n * Cesium's heading/pitch/roll are relative to the local ENU (East-North-Up) frame\n * at the camera's geographic position, so we must:\n *\n * 1. Build the rotation matrix R from Euler XYZ (camera axes in ECEF)\n * 2. Extract the camera's forward (+Y) and up (+Z) vectors in ECEF\n * 3. Compute the ECEF→ENU rotation matrix at the camera's lat/lon\n * 4. Transform forward/up into the ENU frame\n * 5. Extract heading, pitch, roll from those ENU vectors\n *\n * @param ecefPos Camera position in ECEF meters [x, y, z]\n * @param eulerDeg Euler XYZ rotation in degrees [rx, ry, rz]\n * @returns [heading, pitch, roll] in degrees (Cesium convention)\n */\nexport function ecefEulerXyzToHpr(\n ecefPos: [number, number, number],\n eulerDeg: [number, number, number],\n): [number, number, number] {\n const [px, py, pz] = ecefPos;\n\n // ── Lat/Lon from ECEF position (geocentric, sufficient for orientation) ──\n const rHoriz = Math.sqrt(px * px + py * py);\n const lat = Math.atan2(pz, rHoriz);\n const lon = Math.atan2(py, px);\n const slat = Math.sin(lat), clat = Math.cos(lat);\n const slon = Math.sin(lon), clon = Math.cos(lon);\n\n // ── Build camera rotation matrix from Euler XYZ ──\n // R_game = Rz(rz) · Ry(ry) · Rx(rx) (fixed-frame / extrinsic equivalent)\n // Columns of R_game are the camera's body axes expressed in ECEF.\n const rx = eulerDeg[0] * DEG_TO_RAD;\n const ry = eulerDeg[1] * DEG_TO_RAD;\n const rz = eulerDeg[2] * DEG_TO_RAD;\n const cx = Math.cos(rx), sx = Math.sin(rx);\n const cy = Math.cos(ry), sy = Math.sin(ry);\n const cz = Math.cos(rz), sz = Math.sin(rz);\n\n // Column 1 = camera forward (+Y body axis) in ECEF\n const fX = sx * sy * cz - cx * sz;\n const fY = sx * sy * sz + cx * cz;\n const fZ = sx * cy;\n\n // Column 2 = camera up (+Z body axis) in ECEF\n const uX = cx * sy * cz + sx * sz;\n const uY = cx * sy * sz - sx * cz;\n const uZ = cx * cy;\n\n // ── Transform forward and up from ECEF → ENU ──\n // R_enu rows: East = [-slon, clon, 0 ]\n // North = [-slat*clon, -slat*slon, clat]\n // Up = [ clat*clon, clat*slon, slat]\n const fE = -slon * fX + clon * fY;\n const fN = -slat * clon * fX - slat * slon * fY + clat * fZ;\n const fU = clat * clon * fX + clat * slon * fY + slat * fZ;\n\n const uE = -slon * uX + clon * uY;\n const uN = -slat * clon * uX - slat * slon * uY + clat * uZ;\n const uU = clat * clon * uX + clat * slon * uY + slat * uZ;\n\n // ── Extract heading and pitch from forward vector in ENU ──\n const horizLen = Math.sqrt(fE * fE + fN * fN);\n // heading = CW from North: atan2(East component, North component)\n const heading = Math.atan2(fE, fN) * RAD_TO_DEG;\n // pitch = elevation from horizontal (negative = looking down)\n const pitch = Math.atan2(fU, horizLen) * RAD_TO_DEG;\n\n // ── Extract roll from up vector ──\n // Special case: looking straight up or down (gimbal lock for heading)\n if (horizLen < 1e-9) {\n const roll = Math.atan2(uE, uN) * RAD_TO_DEG;\n return [heading, pitch, roll];\n }\n\n // default_right = normalize( forward_enu × [0,0,1] ) — horizontal perp to forward\n // forward × Z_enu = [fE,fN,fU] × [0,0,1] = [fN, -fE, 0]\n const drLen = Math.sqrt(fN * fN + fE * fE); // = horizLen\n const drE = fN / drLen;\n const drN = -fE / drLen;\n // drU = 0\n\n // default_up = normalize( default_right × forward )\n // dr=[drE,drN,0], f=[fE,fN,fU] (normalize f for a unit cross product)\n const fLen = Math.sqrt(fE * fE + fN * fN + fU * fU);\n const fnE = fE / fLen, fnN = fN / fLen, fnU = fU / fLen;\n const duE = drN * fnU; // drN*fnU - 0*fnN\n const duN = -drE * fnU; // 0*fnE - drE*fnU\n const duU = drE * fnN - drN * fnE;\n\n // roll = atan2( up·default_right, up·default_up )\n const roll = Math.atan2(\n uE * drE + uN * drN, // up · default_right (drU=0)\n uE * duE + uN * duN + uU * duU, // up · default_up\n ) * RAD_TO_DEG;\n\n return [heading, pitch, roll];\n}\n\n/**\n * Convert local-frame Euler XYZ intrinsic rotation (right-handed, degrees)\n * to Cesium heading/pitch/roll (degrees).\n *\n * Use this when the rotation is already relative to a local frame aligned with\n * ENU (e.g. manual test inputs). For world-frame ECEF rotations from Unreal,\n * use `ecefEulerXyzToHpr` instead — it accounts for the ENU frame at the\n * camera's geographic position.\n *\n * Euler XYZ: rotation[0] around X, [1] around Y, [2] around Z.\n * Decomposed into Cesium HPR via the rotation matrix R = Rz·Ry·Rx.\n */\nexport function eulerXyzToHpr(\n eulerDeg: [number, number, number],\n): [number, number, number] {\n const rx = eulerDeg[0] * DEG_TO_RAD;\n const ry = eulerDeg[1] * DEG_TO_RAD;\n const rz = eulerDeg[2] * DEG_TO_RAD;\n\n const cx = Math.cos(rx), sx = Math.sin(rx);\n const cy = Math.cos(ry), sy = Math.sin(ry);\n const cz = Math.cos(rz), sz = Math.sin(rz);\n\n // R = Rz(rz) · Ry(ry) · Rx(rx)\n const R02 = cx * sy * cz + sx * sz;\n const R00 = cy * cz;\n const R01 = sx * sy * cz - cx * sz;\n const R12 = cx * sy * sz - sx * cz;\n const R22 = cx * cy;\n\n const heading = Math.atan2(R01, R00) * RAD_TO_DEG;\n const pitch = Math.asin(Math.max(-1, Math.min(1, -R02))) * RAD_TO_DEG;\n const roll = Math.atan2(-R12, R22) * RAD_TO_DEG;\n\n return [heading, pitch, roll];\n}\n\n/**\n * Convert CaptureArgs position [x, y, z] (km from Earth center, ECI-like)\n * to Cesium Cartesian3 (WGS84 meters).\n *\n * The Y axis points to the North Pole, X/Z define the equatorial plane\n * (matching the ZenSpace3D rendering convention after scale correction).\n *\n * Callers sending ECEF meter coordinates (e.g. Unreal Engine) should convert\n * with `ecefMetersToSdkPosition()` before passing to CaptureArgs.\n */\nfunction positionToCartesian3(Cesium: any, position: [number, number, number]): any {\n const [x, y, z] = position;\n const r = Math.sqrt(x * x + y * y + z * z);\n\n // Guard against zero-length position (would cause NaN in acos)\n if (r < 1) {\n return Cesium.Cartesian3.fromDegrees(0, 0, 400_000);\n }\n\n // Extract direction → lat/lon (independent of distance scale)\n const lat = 90 - Math.acos(Math.min(1, Math.max(-1, y / r))) * RAD_TO_DEG;\n const lon = Math.atan2(z, -x) * RAD_TO_DEG - 180;\n\n // Altitude is distance above Earth surface in km; Cesium needs meters\n const altKm = r - EARTH_RADIUS_KM;\n return Cesium.Cartesian3.fromDegrees(lon, lat, altKm * 1000);\n}\n\n/**\n * Compute the optical field of view from physical sensor parameters.\n *\n * Real satellite cameras derive FOV from:\n * FOV = 2 × atan( (sensor_size / 2) / focal_length )\n * where sensor_size = resolution × pixel_pitch (mm).\n *\n * Returns the FOV in degrees, or null if the parameters are insufficient\n * (zero focal_length, zero pixel_pitch, or zero resolution).\n */\nfunction computeOpticalFov(focalLengthMm: number, pixelPitchMm: number, resolutionPx: number): number | null {\n if (focalLengthMm <= 0 || pixelPitchMm <= 0 || resolutionPx <= 0) return null;\n const sensorSizeMm = resolutionPx * pixelPitchMm;\n return 2 * Math.atan(sensorSizeMm / (2 * focalLengthMm)) * RAD_TO_DEG;\n}\n\n/**\n * Apply grayscale filter to canvas pixel data in-place.\n * Uses luminance weights (ITU-R BT.709).\n */\nfunction applyGrayscale(ctx: CanvasRenderingContext2D, width: number, height: number): void {\n const imageData = ctx.getImageData(0, 0, width, height);\n const data = imageData.data;\n for (let i = 0; i < data.length; i += 4) {\n const lum = 0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2];\n data[i] = data[i + 1] = data[i + 2] = lum;\n }\n ctx.putImageData(imageData, 0, 0);\n}\n\n// ─── Depth-of-Field Simulation ────────────────────────────────────────────────\n\n/**\n * Estimate the nadir subject distance in meters from the camera position.\n *\n * The SDK position is in ECI-like coordinates (km). Camera altitude above\n * the Earth surface gives the approximate distance to the ground plane\n * directly below (nadir). For oblique views the actual distance is larger,\n * but nadir is the dominant case for satellite imagery.\n */\nfunction estimateSubjectDistanceM(position: [number, number, number]): number {\n const [x, y, z] = position;\n const rKm = Math.sqrt(x * x + y * y + z * z);\n const altKm = Math.max(0, rKm - EARTH_RADIUS_KM);\n return altKm * 1000;\n}\n\n/**\n * Compute depth-of-field blur radius in CSS pixels using the thin lens model.\n *\n * Thin lens circle of confusion (CoC) diameter:\n * c = |f²/N × (S - D) / (D × (S - f))|\n *\n * where:\n * f = focal length (mm)\n * N = f-number (aperture)\n * S = focusing distance (mm) — the plane in sharp focus\n * D = subject distance (mm) — actual distance to the scene\n *\n * The CoC diameter is converted to pixels via pixel_pitch, then halved to\n * get a blur radius. The result is clamped by the sensor's maximum\n * acceptable CoC (args.coc) to model physical diffraction limits.\n *\n * Returns 0 when the scene is in acceptable focus (blur < 0.5px).\n * Exported for unit testing.\n */\nexport function computeDofBlurPx(args: CaptureArgs, subjectDistanceM: number): number {\n const fMm = args.focal_length;\n const N = args.aperture;\n const sMm = args.focusing_distance * 1000; // meters → mm\n const dMm = subjectDistanceM * 1000; // meters → mm\n const ppMm = args.pixel_pitch;\n\n if (N <= 0 || fMm <= 0 || dMm <= 0 || ppMm <= 0 || sMm <= fMm) return 0;\n\n // Subject is essentially at the focus plane — no blur needed\n if (Math.abs(sMm - dMm) < 1) return 0;\n\n // Thin lens CoC diameter in mm\n // c = |f²/N × (S - D) / (D × (S - f))|\n const cocDiameterMm = Math.abs(\n (fMm * fMm / N) * (sMm - dMm) / (dMm * (sMm - fMm)),\n );\n\n // Note: args.coc is the sensor's acceptable-focus criterion (not a render clamp).\n // We render the FULL optical blur — the sensor CoC only defines what humans\n // consider \"in focus\" for DoF range calculations, not the actual blur disc.\n\n // Convert diameter → pixel radius\n const radiusPx = (cocDiameterMm / ppMm) / 2;\n\n // Below half a pixel is imperceptible; cap at 1/4 of image to prevent whiteout\n if (radiusPx < 0.5) return 0;\n return Math.min(radiusPx, args.resolution / 4);\n}\n\n/**\n * Apply Gaussian depth-of-field blur to a 2D canvas context.\n * Uses the native CSS `filter: blur()` for GPU-accelerated performance.\n *\n * The blur is applied by:\n * 1. Drawing the current canvas content onto a temp canvas with blur filter\n * 2. Copying the blurred result back\n *\n * This simulates the optical defocus that occurs when the subject distance\n * doesn't match the focusing distance — exactly what happens when a\n * satellite camera's focus plane is set incorrectly.\n */\nfunction applyDofBlur(ctx: CanvasRenderingContext2D, width: number, height: number, radiusPx: number): void {\n const tmp = document.createElement('canvas');\n tmp.width = width;\n tmp.height = height;\n const tmpCtx = tmp.getContext('2d')!;\n\n // CSS filter blur uses Gaussian sigma ≈ radius; the visual result closely\n // matches a real optical defocus disc for moderate blur amounts.\n tmpCtx.filter = `blur(${radiusPx}px)`;\n tmpCtx.drawImage(ctx.canvas, 0, 0);\n\n // Copy blurred result back, replacing the original sharp image\n ctx.clearRect(0, 0, width, height);\n ctx.drawImage(tmp, 0, 0);\n}\n\n/**\n * Convert a data-URL string to a Uint8Array of the underlying binary data.\n */\nfunction dataUrlToBytes(dataUrl: string): Uint8Array {\n const base64 = dataUrl.split(',')[1];\n const binaryStr = atob(base64);\n const bytes = new Uint8Array(binaryStr.length);\n for (let i = 0; i < binaryStr.length; i++) {\n bytes[i] = binaryStr.charCodeAt(i);\n }\n return bytes;\n}\n\n// ─── Factory ──────────────────────────────────────────────────────────────────\n\n/**\n * Creates a reusable CesiumJS capture source backed by a hidden viewer.\n *\n * The viewer is initialized once with `preserveDrawingBuffer: true` so\n * canvas pixels survive after `scene.render()`. All Cesium UI widgets\n * are disabled. The container is positioned off-screen (1x1 px) and\n * resized to the requested resolution on each capture.\n *\n * @param options - Configuration for the hidden viewer\n * @returns Handle with `capture(args)`, `destroy()`, and `ready` flag\n */\nexport async function createCesiumCaptureSource(\n options: CesiumCaptureSourceOptions = {}\n): Promise<CesiumCaptureSourceHandle> {\n // Cesium is untyped when loaded via dynamic import in some TS configs\n const Cesium: any = await import('cesium');\n\n // Set default access token if provided\n if (options.accessToken) {\n Cesium.Ion.defaultAccessToken = options.accessToken;\n }\n\n // Create or reuse a hidden container.\n // CRITICAL: The container must remain fully GPU-rendered for WebGL readback.\n // - `left:-9999px` → GPU skips rendering (off-viewport optimisation)\n // - `visibility:hidden` → some browsers throttle WebGL compositing\n // - `display:none` → element has no layout, canvas is 0x0\n // Solution: keep it on-screen at 0,0 but use `clip-path: inset(100%)` to\n // hide it visually. The element participates in layout AND the GPU renders\n // the WebGL context, but nothing is painted to the screen.\n let container = options.container ?? null;\n let ownsContainer = false;\n if (!container) {\n container = document.createElement('div');\n container.style.cssText =\n 'position:fixed;left:0;top:0;width:1px;height:1px;overflow:hidden;' +\n 'clip-path:inset(100%);pointer-events:none;z-index:-1;';\n document.body.appendChild(container);\n ownsContainer = true;\n }\n\n // Create a minimal Cesium Viewer with no UI chrome\n const viewer: any = new Cesium.Viewer(container, {\n baseLayerPicker: false,\n fullscreenButton: false,\n vrButton: false,\n geocoder: false,\n homeButton: false,\n sceneModePicker: false,\n timeline: false,\n navigationHelpButton: false,\n animation: false,\n scene3DOnly: true,\n // Only render when we explicitly call scene.render()\n useDefaultRenderLoop: false,\n requestRenderMode: true,\n contextOptions: {\n webgl: { preserveDrawingBuffer: true },\n },\n });\n\n // Globe settings for clean captures:\n // - Disable dynamic lighting so the globe is fully illuminated regardless of time-of-day\n // - Remove skybox/moon chrome for clean satellite imagery output\n viewer.scene.globe.enableLighting = false;\n viewer.scene.backgroundColor = Cesium.Color.BLACK;\n viewer.scene.skyBox = undefined as any;\n viewer.scene.moon = undefined as any;\n\n // Raster imagery that does not require a Cesium Ion token (Storybook / local dev).\n // Default Ion/Bing layers often fail silently without a token → black frames.\n // We try multiple OSM-compatible tile sources for reliability.\n const useOsm = options.useOpenStreetMapImagery !== false;\n if (useOsm) {\n viewer.imageryLayers.removeAll();\n\n // ESRI World Imagery — real satellite/aerial photography. No token required\n // for reasonable usage. Far more realistic than Carto basemaps for a\n // satellite-camera simulator (shows actual land, ocean, clouds).\n // Falls back to Carto Voyager (visible colors) if ESRI is unreachable.\n const tileUrl = options.imageryUrl\n || 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}';\n\n viewer.imageryLayers.addImageryProvider(\n new Cesium.UrlTemplateImageryProvider({\n url: tileUrl,\n credit: '© Esri, Maxar, Earthstar Geographics',\n maximumLevel: 19,\n }),\n );\n }\n\n // Enable terrain if requested\n if (options.terrain && options.accessToken) {\n try {\n viewer.scene.terrainProvider = await Cesium.CesiumTerrainProvider.fromIonAssetId(1);\n } catch {\n // Fall back to ellipsoid terrain silently\n }\n }\n\n let destroyed = false;\n\n const capture = async (args: CaptureArgs): Promise<Uint8Array> => {\n if (destroyed) return new Uint8Array(0);\n\n try {\n // Determine render resolution (halve for sample/preview mode)\n const res = args.sample ? Math.max(64, Math.round(args.resolution / 2)) : args.resolution;\n\n // Resize the container to the requested resolution.\n // The sequence is critical for correct WebGL framebuffer dimensions:\n // 1. Set CSS dimensions on the container\n // 2. Force a synchronous reflow so clientWidth/clientHeight update\n // 3. Call viewer.resize() — Cesium reads clientWidth/clientHeight\n // 4. Yield an rAF so the GPU/GL viewport picks up the new canvas size\n // Getting this order wrong causes Cesium to render at stale dimensions\n // (e.g. 1×1) and produce distorted/stretched imagery.\n container!.style.width = `${res}px`;\n container!.style.height = `${res}px`;\n\n // Force synchronous reflow — reading a layout property forces the browser\n // to recalculate layout immediately, ensuring clientWidth/clientHeight\n // reflect the new CSS dimensions before Cesium reads them.\n void container!.clientWidth;\n\n viewer.resize();\n\n // Yield two animation frames: the first lets the browser commit the layout\n // change to the compositor; the second ensures the GL viewport/framebuffer\n // is fully allocated at the new dimensions before we render.\n await new Promise<void>((r) => requestAnimationFrame(() => r()));\n await new Promise<void>((r) => requestAnimationFrame(() => r()));\n\n // Resize again after frames — catches edge cases where the canvas element\n // inside the viewer didn't update dimensions during the first resize.\n viewer.resize();\n\n // Position the camera using CaptureArgs scene coordinates\n const cameraPosition = positionToCartesian3(Cesium, args.position);\n const [heading, pitch, roll] = args.rotation;\n\n viewer.camera.setView({\n destination: cameraPosition,\n orientation: {\n heading: Cesium.Math.toRadians(heading),\n pitch: Cesium.Math.toRadians(pitch),\n roll: Cesium.Math.toRadians(roll),\n },\n });\n\n // Resolve effective FOV — prefer the explicit fov arg, but fall back to\n // the optically-computed value from focal_length + pixel_pitch + resolution\n // when fov is invalid (≤0 or ≥180). This ensures realistic captures even\n // when the caller only provides physical optics parameters.\n const opticalFov = computeOpticalFov(args.focal_length, args.pixel_pitch, args.resolution);\n let effectiveFov = args.fov;\n\n if (effectiveFov <= 0 || effectiveFov >= 180) {\n // Explicit FOV is invalid — use the optics-derived value if available\n if (opticalFov !== null && opticalFov > 0 && opticalFov < 180) {\n effectiveFov = opticalFov;\n if (import.meta.env.DEV) {\n console.debug(\n `[CesiumCapture] fov=${args.fov}° invalid, using optics-derived FOV ` +\n `= ${opticalFov.toFixed(2)}° (f=${args.focal_length}mm, pitch=${args.pixel_pitch}mm, ` +\n `res=${args.resolution}px)`\n );\n }\n }\n } else if (import.meta.env.DEV && opticalFov !== null) {\n // Both are valid — warn when they disagree significantly (>5% relative error).\n // This catches configuration mistakes where the optics don't match the stated FOV.\n const relError = Math.abs(effectiveFov - opticalFov) / opticalFov;\n if (relError > 0.05) {\n console.warn(\n `[CesiumCapture] Explicit fov=${effectiveFov}° differs from optics-derived ` +\n `FOV=${opticalFov.toFixed(2)}° by ${(relError * 100).toFixed(1)}%. ` +\n `Check focal_length=${args.focal_length}mm / pixel_pitch=${args.pixel_pitch}mm.`\n );\n }\n }\n\n if (effectiveFov > 0 && effectiveFov < 180) {\n (viewer.camera.frustum as any).fov = effectiveFov * DEG_TO_RAD;\n }\n\n // Render in a loop until imagery tiles finish loading.\n // Cesium streams tiles over the network, so the first render is often blank.\n // We poll globe.tilesLoaded (true when all visible tiles are resident),\n // up to a timeout to avoid hanging on slow networks.\n const maxWaitMs = options.tileLoadTimeoutMs ?? 10_000;\n const pollIntervalMs = 200;\n const startMs = performance.now();\n const deadline = startMs + maxWaitMs;\n\n // Initial render — forces tile requests to be dispatched\n viewer.scene.requestRender();\n viewer.scene.forceRender();\n\n // Poll until tiles are loaded or timeout. A first render can trigger tile\n // downloads; subsequent forceRender() calls rasterize newly arrived tiles.\n let tileWaitLoops = 0;\n while (!viewer.scene.globe.tilesLoaded && performance.now() < deadline) {\n tileWaitLoops++;\n options.onTileLoadProgress?.(performance.now() - startMs);\n await new Promise<void>((r) => setTimeout(r, pollIntervalMs));\n viewer.scene.requestRender();\n viewer.scene.forceRender();\n }\n\n // Three more forced frames after tiles report ready:\n // 1st: GPU uploads decoded texture data from newly arrived tiles\n // 2nd: composites all layers into the draw buffer\n // 3rd: ensures the preserveDrawingBuffer content is stable for readback\n viewer.scene.requestRender();\n viewer.scene.forceRender();\n await new Promise<void>((r) => requestAnimationFrame(() => r()));\n viewer.scene.requestRender();\n viewer.scene.forceRender();\n await new Promise<void>((r) => requestAnimationFrame(() => r()));\n viewer.scene.forceRender();\n\n const canvas = viewer.scene.canvas as HTMLCanvasElement;\n\n if (import.meta.env.DEV) {\n const waitMs = Math.round(performance.now() - startMs);\n const tilesOk = viewer.scene.globe.tilesLoaded;\n console.debug(\n `[CesiumCapture] ${res}x${res} canvas=${canvas.width}x${canvas.height} ` +\n `tiles=${tilesOk ? 'ok' : 'TIMEOUT'} loops=${tileWaitLoops} wait=${waitMs}ms`\n );\n }\n\n // Verify the canvas actually has non-black content (dev-only diagnostic).\n // Guard both getContext result and readPixels existence — Cesium's canvas\n // may expose webgl/webgl2 or neither depending on context options.\n if (import.meta.env.DEV && canvas.width > 0 && canvas.height > 0) {\n try {\n const sampleCtx = canvas.getContext('webgl2') || canvas.getContext('webgl');\n if (sampleCtx && typeof (sampleCtx as any).readPixels === 'function') {\n const gl = sampleCtx as WebGLRenderingContext;\n const px = new Uint8Array(4);\n gl.readPixels(\n Math.floor(canvas.width / 2), Math.floor(canvas.height / 2),\n 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, px,\n );\n if (px[0] === 0 && px[1] === 0 && px[2] === 0) {\n console.warn(\n `[CesiumCapture] Center pixel is black — tiles may not have loaded. ` +\n `Check network for OSM tile 403/CORS errors.`,\n );\n }\n }\n } catch {\n // Non-critical diagnostic — don't let it crash the capture pipeline\n }\n }\n\n // Resolve output format — default to PNG when omitted\n const fmt = args.format === 'jpeg' ? 'image/jpeg' : 'image/png';\n const jpegQuality = args.format === 'jpeg' ? (args.jpeg_quality ?? 0.92) : undefined;\n\n // ── Post-processing pipeline ──────────────────────────────────────\n // Order matters: DoF blur → monochromatic → encode.\n // All post-processing happens on an offscreen 2D canvas to avoid\n // double-reading the WebGL draw buffer.\n\n // Compute DoF blur eagerly so we know whether an offscreen pass is needed\n let dofBlurRadius = 0;\n if (args.focusing_distance > 0 && args.aperture > 0) {\n const subjectDist = estimateSubjectDistanceM(args.position);\n dofBlurRadius = computeDofBlurPx(args, subjectDist);\n }\n\n const needsOffscreen = dofBlurRadius > 0 || args.monochromatic;\n\n if (needsOffscreen) {\n const offscreen = document.createElement('canvas');\n offscreen.width = canvas.width;\n offscreen.height = canvas.height;\n const ctx = offscreen.getContext('2d')!;\n ctx.drawImage(canvas, 0, 0);\n\n // 1. Depth-of-field blur: simulate optical defocus from the thin lens model\n if (dofBlurRadius > 0) {\n applyDofBlur(ctx, offscreen.width, offscreen.height, dofBlurRadius);\n if (import.meta.env.DEV) {\n const subjectDist = estimateSubjectDistanceM(args.position);\n console.debug(\n `[CesiumCapture] DoF blur: radius=${dofBlurRadius.toFixed(1)}px ` +\n `(subject=${(subjectDist / 1000).toFixed(0)}km, focus=${args.focusing_distance}m, ` +\n `f/${args.aperture}, f=${args.focal_length}mm)`\n );\n }\n }\n\n // 2. Monochromatic: BT.709 luminance grayscale\n if (args.monochromatic) {\n applyGrayscale(ctx, offscreen.width, offscreen.height);\n }\n\n const dataUrl = jpegQuality !== undefined\n ? offscreen.toDataURL(fmt, jpegQuality)\n : offscreen.toDataURL(fmt);\n return dataUrlToBytes(dataUrl);\n }\n\n const dataUrl = jpegQuality !== undefined\n ? canvas.toDataURL(fmt, jpegQuality)\n : canvas.toDataURL(fmt);\n return dataUrlToBytes(dataUrl);\n } catch (err) {\n // Log the actual error in dev so we can diagnose rendering failures\n if (import.meta.env.DEV) {\n console.error('[CesiumCapture] Capture failed:', err);\n }\n return loadPlaceholderFallback();\n }\n };\n\n const destroy = () => {\n if (destroyed) return;\n destroyed = true;\n try {\n viewer.destroy();\n } catch {\n // Viewer may already be destroyed\n }\n if (ownsContainer && container && container.parentNode) {\n container.parentNode.removeChild(container);\n }\n };\n\n return {\n capture,\n destroy,\n get ready() {\n return !destroyed;\n },\n };\n}\n"],"names":["roll","dataUrl"],"mappings":"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"}
|