12 KiB
智能星球 Interactable 使用说明
Interactable 是智能星球地表“图标类可交互元素”的通用渲染入口。它把船只图层验证过的模式抽成公共能力:普通态用批量 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 的边界是:公共层负责“图标怎么在地球上稳定显示和被选中”,业务层负责“数据从哪里来、图标表达什么语义、详情卡展示什么、是否有额外动画”。
入口
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 态可选呼吸缩放,支持 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 | |
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-intint 成目标颜色。- 多色图片或不希望被 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,
});
公共层会做这些事:
- 把相机位置转到 Earth local 坐标。
- 跳过背面 marker。
- 把 marker world position 投影到屏幕坐标。
- 用
radiusPx做像素距离命中。 - 返回最近的候选对象。
拖动地球、惯性旋转、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 扩展。
新图层接入清单
- 在业务文件中准备 marker data,并保留必要的业务字段。
- 选择 icon 类型:canvas draw、SVG / 图片 asset,或
getSource()动态选择。 - 配置
pointSize、icon.fitSize、colors、opacity、stateScale。 - 如果需要业务尺寸差异,提供
getPointSizeMultiplier()。 - 如果有旋转,提供
getRotationBin()和稳定的getBucketKey()。 - 加载时先
preloadAssets(),再setData()、attach()、setVisible()。 - 在
main.js接入getPointerIntersections(),并复用现有 hover / locked 状态更新流程。 - 在图层样式索引和渲染顺序文档中记录 altitude、renderOrder、pointSize 和动画层级。