# Earth Interactable 使用说明 `Interactable` 是 Earth 地表“图标类可交互元素”的通用渲染入口。它把船只图层验证过的模式抽成公共能力:普通态用批量 `THREE.Points`,hover / locked 用少量 overlay,拾取走屏幕空间命中,图标资源统一转进 canvas texture,并在公共层处理 glow、状态、尺寸、贴地渲染和同坐标避让。 当前已接入: | 图层 | 业务文件 | 图标来源 | 补充动画 | | --- | --- | --- | --- | | AIS 船只 | `frontend/public/earth/js/vessels.js` | canvas draw,航行三角形 / 停泊圆点 | 船只轨迹仍由业务层维护 | | 算力中心 | `frontend/public/earth/js/compute-centers.js` | `assets/icons/compute-*.svg` | 估算位置 `?` badge 通过 `icon.afterDraw()` 叠加 | | BGP 事件 | `frontend/public/earth/js/bgp.js` | canvas draw,按事件类型绘制符号 | 向外扩散圈仍由 BGP 业务层维护 | | BGP 观测站 | `frontend/public/earth/js/bgp.js` | `assets/icons/bgp-broadcast-pin.svg` | halo、活跃度 core、覆盖扇形和雷达扫掠仍由 BGP 业务层维护 | 登陆点曾尝试接入 Interactable,但 pin 类 SVG 在地球边缘会被 `THREE.Points` 深度测试裁切成碎片;当前退回 `THREE.Sprite` 专用路径,并改为由 canvas 生成黄色扁平球纹理。旧 SVG 资产保留在 `assets/icons/` 目录中,但登陆点运行时不再依赖 SVG。 ## 为什么需要 Interactable 之前每个地表图标图层都容易各写一套: - icon texture 生成 - hover / locked 状态 - glow 样式 - picking 命中半径 - zoom 下的尺寸策略 - 同经纬度对象重叠避让 这些逻辑如果分散在业务文件里,视觉会漂移,后续调参也会变成逐图层修补。`Interactable` 的边界是:公共层负责“图标怎么在地球上稳定显示和被选中”,业务层负责“数据从哪里来、图标表达什么语义、详情卡展示什么、是否有额外动画”。 ## 入口 ```javascript import { createInteractableLayer } from "./interactable.js"; ``` 核心调用形态: ```javascript const layer = createInteractableLayer({ id: "example", objectType: "example_object", renderOrder: 4.4, altitudeOffset: 0.2, pointSize: 34, icon: { draw(context, options) { // draw canvas icon }, }, getPosition: (item) => ({ latitude: item.latitude, longitude: item.longitude, }), getKind: (item) => item.kind || "default", }); ``` 业务模块通常只暴露一层薄封装: ```javascript export function getExampleMarkers() { return layer.getMarkers(); } export function getExamplePointerIntersections(options) { return layer.getPointerIntersections(options); } export function setExampleMarkerState(marker, state = "normal") { layer.setMarkerState(marker, state); } export function updateExampleVisualState(lockedObjectType, lockedObject, camera) { layer.updateVisualState(lockedObjectType, lockedObject, camera); } ``` ## 配置参数 | 参数 | 默认值 | 说明 | | --- | --- | --- | | `id` | 必填 | 图层唯一标识,用于 group name、避让注册和 debug。 | | `objectType` | `id` | marker 写入 `userData.type` 的业务类型,主交互层用它判断 locked 对象。 | | `renderOrder` | `4` | 普通 points 和 hover / locked overlay 的基础渲染顺序。 | | `altitudeOffset` | `0.2` | 业务高度,按 `CONFIG.earthRadius + altitudeOffset` 计算原始地表位置。 | | `pointSize` | `32` | 基准屏幕像素尺寸。普通 points 和 overlay 都以它为基础。 | | `sizeMode` | `"fixed"` | 默认固定屏幕尺寸;非 `"fixed"` 时会按相机距离做比例缩放。 | | `sizeScale` | `{ referenceFov: 75, min: 0.12, max: 3 }` | `sizeMode !== "fixed"` 时的缩放范围。 | | `atlasCellSize` | `128` | icon canvas texture 尺寸。 | | `colors` | `{}` | 支持 `normal`、按 kind 的平铺 key,以及 `byKind`。 | | `opacity` | `{ normal: 0.88, dimmed: 0.26, hover: 0.98, locked: 1 }` | 各状态透明度。 | | `stateScale` | `{ hover: 1, locked: 1, dimmed: 1 }` | 各状态尺寸倍率。 | | `pulse` | `{}` | locked 态可选呼吸缩放,支持 `enabled`、`speed`、`amplitude`。 | | `avoidance` | `{ enabled: true, precision: 4, radius: 1.1, step: 0.35 }` | 跨 Interactable 的同坐标避让配置。 | | `icon` | 必填 | 图标来源,支持 canvas draw、SVG / 图片 asset、状态 asset、锚点和后处理。 | | `getPosition(item)` | 必填 | 返回 `{ latitude, longitude }` 或 `THREE.Vector3`。 | | `getKind(item)` | `item.type || "default"` | 返回业务类型,用于颜色和 texture 分桶。 | | `getRotationBin(marker)` | `0` | 返回旋转分桶,例如船只按航向分 32 桶。 | | `getBucketKey(marker)` | `String(getRotationBin(marker))` | 返回 texture / geometry 分桶 key。 | | `getPointSizeMultiplier(marker)` | `1` | 单 marker 尺寸倍率。BGP 事件按严重级别、观测站按活跃度使用它。 | | `getUserData(item)` | `item` | 写入 marker 的业务字段。 | ## Icon 配置 `icon.anchor` 可选,默认 `{ x: 0.5, y: 0.5 }`,表示纹理中心对齐 marker 坐标。它只适合小范围的视觉锚点偏移;如果图标主体很大、且需要在地球边缘完整显示,例如登陆点曾使用过的 pin 类图标,不应强行走 `THREE.Points + depthTest`,否则图标主体会被地球深度裁切。 ### Canvas 图标 canvas 图标适合船只、BGP 事件这类需要按状态或旋转动态绘制的符号: ```javascript const vesselIconLayer = createInteractableLayer({ id: "vessels", objectType: "vessel", pointSize: 34, icon: { draw(context, { marker, rotationBin = 0, glow = false, color = "#ffffff" }) { if (!marker.userData.anchored) { context.rotate((rotationBin / 32) * Math.PI * 2); } context.fillStyle = color; context.shadowColor = color; context.shadowBlur = glow ? 14 : 0; context.beginPath(); context.moveTo(0, -37); context.lineTo(28, 32); context.lineTo(0, 17); context.lineTo(-28, 32); context.closePath(); context.fill(); }, }, getRotationBin: getCourseBin, getBucketKey: (marker) => `${marker.userData.anchored ? "anchored" : "moving"}:${getCourseBin(marker)}`, }); ``` 当 `icon.coordinates !== "canvas"` 时,`Interactable` 会先把 context 平移到 atlas 中心;船只这类自己使用中心坐标绘制的图标不需要声明 `coordinates`。 ### SVG / 图片 Asset 图标 asset 图标适合算力中心、BGP 观测站这类已有 SVG 的设施图标: ```javascript const computeCenterIconLayer = createInteractableLayer({ id: "computeCenters", objectType: "compute_center", pointSize: 36, atlasCellSize: 128, icon: { coordinates: "canvas", colorable: false, fitSize: 60, glowBlur: 16, getSource({ marker, item }) { const siteType = marker?.userData?.site_type || item?.site_type || "gpu_cluster"; return COMPUTE_CENTER_ICON_SOURCES[siteType]; }, afterDraw(context, { marker, item }) { if (marker?.userData?.is_estimated ?? item?.is_estimated) { drawComputeCenterEstimatedBadge(context, true); } }, }, }); ``` 使用 asset 时有几个约定: - SVG / 图片文件放在 `frontend/public/earth/assets/icons/`,以 `/earth/assets/icons/name.svg` 引用。 - 原始 SVG 应尽量保留标准 `viewBox` 和路径,不要为了显示大小写死 transform。 - 显示尺寸由 `icon.fitSize` 控制;它可以是数字、`{ width, height }`,也可以是函数。 - `icon.colorable !== false` 且提供状态颜色时,公共层会先把 asset 画到临时 canvas,再用 `source-in` tint 成目标颜色。 - 多色图片或不希望被 tint 的 SVG 应设置 `colorable: false`。 ## 生命周期 常规加载流程: ```javascript export async function loadExampleLayer(_scene, earth) { clearExampleData(earth); const markerData = await fetchExampleData(); await layer.preloadAssets(markerData); layer.setData(markerData); layer.attach(earth); layer.setVisible(showExampleLayer); return { totalCount: layer.getCount() }; } ``` 各方法职责: | 方法 | 说明 | | --- | --- | | `preloadAssets(items)` | 收集 normal / hover / locked 可能用到的 asset source,并用浏览器 `Image` 预加载。canvas draw 图标可跳过。 | | `setData(items)` | 清理旧 points,生成 marker,注册避让,按 bucket 重建 `THREE.Points`。 | | `attach(parent)` | 将图层 group 挂到 Earth root。 | | `setVisible(next)` | 控制 group、points 和 overlay 可见性。 | | `setMarkerState(marker, state)` | 设置 `normal` / `hover` 等状态并触发视觉状态失效。 | | `updateVisualState(focusType, focusObject, camera)` | 更新普通态 opacity / size,并刷新 hover / locked overlay。 | | `getPointerIntersections(options)` | 屏幕空间拾取,返回按像素距离排序的命中结果。 | | `clearData(parent)` | 注销避让、释放 geometry / material、清空 marker 并从 parent 移除 group。 | ## Picking 接入 `Interactable` 不依赖 Three.js 对 `Points` 的默认 raycast。主交互层只要把 Earth、camera、pointer 和命中半径传入: ```javascript const intersects = getVesselPointerIntersections({ earth, camera, pointer, radiusPx: 22, width: window.innerWidth, height: window.innerHeight, }); ``` 公共层会做这些事: 1. 把相机位置转到 Earth local 坐标。 2. 跳过背面 marker。 3. 把 marker world position 投影到屏幕坐标。 4. 用 `radiusPx` 做像素距离命中。 5. 返回最近的候选对象。 拖动地球、惯性旋转、hover 节流这些策略仍属于 `main.js`,因为它们和全局输入状态有关。 ## 同坐标避让 避让默认开启,作用范围是所有通过 `createInteractableLayer()` 创建的图层。公共层会按经纬度或 `THREE.Vector3` 生成 `icon_avoidance_key`,同 key 的 marker 会沿地表切平面排成小圈。 关键点: - `icon_base_position` 保留业务原始位置。 - 避让只改渲染位置和 picking 位置,不改业务经纬度。 - 单个 marker 回到原始位置时会直接使用 `altitudeOffset` 计算出的业务贴地位置。 - 多个 marker 同坐标时,第一圈用 `avoidance.radius`,后续每圈加 `avoidance.step`。 如果某个业务图层需要严格压在原始点位,可以显式关闭: ```javascript createInteractableLayer({ id: "strict-layer", avoidance: { enabled: false }, }); ``` ## 业务动画边界 `Interactable` 当前只负责图标本体和通用 hover / locked overlay。复杂动画仍放在业务模块里,但要跟随 Interactable marker 的位置: - BGP 事件扩散圈由 `bgp.js` 创建独立 ring sprite,并在每帧 `position.copy(marker.position)`。 - BGP 观测站 halo、status core、coverage halo 和覆盖扇形由 `bgp.js` 管理,图标本体由 Interactable 管理。 - 船只轨迹线仍由 `vessels.js` 管理,因为它依赖点击后额外加载的轨迹数据。 这个边界能避免通用接口过早承载所有动画类型。后续如果多个图层复用同一类动画,再把它收进 Interactable 的 `animations` 扩展。 ## 新图层接入清单 1. 在业务文件中准备 marker data,并保留必要的业务字段。 2. 选择 icon 类型:canvas draw、SVG / 图片 asset,或 `getSource()` 动态选择。 3. 配置 `pointSize`、`icon.fitSize`、`colors`、`opacity`、`stateScale`。 4. 如果需要业务尺寸差异,提供 `getPointSizeMultiplier()`。 5. 如果有旋转,提供 `getRotationBin()` 和稳定的 `getBucketKey()`。 6. 加载时先 `preloadAssets()`,再 `setData()`、`attach()`、`setVisible()`。 7. 在 `main.js` 接入 `getPointerIntersections()`,并复用现有 hover / locked 状态更新流程。 8. 在图层样式索引和渲染顺序文档中记录 altitude、renderOrder、pointSize 和动画层级。