342 lines
28 KiB
Markdown
342 lines
28 KiB
Markdown
# Earth Frontend Context
|
||
|
||
This document describes the current real structure of the Earth display frontend. The focus is on helping future changes to the HUD, layers, media panel, real terrain, and BGP visualization avoid repeating past structural and state-sync pitfalls.
|
||
|
||
Related references:
|
||
|
||
- [Project Rules](/home/ray/dev/linkong/planet/rules.md)
|
||
- [Frontend Layout Guidelines](/home/ray/dev/linkong/planet/docs/technical/en/frontend-layout-guidelines.md)
|
||
|
||
## Current Goal
|
||
|
||
The Earth frontend is not an ordinary admin page — it is an independent large-screen display frontend. Current product goals:
|
||
|
||
- Maintain the spatial depth and readability of the globe view
|
||
- Keep HUD, layers, media panel, BGP, satellites, cables, and similar elements in a unified interaction model
|
||
- Clearly represent states like loading, enabled, hidden, and locked
|
||
|
||
## Current Entry Point
|
||
|
||
React route entry:
|
||
|
||
- [Earth.tsx](/home/ray/dev/linkong/planet/frontend/src/pages/Earth/Earth.tsx)
|
||
|
||
The current approach is simple:
|
||
|
||
- The React page only provides a full-screen `iframe`
|
||
- The actual Earth application runs at:
|
||
- [index.html](/home/ray/dev/linkong/planet/frontend/public/earth/index.html)
|
||
|
||
Earth frontend is essentially a standalone static application under `public/earth`.
|
||
|
||
## Current File Layers
|
||
|
||
### 1. Page Entry and Structure
|
||
|
||
- [index.html](/home/ray/dev/linkong/planet/frontend/public/earth/index.html)
|
||
|
||
Responsibilities:
|
||
|
||
- Base HUD DOM
|
||
- Layer panel
|
||
- Media panel
|
||
- Toolbar
|
||
- Settings dialog
|
||
- Legacy element ID compatibility
|
||
|
||
### 2. Main Runtime
|
||
|
||
- [main.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/main.js)
|
||
|
||
Responsibilities:
|
||
|
||
- Globe initialization
|
||
- Three.js scene assembly
|
||
- Data loading and refresh
|
||
- Layer module integration
|
||
- Earth-level state synchronization
|
||
|
||
### 3. Earth Control Layer
|
||
|
||
- [controls.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/controls.js)
|
||
|
||
Responsibilities:
|
||
|
||
- Toolbar interaction
|
||
- Layer panel interaction
|
||
- Rotation / zoom / layout
|
||
- HUD panel drag
|
||
- Layer toggle state machine
|
||
- Earth settings read, persist, and reset
|
||
|
||
This is currently the most critical UI control entry point for the Earth frontend.
|
||
|
||
Earth settings are now grouped by `data-settings-tab` and `data-settings-tab-panel`. Desktop and mobile share the same category semantics: Runtime, Display, Panels, Motion, Shortcuts, and System. When adding a setting, first choose its category, then add the DOM, persistence field, and restore logic; do not keep growing one long undifferentiated panel.
|
||
|
||
Shortcut configuration is a device-local preference owned by `controls.js`: read, capture, enable/disable, and reset all stay in the Earth frontend. It should not be written to backend user settings and should not affect other browsers. New shortcuts must provide a default key, display label, disabled/enabled state, and reset path instead of being hard-coded only in a keydown handler.
|
||
|
||
### 4. UI and Status Messages
|
||
|
||
- [ui.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/ui.js)
|
||
|
||
Responsibilities:
|
||
|
||
- Loading panel
|
||
- Status message
|
||
- Tooltip / error / cleanup logic
|
||
|
||
### 5. Motion Capture Control Adapter
|
||
|
||
- [motion-control.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/motion-control.js)
|
||
|
||
Responsibilities:
|
||
|
||
- Act as the Motion Provider manager for both `browser_camera` and `motion_agent`.
|
||
- Use browser `getUserMedia` plus local MediaPipe recognition by default; advanced setups can connect to the local Motion Capture Agent WebSocket.
|
||
- Handle browser camera permission/secure-context errors, plus Agent disconnects, reconnects, `status`, and `heartbeat` messages.
|
||
- Filter low-confidence and overly repeated gesture events.
|
||
- Map `rotate_left`, `rotate_right`, `rotate_up`, `rotate_down`, `zoom_in`, `zoom_out`, `focus_prev`, `focus_next`, `layer_prev`, `layer_next`, and `confirm` to the action entry points exposed by `main.js`.
|
||
- Parse `skeleton` debug events and dispatch `earth:motion-debug-frame`.
|
||
|
||
Gesture recognition may run locally in the browser or inside the local Agent, but neither path sends realtime camera frames to the SaaS cloud. `main.js` exposes rotation, zoom, target focus, layer switching, and confirm entry points, plus a `window.__planetEarth.motion` debug entry. The adapter starts only when `?motion=1` is present, browser local storage contains `planet-earth-motion-control-enabled=true`, or Earth settings enable Motion Debug Mode.
|
||
|
||
[motion-debug-panel.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/motion-debug-panel.js) owns the debug panel. It listens for `earth:motion-debug-frame` and draws normalized skeleton joints and bones on a canvas. The Browser Camera provider also emits `earth:motion-debug-video-source` with the local `<video>` element so the panel can show a local preview behind the skeleton; `shared.motionDebugSkeletonOnly` switches the panel back to skeleton-only rendering. `Stop Matching Gestures` dispatches `earth:motion-recognition-pause`, which suppresses gesture execution while video and skeleton drawing continue. Unmatched skeletons are red; matched gestures turn green and display the gesture name. Settings are persisted under `shared.motionDebugEnabled`, `shared.motionProvider`, and `shared.motionDebugSkeletonOnly` in `planet.earth.settings.v2`, and both the switch and provider selector reserve `data-gatekeeper-permission="earth.motion_debug"`.
|
||
|
||
The Browser Camera provider's gesture pipeline lives in `recognizeGesture()` inside [motion-browser-provider.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/motion-browser-provider.js). Detectors are evaluated in this order, first match wins:
|
||
|
||
1. **`getZoomTrend` (trend zoom)** — derived from the per-frame change in `Math.abs(rightWrist.x - leftWrist.x)`. Both wrists must cross the noise floor (`ZOOM_TREND_MIN_WRIST_DELTA = 0.010`) and stay within `ZOOM_TREND_HEIGHT_TOLERANCE` of each other vertically. Growing span → `zoom_in`, shrinking span → `zoom_out`. Trend has highest priority so mid-motion frames cannot be hijacked by the layer/focus/rotate detectors.
|
||
2. **`getZoomHoldPose` (sustained zoom)** — after motion stops, keeps emitting `zoom_in` while the wrists stay at chest level or above with span > `ZOOM_HOLD_SPREAD_FACTOR × shoulderWidth` (default 1.30), and `zoom_out` while elbows sit visibly outward and span < `ZOOM_HOLD_CLOSE_FACTOR × shoulderWidth` (default 0.85).
|
||
3. **layer / focus** — left-wrist raise + vertical motion fires `layer_prev/next`; head tilt fires `focus_prev/next`.
|
||
4. **`getRightArmPattern` (single-arm rotate)** — only considered when both `!isZoomCandidatePose(...)` and `isLeftArmAtRest(...)` hold. `isLeftArmAtRest` requires the left wrist to hang clearly below the shoulder line (≥ 0.13) and both left elbow and left wrist to stay near the body — any ambiguous left-arm posture (mid-spread, raised, held at chest) blocks single-arm rotate.
|
||
|
||
Two non-obvious decisions worth preserving:
|
||
|
||
- **Mirror-safe**: all zoom checks use `Math.abs(rightWrist.x - leftWrist.x)` and never rely on per-side x direction. `getUserMedia` returns the raw camera feed without horizontal flip, so a subject's anatomical left arm appears on the image right. A direction-based detector (e.g. "left wrist moves left, right wrist moves right") inverts on non-mirrored feeds — span-based detection is invariant.
|
||
- **Continuous vs. discrete**: `rotate`, `layer`, `focus`, and `confirm` go through `applyPoseLatch`, which emits each gesture once until the pose returns to neutral (one wave = one rotation step). Zoom intentionally bypasses the latch and re-matches every frame; downstream `GESTURE_POLICIES.zoom_in/out.cooldownMs = 120` rate-limits to ~8 emits/sec, so holding a spread pose keeps zooming in until the user changes their pose. Do not reuse the latch for zoom — that semantic difference is the point.
|
||
|
||
[presentation-controller.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/presentation-controller.js) is the new Presentation layer. In the first stage only Motion uses it: `motion-cruise-adapter.js` uses a persistent presentation that reuses the cruise fixed-card placement and connector, but mouse movement does not auto-hide the card. The connector recalculates source and target anchors every frame so dragged cards, globe rotation, and moving targets stay connected. BGP/News still use the existing `CruiseSequencer` auto-advance path to preserve the old cruise experience.
|
||
|
||
### 6. Globe and Terrain
|
||
|
||
- [earth.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/earth.js)
|
||
- [terrain.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/terrain.js)
|
||
|
||
Responsibilities:
|
||
|
||
- Globe sphere, cloud layer, atmosphere
|
||
- Real terrain mesh
|
||
- Terrain tile fetch, decode, displacement, and shading
|
||
- Whole-globe land/ocean and border base overlays
|
||
|
||
The Earth surface is a stack of near-concentric shells, not a single mesh. The base sphere and HD texture overlay in `earth.js`, plus the land/ocean base in `country-boundaries.js`, need explicit radius separation. At far zoom, GPU depth precision drops; neighboring shells that are too close can z-fight and show black flicker blocks or snow. The current stable spacing is `landAltitudeOffset = 0.32` and `textureOverlayAltitudeOffset = 0.48`. When adding or adjusting whole-globe surface overlays, update [Earth Render Layer Order](/home/ray/dev/linkong/planet/docs/technical/en/earth-render-layer-order.md) and verify at 50% zoom.
|
||
|
||
### 7. Layer Modules
|
||
|
||
- [satellites.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/satellites.js)
|
||
- [cables.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/cables.js)
|
||
- [vessels.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/vessels.js)
|
||
- [bgp.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/bgp.js)
|
||
- [bgp-cruise-adapter.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/bgp-cruise-adapter.js)
|
||
- [compute-centers.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/compute-centers.js) renders supercomputer and GPU-cluster markers. The backend renders compute centers only from source-provided coordinates or `compute_center_locations` dimension-table coordinates during startup; manual candidate collection can query ROR and Nominatim/OpenStreetMap, and the layer keeps the `?` badge for unconfirmed positions while the details card shows precision, confidence, source notes, and verification date.
|
||
- [country-boundaries.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/country-boundaries.js)
|
||
|
||
Each module is responsible for its own:
|
||
|
||
- Data fetching
|
||
- Three.js mesh creation and update
|
||
- State tracking (loaded, visible, hover, locked)
|
||
- Self-cleanup (dispose on scene destroy)
|
||
|
||
`tv.js` owns the live / aggregation-news tabs inside `media-panel`. Toolbar open and tab-switch actions write back through `earth:tv-visibility-change` and `earth:tv-tab-change`: panel visibility remains viewport-scoped at `views.<scope>.panelVisibility.media-panel`, while the active tab is stored at `shared.mediaPanelActiveTab`. Refreshing the page therefore restores the user's last live/news state. Temporary hides from `closeTransientMobileOverlays()` carry `persist:false` and do not overwrite the preference.
|
||
|
||
`brand.js` manages Earth HUD brand resources. Static assets provide the default brand; runtime overrides come from `/api/v1/earth/brand`, and uploaded images are served from `/earth-brand-assets/...`. The frontend must treat logo/title images and text fallback separately: if an image fails, show the text title; if text fields are empty, rely on backend defaults so the HUD brand area never renders blank. The console Earth Content page owns saving and resetting brand configuration; the Earth frontend only consumes it.
|
||
|
||
`about.js` manages the About card inside Earth settings. Frontend defaults remain as a fallback, while runtime content is loaded from `/api/v1/earth/about`. If the request fails or fields are missing, the renderer must fall back per field so the settings page never renders an empty card. Admin Next exposes an Earth Content `About` tab; saving uses `PUT /api/v1/earth/about`, and restoring defaults uses `DELETE /api/v1/earth/about`.
|
||
|
||
`oobe.js` manages the first-run Earth initialization guide. OOBE visibility must be driven by `/api/v1/earth/oobe-status` and its `ready` field, not by `localStorage`. `localStorage` may only store a short-lived "skip on this browser" flag; if the backend reports `ready: true`, logout, cleared browser storage, or a different browser must not show OOBE again. Desktop uses a dark starfield scrim and glass startup panel, while mobile uses a bottom sheet and respects `prefers-reduced-motion`.
|
||
|
||
The Admin Next Earth Content page must preserve runtime semantics:
|
||
|
||
- `Brand`: brand preview should use the same dark starfield background, size, spacing, logo/title rendering, and text fallback as the Earth HUD top-left brand block, not a generic form preview.
|
||
- `About`: configures the About card in Earth settings, including logo, kicker, title, version, description, and metadata items. Earth runtime reads `/earth/about` and falls back to defaults on failure.
|
||
- `Boundary Precision`: build boundary, refresh status, and restore defaults belong inside this section, not in the global page toolbar.
|
||
- `TV`: the list distinguishes built-in, collected, and custom sources. Card state represents enabled, disabled, draft, or error. Built-in sources cannot be deleted; collected and custom sources can. A new live source only enters draft state after the plus button is clicked; save persists it into the list, while cancel destroys the draft.
|
||
- `Basemap`, `Layer Resources`, `3D Models`, and `News Anchor Strategy`: if backend capability is not available yet, the console should show an explicit pending state instead of mixing those items into TV or brand configuration.
|
||
|
||
TV preview should reuse the Earth runtime live-card structure and state labels as closely as possible so built-in markers, live loading state, stream source, region, and language match what users see on Earth.
|
||
|
||
The compute-center layer row has a notification badge for GeoJSON `unresolved` records. The badge means "no trustworthy coordinates, cannot render on the globe"; it is different from the `?` marker drawn on already positioned but unconfirmed compute centers. Clicking the badge opens a fixed info card beside the layer panel. Row-level `采集` fetches candidates only. Header-level `一键采用` processes the queue top-to-bottom, saves the highest-confidence valid candidate, removes successful rows, renumbers the list, and dispatches `earth:compute-center-unresolved-count-change` so the badge updates immediately. When the batch ends, `earth:compute-center-location-saved` refreshes the real layer.
|
||
|
||
Location candidate state in the details card is cached in [info-card.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/info-card.js) by `entityType:entityId`. If the user closes the details card or unresolved queue and reopens the same compute center / BGP collector, previously collected candidates and status text are restored. Header-level `一键采用` prefers cached candidates, avoiding repeated online geocoding or LLM factcheck calls. After a location is saved, that entity's candidate list is cleared to a "refreshing layer" status so stale candidates do not keep misleading the user.
|
||
|
||
The `预览 / 保存` buttons on each candidate row use a single delegated `click` handler per candidate root (the `[data-collect-cache-key]` block in the details card, or `[data-unresolved-item]` in the unresolved queue), guarded by a `data-candidate-actions-bound` flag so it cannot be double-bound. Direct `pointerup` / `click` listeners on individual buttons and overlapping delegated handlers were removed. Candidate objects are no longer JSON-stringified into an HTML attribute and parsed back; buttons only carry `data-candidate-index`, and the handler resolves the candidate object from a module-level `Map` keyed by cache-key. This removes the entire class of failures caused by HTML entity escaping of `&` / `<` / `"` in candidate fields. Clicking `预览` dispatches `earth:preview-location-candidate`; `main.js`'s `previewLocationCandidate()` calls `showComputeCenterLocationPreview()`, which attaches a hollow breathing-ring sprite pair at the candidate coordinates (visually mirroring the BGP event ring) and focuses the camera on the candidate. Previewing another candidate replaces the ring; saving clears it and `spawnSavedComputeCenterLocation()` immediately spawns the formal compute-center interactable. Note that `main.js` has no module-level `earth` variable — every location-save / preview handler must call `const earth = getEarth();` first, otherwise the event handler throws a `ReferenceError` that the surrounding `.catch` swallows, producing the failure mode where the button "does nothing".
|
||
|
||
The `earth:compute-center-location-saved` reconciliation pipeline is deliberately silent on background-refresh failures. `spawnComputeCenterAfterLocationSave()` already presents the success toast and locked state; `refreshComputeCentersAfterLocationSave()` only reloads backend data when the scene is ready and no longer emits its own `已保存` toast. `handleComputeCenterLocationSaved()` runs refresh in the background after a successful spawn; only when spawn returns `null` (scene not ready) or throws does refresh take over the success toast. A refresh error is only `console.warn`'d — it must never surface as a `保存失败` message, because the save itself succeeded and the refresh is a follow-up sync.
|
||
|
||
### AIS Vessel Layer
|
||
|
||
The vessel layer now uses `/api/v1/vessels/snapshot` for the initial viewport snapshot and the `/ws` `vessels` channel for realtime deltas. Snapshot requests must include `bbox`, `zoom`, and a bounded `limit`; the backend defaults to `limit=1000` and caps it at `5000`. WebSocket subscriptions must include the same viewport fields so the server can filter updates per connection.
|
||
|
||
The legacy `/api/v1/visualization/geo/vessels` route has been removed. Frontend code should fetch a snapshot for the current viewport when the layer opens, then subscribe to `vessels` deltas. After map pan or zoom, reload the snapshot and send a fresh vessels subscription. The backend only falls back to legacy `vessel_position` / `vessel_static` rows when the current raw window is empty; frontend code can detect that state through `diagnostics.legacy_fallback_used`.
|
||
|
||
The new layer API family is `/api/v1/layers/*`, which separates map rendering payloads from aggregate panel statistics. Layer requests must include `bbox`, `zoom`, and a bounded `limit`; responses include `visible_count`, `returned_count`, and `diagnostics`, where `degraded`, `truncated`, and `limit_clamped` are the frontend signals for fallback UI. Right-side aggregate panels should not sum the layer response. They should read `/api/v1/data-products` or `/api/v1/data-products/{product_id}/status`, because those statistics stay global and do not change with the viewport.
|
||
|
||
Vessel color and vessel type text must use the same normalized classification. `vessels.js` derives `type` from both `vessel_type_name` and the AIS numeric `vessel_type` code; that `type` drives marker color. It also derives `vessel_type_display`, which `main.js` uses for the info card, hover summary, and search result subtitle. Do not make the info card read only the raw `vessel_type_name`, because AISStream can provide a numeric type while the raw name is still `Other`.
|
||
|
||
AISStream `PositionReport` messages commonly carry live position and `MetaData.ShipName`, while vessel type usually comes from lower-frequency `ShipStaticData.Type`. The backend normalizes `MetaData.ShipName` into the vessel name and maps numeric type codes into Cargo / Tanker / Passenger / Fishing / Military where available. Missing type detail should wait for a static AIS message or the planned vessel profile enrichment; the frontend should not invent a more specific type.
|
||
|
||
### 8. HUD Panels and Search
|
||
|
||
- [hud-panels.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/hud-panels.js)
|
||
- [info-card.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/info-card.js)
|
||
- [search.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/search.js)
|
||
- [legend.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/legend.js)
|
||
|
||
### 9. Cruise Mode
|
||
|
||
- [cruise-sequencer.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/cruise-sequencer.js)
|
||
- [callout-connector.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/callout-connector.js)
|
||
|
||
The cruise sequencer handles generic logic: current target, queue order, camera focus, and dwell / hide / switch. Business modules supply target queues and content — they should not contain camera control logic.
|
||
|
||
### 10. Constants
|
||
|
||
- [constants.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/constants.js)
|
||
|
||
All material, layer, satellite, BGP, cable, terrain, celestial, and other style parameters are maintained here. Do not scatter magic numbers in module files.
|
||
|
||
## Current Style Layers
|
||
|
||
CSS files in `frontend/public/earth/css/` each correspond to a specific component scope. Do not write global Earth styles into `base.css` unless they genuinely apply to everything.
|
||
|
||
## Current Layer Toggle State Semantics
|
||
|
||
### `data-status-target`
|
||
|
||
Layer toggle buttons use `data-status-target` attributes to link button state to layer state. The state machine in `controls.js` handles:
|
||
|
||
- `loading`: showing the loading indicator
|
||
- `enabled`: layer is active
|
||
- `hidden`: layer is hidden
|
||
- `error`: layer failed to load
|
||
|
||
This is the canonical way to synchronize button visual state with actual layer state. Do not maintain separate boolean flags for button display.
|
||
|
||
Terrain should not block startup when it is not the restored visible layer. After deferred layer visibility settings are applied, `controls.js` schedules `scheduleTerrainPrefetch()` only when HD texture is enabled, terrain is not ready, and no prefetch is already running. The prefetch uses `setTimeout` plus `requestIdleCallback` so cloud, HD texture, and startup layer work keep first-screen priority.
|
||
|
||
## Current Settings Persistence
|
||
|
||
Earth settings are stored in `localStorage`. The key is typically a namespaced string defined in `constants.js`. `controls.js` handles read, write, and reset.
|
||
|
||
Settings that affect visual layers and surface interaction (terrain opacity, day/night mode, satellite display style, satellite idle breathing, real satellite altitude, track display, hover tooltip mode, etc.) are read during initialization and applied immediately.
|
||
|
||
The surface hover tooltip preference is persisted by `controls.js` as `shared.surfaceHoverInfoMode`, while `main.js` composes the actual tooltip in the globe-surface hover branch. `Country` shows country details only when a country polygon is hit and stays silent over ocean; `Position` shows latitude, longitude, and sampled terrain elevation and clears country-boundary hover; `Full` shows country + position on land and position over ocean.
|
||
|
||
The real satellite altitude preference is persisted by `controls.js`, while the rendering state lives in `satellites.js`. When enabled, the real radius from SGP4 is compressed logarithmically into the current Earth visual radius range. When disabled, satellite dots, trails, and predicted orbits all return to the legacy same-sphere display. Toggling this setting must refresh satellite positions and clear trail buffers so a trail never mixes both height models. `maxRealAltitudeOffset = 25` is a visual cap tuned for the current camera and `earthRadius = 100`: GEO / MEO remain clearly higher than LEO, but the highest orbits stay within about 25% beyond the globe radius so selection targets, red trails, and the globe do not feel disconnected.
|
||
|
||
SGP4 propagation returns an inertial-frame position, so it must not be drawn directly as Earth-fixed longitude / latitude. `satellites.js` uses `gstime` to convert ECI/TEME positions to ECF, then maps that result into the same Three.js axes as `latLonToVector3()`. Satellite dots and short trails use Earth-fixed coordinates for each sample time, representing the object's current position relative to the globe surface. The locked predicted orbit uses the `gstime` from the lock moment for the whole future orbit, projecting the inertial orbit plane onto the current globe pose; that keeps the line closed and keeps the visual orbit inclination aligned with the details card. Fallback predicted orbits must also use a real RAAN + inclination orbital-plane formula, not treat inclination as a constant latitude.
|
||
|
||
Boundary precision is stored separately by `country-boundaries.js` under `planet.earth.boundaries.highPrecisionEnabled`. When high precision is off, Earth keeps using the bundled low-precision `countries-admin0.min.geojson` fallback even if high-precision manifest/PMTiles files exist locally. When high precision is on but the artifact is missing, the Earth toolbar settings call `/api/v1/earth/boundaries/build` and poll progress. After success, `reloadCountryBoundaries()` hot-swaps the boundary layer without refreshing the page. Boundary hover is independent from interactable hover: a country polygon remains highlighted whenever the surface coordinate is inside it, while the tooltip can still prioritize a satellite, vessel, BGP marker, or other interactable.
|
||
|
||
## Current Terrain Pipeline
|
||
|
||
1. `terrain.js` creates a sphere geometry with enough segments
|
||
2. On load, fetches Terrarium-format elevation tiles from the backend
|
||
3. Decodes R/G/B into elevation values
|
||
4. Displaces vertex positions radially based on elevation
|
||
5. Applies a vertex alpha that fades terrain edges at coastlines
|
||
6. Terrain writes to the scene as a mesh above the HD texture layer
|
||
|
||
When HD texture is off, terrain is temporarily hidden and its state is remembered. When HD texture comes back on, terrain restores its prior visibility.
|
||
|
||
Terrain tile fetching is batched. `terrain.js` deduplicates required Terrarium tile keys and sends chunks sized by `TERRAIN_CONFIG.batchRequestSize` to `/api/v1/visualization/terrain/terrarium/batch`. The backend proxies S3 Terrarium tiles with an in-memory LRU cache, per-batch deduplication, and bounded concurrency. The single tile endpoint remains for fallback paths and browser cache semantics.
|
||
|
||
## Current High-Frequency Risk Points
|
||
|
||
### 1. Visual State and Business State Out of Sync
|
||
|
||
The most common class of Earth bugs:
|
||
|
||
- Button shows "loaded," but layer has no objects rendered
|
||
- Button shows "hidden," but objects are still visible
|
||
- Loading ended, but button still looks like it hasn't
|
||
|
||
All future changes must prioritize checking state sync.
|
||
|
||
### 2. HUD Layout: Check Structure First, Not CSS Patches
|
||
|
||
Earth HUD has repeatedly experienced:
|
||
|
||
- Panel compressed to a sliver
|
||
- Markdown content clipped
|
||
- Tabs/iframe content consumed by `overflow: hidden`
|
||
|
||
Inspection order:
|
||
|
||
1. Who is responsible for height
|
||
2. Who is responsible for scrolling
|
||
3. Which layer is doing the clipping
|
||
|
||
Do not immediately add `overflow: hidden` or extra wrapper layers.
|
||
|
||
### 3. Transitional Paths Must Be Closed Off
|
||
|
||
Earth has gone through multiple rounds of HUD, toolbar, and media panel refactoring, making it easy to accumulate:
|
||
|
||
- Old helpers
|
||
- Old classes
|
||
- Old fallback logic
|
||
- Deprecated variants
|
||
|
||
After each major feature is complete, do a cleanup pass.
|
||
|
||
### 4. Cruise Mode and Business Events Must Not Be Deeply Coupled
|
||
|
||
The correct boundary:
|
||
|
||
- The generic cruise layer only knows:
|
||
- Current target
|
||
- Queue order
|
||
- Camera focus
|
||
- Dwell / hide / switch
|
||
- Business modules only supply:
|
||
- Target queues
|
||
- Focus coordinates
|
||
- Card content
|
||
- Highlight / layer side effects
|
||
|
||
If future cable, satellite, or news cruise is added, do not copy a new set of `main.js` state variables. Instead reuse:
|
||
|
||
- [cruise-sequencer.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/cruise-sequencer.js)
|
||
- [callout-connector.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/callout-connector.js)
|
||
- The business adapter pattern from [bgp-cruise-adapter.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/bgp-cruise-adapter.js)
|
||
|
||
## Recommended Change Approach
|
||
|
||
For future Earth changes:
|
||
|
||
1. First identify what you're changing:
|
||
- Three.js rendering layer
|
||
- HUD structure layer
|
||
- Layer state layer
|
||
- Panel content layer
|
||
2. If involving layer buttons, connect to the unified state machine
|
||
3. If involving visibility toggle, check whether tooltip / legend / info-card / lock all close together
|
||
4. If involving panel layout, check structure before touching CSS
|
||
|
||
## Current Boundary with the Console Frontend
|
||
|
||
The Earth frontend and the console frontend are not the same UI system:
|
||
|
||
- Console frontend: React + Ant Design workbench
|
||
- Earth frontend: native HUD + Three.js display under `public/earth`
|
||
|
||
Therefore:
|
||
|
||
- Earth should not directly reuse Ant Table / AppLayout semantics
|
||
- The console should not copy Earth HUD animations and glass-layer design language
|
||
|
||
For console structure, see:
|
||
|
||
- [Admin Frontend Context](/home/ray/dev/linkong/planet/docs/technical/en/frontend-admin-frontend-context.md)
|