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

271 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 和动画层级。