Files
planet/docs/technical/zh/earth-interactable-usage.md
2026-04-30 04:42:29 +08:00

12 KiB
Raw Blame History

Earth Interactable 使用说明

Interactable 是 Earth 地表“图标类可交互元素”的通用渲染入口。它把船只图层验证过的模式抽成公共能力:普通态用批量 THREE.Pointshover / 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 的边界是:公共层负责“图标怎么在地球上稳定显示和被选中”,业务层负责“数据从哪里来、图标表达什么语义、详情卡展示什么、是否有额外动画”。

入口

import { createInteractableLayer } from "./interactable.js";

核心调用形态:

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",
});

业务模块通常只暴露一层薄封装:

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 态可选呼吸缩放,支持 enabledspeedamplitude
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
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 事件这类需要按状态或旋转动态绘制的符号:

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 的设施图标:

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

生命周期

常规加载流程:

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 和命中半径传入:

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

如果某个业务图层需要严格压在原始点位,可以显式关闭:

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 / 图片 assetgetSource() 动态选择。
  3. 配置 pointSizeicon.fitSizecolorsopacitystateScale
  4. 如果需要业务尺寸差异,提供 getPointSizeMultiplier()
  5. 如果有旋转,提供 getRotationBin() 和稳定的 getBucketKey()
  6. 加载时先 preloadAssets(),再 setData()attach()setVisible()
  7. main.js 接入 getPointerIntersections(),并复用现有 hover / locked 状态更新流程。
  8. 在图层样式索引和渲染顺序文档中记录 altitude、renderOrder、pointSize 和动画层级。