diff --git a/VERSION b/VERSION
index bcce5d06..30109231 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-0.45.0
+0.46.0
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index e8ce9ccc..fe38d160 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -8,6 +8,24 @@ This project follows the repository versioning rule:
- `improvement` -> `+0.0.1`(bugfix + 小功能混合)
- `bugfix` -> `+0.0.1`
+## [0.46.0] — 2026-04-30
+
+Released: 2026-04-30
+
+### ✨ Highlights
+- Earth 新增通用 Interactable 图标层,船只、算力中心、BGP 事件与观测站统一使用批量 Points、屏幕拾取、状态 glow 和状态缩放。
+- BGP 事件保留向外扩散圈,观测站保留雷达扫描层,并与 Interactable 主图标解耦到稳定的地表渲染层级。
+- 登陆点回归黄色球形 Sprite,贴近海缆层级并保持更稳定的地表显示和遮挡表现。
+
+### 🔧 Improvements
+- 新增 SVG asset 到 canvas texture 的 Interactable 资产加载路径,支持统一图标资源、缓存和可选染色。
+- 同坐标 Interactable 自动做地表切向避让,降低重叠物件无法选择的问题。
+- 优化 Earth toolbar 初始尺寸注入,避免首次显示原始尺寸后再跳到缩放尺寸。
+- 补充 Interactable 计划、使用说明、图层顺序和 Earth 前端上下文文档。
+- 修复船只 hover/locked 状态仅发光但放大反馈不明显的问题,将已有状态缩放接入通用图标层。
+
+---
+
## [0.45.0] — 2026-04-29
### ✨ Highlights
diff --git a/docs/plans/README.md b/docs/plans/README.md
index 0737f095..1ca4f784 100644
--- a/docs/plans/README.md
+++ b/docs/plans/README.md
@@ -26,6 +26,7 @@
- [earth-news-source-configuration-and-collector-plan.md](/home/ray/dev/linkong/planet/docs/plans/earth-news-source-configuration-and-collector-plan.md)
- [earth-news-cruise-summary-plan.md](/home/ray/dev/linkong/planet/docs/plans/earth-news-cruise-summary-plan.md)
- [earth-vessel-rendering-performance-plan.md](/home/ray/dev/linkong/planet/docs/plans/earth-vessel-rendering-performance-plan.md)
+- [earth-interactable-layer-plan.md](/home/ray/dev/linkong/planet/docs/plans/earth-interactable-layer-plan.md)
- [frontend-public-docs-site-plan.md](/home/ray/dev/linkong/planet/docs/plans/frontend-public-docs-site-plan.md)
- [frontend-ai-playground-development-plan.md](/home/ray/dev/linkong/planet/docs/plans/frontend-ai-playground-development-plan.md)
- [ue5-mvp-fused-plan.md](/home/ray/dev/linkong/planet/docs/plans/ue5-mvp-fused-plan.md)
diff --git a/docs/plans/earth-interactable-layer-plan.md b/docs/plans/earth-interactable-layer-plan.md
new file mode 100644
index 00000000..f6940a58
--- /dev/null
+++ b/docs/plans/earth-interactable-layer-plan.md
@@ -0,0 +1,312 @@
+# Earth Interactable Layer Plan
+
+## 背景
+
+状态:Phase 1 已经开始落地,Phase 2 的 BGP 事件 / 观测站迁移和 Phase 3 的算力中心迁移也已完成。`frontend/public/earth/js/interactable.js` 已新增,AIS 船只、BGP 事件、BGP 观测站和算力中心图层已经改为通过 `createInteractableLayer()` 使用通用批量 `Points`、hover / locked overlay、默认 glow、状态更新、asset icon 预加载、屏幕空间 picking、固定 / 距离缩放和跨 Interactable 同坐标避让。登陆点因 `THREE.Points` 边缘深度裁切和贴地层级要求,已退回专用 `THREE.Sprite` 黄色球路径,并与海缆同高度同 renderOrder。后续阶段聚焦把可复用的扩圈 / 雷达扇形动画正式沉淀成 `animations` 扩展。
+
+当前实现说明和接入示例见:
+
+- [earth-interactable-usage.md](/home/ray/dev/linkong/planet/docs/technical/zh/earth-interactable-usage.md)
+
+当前 AIS 船只图层已经形成了一个适合作为基准的交互图标模式:
+
+- 普通态使用批量 `THREE.Points` 渲染,避免每个对象一个 `Sprite` 带来的 draw call 和透明排序压力。
+- hover / locked 态使用单点 overlay 叠加 glow,不改变普通批次,交互反馈清晰且成本低。
+- moving / anchored 船只通过 canvas 点纹理表达不同形状,moving 船只还按航向分桶。
+- 拾取走屏幕空间命中,拖动和惯性期间跳过高频 hover picking。
+- 图层高度贴近地表,仅保留很小的深度余量,避免“浮在表面层”的观感。
+
+这个模式不应该只服务船只。后续 BGP 事件、BGP 观测站、算力中心、新闻事件、告警、地面传感器等都可能需要“图标类可交互元素”。如果每个图层继续各写一套 icon、glow、hover、locked、动画、picking 和图例逻辑,视觉会漂移,性能策略也会重复分叉。登陆点已经验证为例外:需要完整贴地且不被球面边缘裁切时,专用 Sprite 路径比通用 `Points` 更合适。
+
+目标是把船只图层的成功做法抽象成一个通用接口:业务图层只描述“要画什么、在哪里、怎么交互”,底层统一负责批量渲染、默认 glow、状态 overlay、动画槽位、拾取和生命周期。
+
+## 目标
+
+1. 建立统一的 Earth 交互图标接口,作为未来地表图标类元素的默认入口。
+2. 以 AIS 船只 glow 为默认 glow 视觉,其它图标默认沿用同一套 glow 质感。
+3. 保留图标颜色、状态颜色、hover 放大、locked 强调、dimmed 聚焦、动画扩展等能力。
+4. 支持 canvas / SVG / image icon,不强行要求所有图标都可重着色。
+5. 保持船只当前性能路线:批量绘制普通态,少量 overlay 处理交互态。
+6. 给 BGP 事件扩圈、BGP 观测站雷达扇形等补充动画留出正式扩展点。
+
+## 非目标
+
+- 不在第一阶段重写所有 Earth 图层。
+- 不把卫星、海缆、国家边界、真实地形这类非图标图层纳入同一个接口。
+- 不为了抽象牺牲业务图标的差异表达,例如船只航向、BGP 事件严重级别、观测站雷达扫掠。
+- 不要求图片图标支持运行时重着色;图片图标只能通过预制多状态图片或 overlay tint 做有限表达。
+
+## 核心设计
+
+建议新增一个通用模块,例如:
+
+```text
+frontend/public/earth/js/interactable.js
+```
+
+它导出一个工厂或注册函数:
+
+```js
+createInteractableLayer({
+ id,
+ earth,
+ renderOrder,
+ altitudeOffset,
+ icon,
+ scale,
+ glow,
+ colors,
+ states,
+ animations,
+ picking,
+ data,
+ getPosition,
+ getKind,
+ getRotation,
+ getPayload,
+});
+```
+
+业务模块仍保留自己的数据加载、图例、详情卡字段和业务语义。例如 `vessels.js` 负责 AIS 数据和船型映射,但 icon 渲染、hover overlay、locked overlay、默认 glow 和屏幕空间 picking 可以逐步迁入 `interactable.js`。
+
+## 参数草案
+
+| 参数 | 类型 / 示例 | 默认值 | 说明 |
+| --- | --- | --- | --- |
+| `id` | `"vessels"` | 必填 | 图层唯一标识,用于 debug、picking、legend 和状态缓存。 |
+| `earth` | `THREE.Object3D` | 必填 | 图层挂载目标,通常是 Earth root。 |
+| `renderOrder` | `4.4` | `4` | 普通 icon 批次和 overlay 的基础渲染顺序。 |
+| `altitudeOffset` | `0.2` | `0.2` | 图层高度,语义为 `CONFIG.earthRadius + altitudeOffset`。地表图标默认贴近真实地形基础层。 |
+| `icon` | `{ type, source, draw, size, bins }` | 必填 | 图标来源。支持 canvas draw、SVG URL、image URL、内置 shape。 |
+| `icon.fitSize` | `60` 或 `{ width: 60, height: 60 }` | `atlasCellSize` | asset 图标在 atlas canvas 内的最大绘制尺寸,默认居中等比 contain。SVG / 图片文件只负责原始形状,不需要为了显示大小手写 transform。 |
+| `scale` | `{ base, min, max }` | `{ base: 1 }` | 基础缩放和距离稳定范围。当前船只可映射到 `VESSEL_POINT_SIZE` / `baseScale`。 |
+| `sizeMode` | `"fixed" / "distance"` | `"fixed"` | 是否固定屏幕像素尺寸;非 fixed 时按相机到地表距离做比例缩放。 |
+| `sizeScale` | `{ min, max, referenceFov }` | `{ min: 0.12, max: 3, referenceFov: 75 }` | `sizeMode !== "fixed"` 时的缩放限制和参考视角。 |
+| `glow.enabled` | `true / false` | `true` | 是否启用默认 glow。默认 glow 以船只 hover / locked overlay 为基准。 |
+| `glow.intensity` | `0.0 - 2.0` | `1` | glow 强度,内部映射到 canvas `shadowBlur`、opacity 或 shader uniform。 |
+| `glow.colorMode` | `"state" / "icon" / "fixed"` | `"state"` | glow 颜色来源,默认跟随状态颜色。 |
+| `hover.scale` | `1.0 - 2.0` | `1.18` | hover 放大倍率。当前船只保持同尺寸 glow overlay,接口仍保留放大能力供其它图层使用。 |
+| `hover.mode` | `"scale" / "glow-only" / "custom"` | `"scale"` | hover 反馈方式。船只可用 `"glow-only"`,其它图标默认放大。 |
+| `colors.normal` | `"#4A90D9"` | icon 原色 | 普通态颜色。只有可上色 icon 生效。 |
+| `colors.hover` | `"#7dd3fc"` | normal | hover 态颜色。 |
+| `colors.locked` | `"#ffffff"` | hover | locked 态颜色。 |
+| `colors.dimmed` | `"#9B9B9B"` | normal | 聚焦其它对象时的弱化颜色。 |
+| `colors.byKind` | `{ cargo: "#4A90D9" }` | `{}` | 按业务类型着色,如船型、BGP 严重级别。 |
+| `colorable` | `true / false` | 由 icon 类型推断 | canvas shape 和 SVG mask 通常可上色;图片默认不可上色。 |
+| `opacity` | `{ normal, hover, locked, dimmed }` | 船只当前值 | 各状态透明度。 |
+| `rotation` | `{ enabled, bins, getAngle }` | disabled | 是否按角度分桶,例如船只按 COG 分 32 桶。 |
+| `animations` | `IconAnimationSpec[]` | `[]` | 补充动画列表,例如扩圈、雷达扇形、脉冲、轨迹尾迹。 |
+| `picking.radiusPx` | `22` | `20` | 屏幕空间命中半径。 |
+| `picking.throttleMs` | `100` | `80` | hover picking 节流。 |
+| `picking.skipWhileDragging` | `true` | `true` | 拖动和惯性期间跳过 hover picking。 |
+| `zIndexPolicy` | `"surface-icon"` | `"surface-icon"` | 预设层级策略,避免每个业务图层手写高度和 renderOrder。 |
+| `avoidance.enabled` | `true / false` | `true` | 是否参与跨 Interactable 的同坐标避让。默认开启,同一经纬度下的图标会沿地表切平面小幅排开,方便辨认和选择。 |
+| `avoidance.radius` | `number` | `1.1` | 同坐标避让的第一圈半径,单位为地球本地坐标单位。 |
+| `avoidance.precision` | `number` | `4` | 经纬度归并精度,默认约等于只处理几乎完全重叠的图标。 |
+| `legend` | `{ label, color, shape }[]` | `[]` | 可选图例声明,业务层也可以继续自己导出。 |
+| `metadata` | object | `{}` | 业务扩展数据,不参与渲染但参与 tooltip / info-card / search。 |
+
+## Icon 规格
+
+图标输入建议分三类:
+
+```js
+{
+ type: "canvas-shape",
+ size: 128,
+ draw(ctx, state) {
+ // draw triangle / dot / custom shape
+ },
+}
+```
+
+```js
+{
+ type: "svg-mask",
+ source: "/earth/assets/icons/bgp-event-dot.svg",
+ colorable: true,
+}
+```
+
+```js
+{
+ type: "image",
+ source: "/earth/assets/icons/vendor-logo.png",
+ colorable: false,
+ stateSources: {
+ hover: "/earth/assets/icons/vendor-logo-hover.png",
+ },
+}
+```
+
+颜色策略:
+
+- `canvas-shape` 默认可上色,适合船只、事件点、雷达站这类符号。
+- `svg-mask` 如果能作为 mask 使用,则可上色;如果是完整多色 SVG,则按图片处理。
+- `image` 默认不可上色;需要状态变化时使用 `stateSources` 或额外 glow / ring。
+
+## 默认 Glow 规范
+
+默认 glow 以当前船只 overlay 为视觉基准:
+
+- 普通态尽量不启用 glow,保持地图干净。
+- hover / locked 态叠加同位置 overlay。
+- glow 颜色默认跟随状态颜色或业务类型颜色。
+- glow blur 应该稳定,不随 camera zoom 夸张膨胀。
+- 允许通过 `glow.intensity` 控制强度,但不要让业务图层各自发明完全不同的光晕语言。
+
+建议内部把 glow 拆成两个层次:
+
+1. `textureGlow`:canvas texture 里的 `shadowBlur`,适合小图标 hover / locked。
+2. `effectGlow`:额外 ring / halo / pulse,适合告警、BGP 事件和锁定强调。
+
+## 状态模型
+
+通用状态至少包含:
+
+| 状态 | 触发 | 默认表现 |
+| --- | --- | --- |
+| `normal` | 普通显示 | 批量 Points,使用 normal 颜色和 opacity。 |
+| `hover` | 指针悬停 | 默认放大并显示 glow;船只可配置为同尺寸 glow-only。 |
+| `locked` | 点击锁定 / 详情打开 | 强 glow、更高 opacity,可选 ring 或 pulse。 |
+| `dimmed` | 聚焦其它对象 | 降低 opacity,保留上下文。 |
+| `hidden` | 图层关闭或过滤 | 不参与绘制和 picking。 |
+| `alert` | 业务告警 | 可叠加动画,不替代 locked 状态。 |
+
+状态更新需要增量化:只在 hover 目标、locked 目标、过滤条件、数据版本或相机距离阈值变化时更新,不在每帧遍历全部 icon 写材质属性。
+
+## 动画扩展
+
+动画不直接塞进 icon 基础参数,而是作为 `animations` 列表注册。每个动画声明自己的 geometry / material / update 策略:
+
+```js
+{
+ type: "expanding-ring",
+ when: ["alert", "locked"],
+ color: "state",
+ radiusPx: [10, 42],
+ durationMs: 1400,
+ opacity: [0.8, 0],
+}
+```
+
+```js
+{
+ type: "radar-sweep",
+ when: ["normal", "hover", "locked"],
+ angleDeg: 72,
+ rotationMs: 2600,
+ opacity: 0.36,
+}
+```
+
+首批建议内置动画:
+
+| 动画 | 用例 | 说明 |
+| --- | --- | --- |
+| `pulse-ring` | locked、告警点 | 原地呼吸环,强调选中对象。 |
+| `expanding-ring` | BGP 事件 | 向外扩散的事件波纹。 |
+| `radar-sweep` | BGP 观测站 | 扇形扫描,可持续旋转。 |
+| `orbiting-dot` | 数据流 / collector 活跃态 | 小点绕 icon 环绕,表达活动状态。 |
+| `trail` | 移动目标 | 可选短尾迹,船只或飞机类目标使用。 |
+
+动画必须支持批量或分组绘制,避免为每个对象创建独立的高频更新对象。只有 locked / hover / 少量 alert 对象可以使用单对象 overlay。
+
+## 渲染策略
+
+### 普通态
+
+普通态优先使用分桶 `THREE.Points`:
+
+- 按 icon 类型、可上色策略、旋转分桶、纹理 key 分组。
+- 每组一个 `BufferGeometry`,存 `position`、`color`、必要的 `payloadIndex`。
+- `PointsMaterial.sizeAttenuation = false`,保持屏幕尺寸稳定。
+- `depthTest = true`,`depthWrite = false`,避免遮挡关系破坏地表。
+
+### 交互态
+
+hover / locked 使用少量 overlay:
+
+- overlay 复用 `THREE.Points` 单点对象或小型 ring mesh。
+- overlay texture 从统一 cache 获取。
+- overlay 更新只写当前 hover / locked 的 position、texture、opacity、size。
+
+### 高密度升级
+
+当某类图标超过分桶 Points 的舒适区,才考虑升级:
+
+- `InstancedBufferGeometry` billboard。
+- 自定义 shader 支持 per-instance rotation / scale / opacity。
+- 视口 bbox / LOD / cluster。
+
+这个升级不应该改变业务接口,只替换底层 renderer。
+
+## Picking 策略
+
+沿用船只当前方向:
+
+- 默认屏幕空间 picking,而不是 Three.js 对每个 Sprite / Points 做 raycast。
+- 每个 icon 保留世界坐标和业务 payload。
+- 每次 pointer move 将候选点投影到屏幕,按半径和深度判断命中。
+- 拖动、惯性旋转、相机剧烈变化期间跳过 hover picking。
+- click 时允许做一次更精确的 picking。
+
+后续可以按图层或经纬度网格增加空间索引,减少候选点数量。
+
+## 与现有图层的迁移路径
+
+### Phase 1:抽出船只基准能力
+
+- 从 `vessels.js` 提取 texture cache、canvas icon draw、overlay glow、分桶 Points 创建、状态增量更新。
+- 保持 `vessels.js` 的公开 API 不变:`loadVessels()`、`toggleVessels()`、`getVesselMarkers()` 等继续可用。
+- 新模块先只服务船只,确保视觉没有回退。
+
+### Phase 2:迁移 BGP 事件和观测站
+
+- BGP 事件使用 `canvas-shape`,已接入 `Interactable`。
+- 严重级别映射到 `colors.byKind`,并通过通用 `getPointSizeMultiplier` 保留严重级别尺寸倍率。
+- 当前扩圈效果保留在 BGP 业务动画中,并跟随 `Interactable` marker 位置更新。
+- BGP 观测站主图标已接入 `Interactable`,活跃度映射到颜色和 `getPointSizeMultiplier`。
+- BGP 观测站 halo / 覆盖扇形继续由 BGP 业务动画表达扫描,并跟随 `Interactable` marker 位置更新。
+
+### Phase 3:迁移算力中心并评估登陆点
+
+- 算力中心保留现有业务 icon,但接入统一 hover / locked / glow。(已完成)
+- 登陆点曾接入同一套 `Points` 渲染,但 pin 类 SVG 在地球边缘会被深度测试裁切;当前保留专用 `THREE.Sprite`,并使用 canvas 生成黄色扁平球,贴到海缆层级。后续如要重新设计登陆点,需要先确认图标能在边缘视角完整显示。
+- 检查图例、搜索和 info-card 是否只依赖业务 payload,而不是依赖渲染对象类型。
+
+### Phase 4:形成 Earth 图标层规范
+
+- 在 `docs/technical/zh/earth-frontend-context.md` 记录当前实现入口。
+- 在 `docs/technical/zh/earth-layer-style-reference.md` 记录默认 glow、状态颜色、默认高度和动画参数。
+- 在 `docs/technical/zh/earth-render-layer-order.md` 记录 surface icon renderOrder 范围。
+
+## 风险与约束
+
+- 过早抽象可能让船只这种高质量基准被平均化,因此第一阶段必须以船只视觉不回退为验收标准。
+- 图片 icon 不可上色,接口需要明确 `colorable = false` 的行为,避免业务层误以为颜色一定生效。
+- 动画如果默认开启过多,会重新引入 overdraw 和每帧更新压力;默认只给 hover / locked 或少量 alert 使用。
+- 地形开启时,贴地 icon 需要在高度、`depthTest`、`polygonOffset` 和 renderOrder 之间保持平衡。
+- 统一 glow 不等于所有图标一模一样;业务可以调强度和颜色,但不应破坏整体视觉语言。
+
+## 验收标准
+
+1. 船只迁入通用接口后,普通态、hover、locked、航向、颜色、轨迹和 picking 行为保持一致。
+2. 新增一个 BGP 事件示例图层配置,不需要复制船只渲染代码即可得到 icon、glow、hover 和扩圈动画。
+3. 新增一个 BGP 观测站示例图层配置,不需要自写独立动画循环即可得到雷达扇形。
+4. 关闭图层后对应 icon、overlay、动画和 picking 全部停止。
+5. 高密度数据下普通态仍走批量绘制,hover / locked 只更新少量 overlay。
+6. 文档同步说明默认高度、默认 glow、状态模型和动画扩展点。
+
+## 相关文件
+
+| 文件 | 当前角色 | 未来关系 |
+| --- | --- | --- |
+| `frontend/public/earth/js/vessels.js` | 船只基准实现,包含分桶 Points、hover / locked overlay、默认 glow 形态 | Phase 1 的抽象来源 |
+| `frontend/public/earth/js/constants.js` | 保存船只高度、颜色、透明度、轨迹参数 | 后续可加入通用 surface icon 默认配置 |
+| `frontend/public/earth/js/bgp.js` | BGP 事件和观测站视觉逻辑 | BGP 事件和观测站主图标已接入 Interactable;扩圈、halo 和覆盖扇形仍保留业务动画 |
+| `frontend/public/earth/js/compute-centers.js` | 算力中心 icon 和交互 | 已通过 Interactable 接入统一 Points、overlay、glow 和 picking |
+| `frontend/public/earth/js/cables.js` | 登陆点 icon 和海缆线 | 登陆点当前使用专用 `THREE.Sprite` 黄色球,不再走 Interactable;海缆线仍独立渲染 |
+| `frontend/public/earth/js/main.js` | 当前集中处理 hover、click、locked 和 info-card 入口 | 后续需要接入通用 icon picking 结果 |
+| `docs/technical/zh/earth-layer-style-reference.md` | 当前视觉参数参考 | 实现后同步默认 glow 和通用参数 |
+| `docs/technical/zh/earth-render-layer-order.md` | 当前层级参考 | 实现后同步 surface icon 层级范围 |
diff --git a/docs/technical/en/earth-layer-style-reference.md b/docs/technical/en/earth-layer-style-reference.md
index f51f0c54..369a46f1 100644
--- a/docs/technical/en/earth-layer-style-reference.md
+++ b/docs/technical/en/earth-layer-style-reference.md
@@ -140,17 +140,17 @@ This document records the material, color, opacity, line width, radius offset, a
| Cable line width | `CABLE_CONFIG.line.lineWidth` | `1` | `LineBasicMaterial.linewidth` |
| Cable opacity | `CABLE_CONFIG.line.opacity` | `1.0` | Cable line opacity |
| Cable renderOrder | `CABLE_CONFIG.line.renderOrder` | `1` | Cable line level |
-| Landing point radius offset | `CABLE_CONFIG.landingPoint.altitudeOffset` | `0.48` | Aligns with compute center marker height |
-| Landing point icon texture size | `CABLE_CONFIG.landingPoint.textureSize` | `256` | Canvas size for solid map-pin icon |
-| Landing point icon aspect ratio | `CABLE_CONFIG.landingPoint.iconAspectRatio` | `0.82` | `Sprite.scale.x = height * aspect` |
-| Landing point icon anchor | `CABLE_CONFIG.landingPoint.anchorX / anchorY` | `0.52 / 0.276` | `Sprite.center`, aligns pin tip to landing point lat/lon |
-| Landing point base scale | `CABLE_CONFIG.landingPoint.baseScale` | `12` | Matches compute center sprite height |
+| Landing point radius offset | `CABLE_CONFIG.landingPoint.altitudeOffset` | `0.2` | Same surface height as cable lines, avoiding a floating marker |
+| Landing point sprite height | local `LANDING_POINT_SPRITE_HEIGHT` | `3` | `THREE.Sprite` base height |
+| Landing point reference FOV | local `LANDING_POINT_SIZE_REFERENCE_FOV` | `75` | Matches the current Earth camera FOV |
+| Landing point scale minimum | local `LANDING_POINT_SIZE_SCALE_MIN` | `0.36` | Minimum multiplier at maximum zoom; `3 * 0.36 = 1.08` |
+| Landing point scale maximum | local `LANDING_POINT_SIZE_SCALE_MAX` | `3` | Maximum multiplier at far distance; current minimum zoom reaches roughly `2.50` |
+| Landing point atlas size | local `LANDING_POINT_ATLAS_CELL_SIZE` | `128` | Canvas flat shaded sphere texture size |
| Landing point color | `CABLE_CONFIG.landingPoint.color` | `0xffaa00` | `SpriteMaterial.color` |
| Landing point opacity | `CABLE_CONFIG.landingPoint.opacity` | `1.0` | `SpriteMaterial.opacity` |
-| Landing point renderOrder | `CABLE_CONFIG.landingPoint.renderOrder` | `4.5` | Aligns with compute center surface level |
+| Landing point renderOrder | `CABLE_CONFIG.landingPoint.renderOrder` | `1` | Same level as cable lines; `depthTest: false` keeps the ball whole, while camera-to-center globe occlusion hides back-side points |
| Landing point dim brightness | `landingPointVisual.dimBrightness` | `0.62` | Dim state color multiplier |
| Dimmed landing point color | `landingPointVisual.dimmed.colorRGB` | `{ r: 180, g: 116, b: 28 }` | Dim state color; avoids dark base showing through as a dark hole |
-| Dimmed landing point emissive | `landingPointVisual.dimmed.emissive` | `0x3a2200` | Dim state weak amber self-emission |
| Dimmed landing point opacity | `landingPointVisual.dimmed.opacity` | `0.78` | Dim state opacity; no longer uses low alpha blending with dark base |
## Satellites, Trails, and Footprints
@@ -175,11 +175,13 @@ This document records the material, color, opacity, line width, radius offset, a
| Name | Variable | Current Value | Location / Notes |
| --- | --- | --- | --- |
| Compute center radius offset | `COMPUTE_CENTER_CONFIG.altitudeOffset` | `0.48` | Marker position |
-| Compute center base opacity | `COMPUTE_CENTER_CONFIG.marker.baseOpacity` | `0.88` | `SpriteMaterial.opacity` |
-| Supercomputer marker scale | `COMPUTE_CENTER_CONFIG.marker.supercomputerScale` | `12` | Supercomputer marker |
-| GPU cluster marker scale | `COMPUTE_CENTER_CONFIG.marker.gpuClusterScale` | `12` | GPU marker |
-| Hover scale | `COMPUTE_CENTER_CONFIG.marker.hoverScale` | `1.16` | Hover state |
-| Locked scale | `COMPUTE_CENTER_CONFIG.marker.lockedScale` | `1.22` | Locked state |
+| Compute center point size | local `COMPUTE_CENTER_POINT_SIZE` | `36` | Shared Interactable base size for normal markers and hover / locked overlays |
+| Compute center asset fit size | local `COMPUTE_CENTER_ICON_FIT_SIZE` | `60` | Maximum SVG asset draw size inside the `128x128` atlas canvas, controlled by `icon.fitSize` |
+| Compute center base opacity | `COMPUTE_CENTER_CONFIG.marker.baseOpacity` | `0.88` | Normal `PointsMaterial.opacity` |
+| Supercomputer marker scale | `COMPUTE_CENTER_CONFIG.marker.supercomputerScale` | `12` | Legacy Sprite scale; not directly used by the current Interactable path |
+| GPU cluster marker scale | `COMPUTE_CENTER_CONFIG.marker.gpuClusterScale` | `12` | Legacy Sprite scale; not directly used by the current Interactable path |
+| Hover scale | `COMPUTE_CENTER_CONFIG.marker.hoverScale` | `1.16` | Hover overlay size multiplier |
+| Locked scale | `COMPUTE_CENTER_CONFIG.marker.lockedScale` | `1.22` | Locked overlay size multiplier, with pulse |
| Dimmed scale / opacity | `dimmedScale / dimmedOpacity` | `0.82 / 0.34` | Dim state |
| Supercomputer color | `COMPUTE_CENTER_CONFIG.colors.supercomputer` | `"#38bdf8"` | Marker texture |
| GPU cluster color | `COMPUTE_CENTER_CONFIG.colors.gpu_cluster` | `"#2dd4bf"` | Marker texture |
@@ -190,12 +192,16 @@ This document records the material, color, opacity, line width, radius offset, a
| Name | Variable | Current Value | Location / Notes |
| --- | --- | --- | --- |
-| BGP event radius offset | `BGP_CONFIG.altitudeOffset` | `2.1` | Anomaly marker |
-| BGP collector radius offset | `BGP_CONFIG.collectorAltitudeOffset` | `1.6` | Collector marker |
-| Event base scale | `BGP_CONFIG.marker.eventBaseScale` | `6.2` | Anomaly sprite |
-| Collector base scale | `BGP_CONFIG.marker.collectorBaseScale` | `7.4` | Collector plane |
+| BGP event radius offset | `BGP_CONFIG.altitudeOffset` | `0.48` | BGP event Interactable marker |
+| BGP collector radius offset | `BGP_CONFIG.collectorAltitudeOffset` | `0.2` | BGP collector Interactable marker, aligned with the vessel layer |
+| BGP event point size | local `BGP_EVENT_POINT_SIZE` | `34` | Event Interactable base size, adjusted by severity through `getPointSizeMultiplier()` |
+| BGP event symbol draw size | local `BGP_EVENT_SYMBOL_SIZE` | `60` | Event canvas symbol draw size inside the `128x128` atlas |
+| BGP collector point size | local `BGP_COLLECTOR_POINT_SIZE` | `36` | Collector Interactable base size, adjusted by activity through `getPointSizeMultiplier()` |
+| BGP collector asset fit size | local `BGP_COLLECTOR_ICON_FIT_SIZE` | `60` | Maximum `bgp-broadcast-pin.svg` draw size inside the atlas canvas |
+| Event base scale | `BGP_CONFIG.marker.eventBaseScale` | `6.2` | Event ring anchor |
+| Collector base scale | `BGP_CONFIG.marker.collectorBaseScale` | `7.4` | Collector halo / coverage animation anchor |
| Hover / dim scale | `hoverScale / dimmedScale` | `1.16 / 0.92` | Interaction states |
-| Normal event opacity | `BGP_CONFIG.opacity.normal` | `0.78` | Anomaly sprite |
+| Normal event opacity | `BGP_CONFIG.opacity.normal` | `0.78` | BGP event Interactable normal state |
| Hover opacity | `BGP_CONFIG.opacity.hover` | `1.0` | Hover state |
| Dimmed opacity | `BGP_CONFIG.opacity.dimmed` | `0.24` | Dim state |
| Collector opacity | `BGP_CONFIG.opacity.collector` | `0.62` | Collector state |
diff --git a/docs/technical/zh/README.md b/docs/technical/zh/README.md
index 2ced26da..a1d62650 100644
--- a/docs/technical/zh/README.md
+++ b/docs/technical/zh/README.md
@@ -24,6 +24,7 @@
- [quickstart.md](/home/ray/dev/linkong/planet/docs/technical/zh/quickstart.md):从零启动 Planet 的最短路径
- [manual.md](/home/ray/dev/linkong/planet/docs/technical/zh/manual.md):控制台、`planet.sh`、Earth 和 Docs 的完整使用手册
- [datasource-collector-settings-connectivity.md](/home/ray/dev/linkong/planet/docs/technical/zh/datasource-collector-settings-connectivity.md):数据源目录、采集器设置、连接验证、BarentsWatch 凭证链路
+- [earth-interactable-usage.md](/home/ray/dev/linkong/planet/docs/technical/zh/earth-interactable-usage.md):Earth 地表可交互图标 `Interactable` 的接口、生命周期和接入示例
不适合放入这里的内容:
diff --git a/docs/technical/zh/earth-frontend-context.md b/docs/technical/zh/earth-frontend-context.md
index 2690cb36..3355b3d6 100644
--- a/docs/technical/zh/earth-frontend-context.md
+++ b/docs/technical/zh/earth-frontend-context.md
@@ -249,11 +249,13 @@ Earth 图层按钮现在不应再只有“开/关”两态,而应支持:
AIS 船只图层入口:
- [vessels.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/vessels.js)
+- [interactable.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/interactable.js)
船只图层当前负责:
- 请求 `/api/v1/visualization/geo/vessels`
- 将 BarentsWatch AIS GeoJSON 转为地球局部坐标 marker 数据
+- 通过 `createInteractableLayer()` 注册 Interactable 图标层
- 用按航向分桶的 `THREE.Points` 批量渲染普通船只 marker
- 按船型映射颜色
- 根据航行/停泊状态绘制三角形或圆点纹理
@@ -271,7 +273,7 @@ AIS 船只图层入口:
方向标准以 AIS `course / cog` 为准:从正北开始顺时针。普通态和交互态都通过同一套 canvas 旋转规则生成纹理,避免 hover 后箭头方向和原 marker 不一致。
-船只 hover / click 也不再对渲染对象做 `raycaster.intersectObjects()`。`main.js` 会:
+船只 hover / click 也不再对渲染对象做 `raycaster.intersectObjects()`。`main.js` 只负责传入当前 Earth、camera、pointer 和命中半径,实际命中计算由 `interactable.js` 的图标层接口完成:
1. 拖动地球或惯性旋转时跳过 hover picking。
2. 对 hover picking 做轻量节流。
@@ -293,6 +295,24 @@ AIS 船只图层入口:
图例项颜色来自 `VESSEL_CONFIG.colors`,不要在 `legend.css` 里重新定义业务颜色。新增船型时,应优先改 `vessels.js` 和 `constants.js` 的船型映射,再同步图例项。
+`interactable.js` 是后续地表图标类图层的共用入口。它当前已经承载船只、BGP 事件、BGP 观测站和算力中心图层的批量 `Points`、texture cache、hover / locked overlay、默认 glow、状态增量更新和屏幕空间 picking;BGP 事件的向外扩散圈、BGP 观测站的 halo / 覆盖扇形仍由 `bgp.js` 保留业务动画,但图标本体和 pointer 命中已经接入通用层。新增小型、中心对齐、可以参与深度测试的图标类元素时,应优先复用这个接口,而不是再次复制船只渲染逻辑。
+
+登陆点是当前明确保留的例外:它曾接入 `Interactable`,但 pin 类 SVG 在地球边缘会被 `THREE.Points` 的深度测试裁切成碎片;关闭 depthTest 又会破坏背面遮挡语义。因此登陆点退回 `cables.js` 内的专用 `THREE.Sprite` 路径,并改为 canvas 生成的黄色扁平球纹理。它的 `altitudeOffset` 和 `renderOrder` 与海缆线一致,避免漂在海缆之上;Sprite 本体关闭 `depthTest` 保持球完整,背面可见性由 `isFacingCamera()` 的球体遮挡判断控制。
+
+图标资源可以继续用 canvas draw,也可以放到 `frontend/public/earth/assets/icons/` 后由 `Interactable` 预加载。asset 路径不会在每帧读取;图层加载阶段通过 `preloadAssets()` 只加载一次 SVG / 图片,之后按 `icon source + state + bucket + color` 生成 `CanvasTexture` 并复用。当前算力中心已经从 `assets/icons/compute-supercomputer.svg`、`assets/icons/compute-gpu-cluster.svg` 和备用 `assets/icons/compute-hdd-network.svg` 读取图标,再在 canvas 上叠加估算位置的 `?` badge。
+
+asset 图标大小由 `Interactable` 的 `icon.fitSize` 控制。SVG / 图片文件应尽量保持原始 viewBox 和路径,不要为了在地球上显示成 60x60 而手写 `transform`;`drawAssetIcon()` 会把资源等比 contain 到指定尺寸并居中绘制到 atlas canvas。
+
+`Interactable` 默认使用固定屏幕像素尺寸,适合船只、BGP 事件、BGP 观测站、算力中心这类需要稳定识别的图标。如果某类图标需要跟随相机距离缩放,可以把 `sizeMode` 设为非 `"fixed"`,并用 `sizeScale.min / max / referenceFov` 控制缩放范围;单个 marker 的业务尺寸差异可以通过 `getPointSizeMultiplier()` 表达,例如 BGP 事件按严重级别调整点大小,BGP 观测站按活跃度调整点大小。
+
+`Interactable` 不再把图标本体额外抬离业务高度。`altitudeOffset` 就是 marker、hover glow、locked glow 和 picking 共同使用的地表高度;这样船只图标会继续贴着船只轨迹线,不会因为单独抬高显示位置而显得漂浮。后续如果要解决边缘 glow 裁切,应优先考虑 glow 纹理、overlay 尺寸或图层专属特效,而不是把通用图标层整体抬高。
+
+跨 Interactable 的同坐标避让也在公共层处理。每个 marker 会保留 `icon_base_position` 作为业务原始位置;当多个 Interactable marker 归入同一个经纬度 key 时,公共层会把它们沿地表切平面排成小圈,并刷新已创建的 `THREE.Points` geometry。这样视觉位置和屏幕空间 picking 位置一致,不需要业务层再单独判断“算力中心和 BGP 事件重叠”这类场景。
+
+接口细节、生命周期和接入示例见:
+
+- [earth-interactable-usage.md](/home/ray/dev/linkong/planet/docs/technical/zh/earth-interactable-usage.md)
+
### 视角控制反馈
[controls.js](/home/ray/dev/linkong/planet/frontend/public/earth/js/controls.js) 统一维护 Earth 缩放状态。滚轮缩放、缩放按钮和触屏双指捏合最终都会更新 `zoomLevel`,并通过 `showZoomStatusCapsule()` 显示当前缩放比例:
diff --git a/docs/technical/zh/earth-interactable-usage.md b/docs/technical/zh/earth-interactable-usage.md
new file mode 100644
index 00000000..9605b156
--- /dev/null
+++ b/docs/technical/zh/earth-interactable-usage.md
@@ -0,0 +1,270 @@
+# 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 和动画层级。
diff --git a/docs/technical/zh/earth-layer-style-reference.md b/docs/technical/zh/earth-layer-style-reference.md
index 2225a1e4..92c8115c 100644
--- a/docs/technical/zh/earth-layer-style-reference.md
+++ b/docs/technical/zh/earth-layer-style-reference.md
@@ -146,21 +146,18 @@
| 海缆线宽 | `CABLE_CONFIG.line.lineWidth` | `1` | `LineBasicMaterial.linewidth` |
| 海缆透明度 | `CABLE_CONFIG.line.opacity` | `1.0` | 海缆线 opacity |
| 海缆 renderOrder | `CABLE_CONFIG.line.renderOrder` | `1` | 海缆线层级 |
-| 登陆点半径偏移 | `CABLE_CONFIG.landingPoint.altitudeOffset` | `0.48` | 对齐算力中心贴地表 marker 高度 |
-| 登陆点 icon 贴图尺寸 | `CABLE_CONFIG.landingPoint.textureSize` | `256` | canvas 渲染 EPS 参考图的实心 map-pin,中间圆孔透明镂空 |
-| 登陆点 icon 宽高比 | `CABLE_CONFIG.landingPoint.iconAspectRatio` | `0.82` | `Sprite.scale.x = height * aspect` |
-| 登陆点 icon 锚点 | `CABLE_CONFIG.landingPoint.anchorX / anchorY` | `0.52 / 0.276` | `Sprite.center`,将 pin 下端点对齐登陆点经纬度 |
-| 登陆点基础缩放 | `CABLE_CONFIG.landingPoint.baseScale` | `12` | 对齐算力中心等地表 icon 的 sprite 高度 |
+| 登陆点半径偏移 | `CABLE_CONFIG.landingPoint.altitudeOffset` | `0.2` | 与海缆线同层贴地,避免凌空 |
+| 登陆点 sprite 高度 | local `LANDING_POINT_SPRITE_HEIGHT` | `3` | `THREE.Sprite` 基准高度 |
+| 登陆点缩放参考 FOV | local `LANDING_POINT_SIZE_REFERENCE_FOV` | `75` | 与当前 Earth 相机 FOV 一致 |
+| 登陆点缩放下限 | local `LANDING_POINT_SIZE_SCALE_MIN` | `0.36` | 地球放到最大时的最小倍率;`3 * 0.36 = 1.08` |
+| 登陆点缩放上限 | local `LANDING_POINT_SIZE_SCALE_MAX` | `3` | 远距离时的最大倍率;当前最小缩放约只能到 `2.50` |
+| 登陆点 atlas 尺寸 | local `LANDING_POINT_ATLAS_CELL_SIZE` | `128` | canvas 扁平立体球纹理尺寸 |
| 登陆点颜色 | `CABLE_CONFIG.landingPoint.color` | `0xffaa00` | `SpriteMaterial.color` |
-| 登陆点 emissive | `CABLE_CONFIG.landingPoint.emissive` | `0x442200` | 兼容旧球体材质;sprite 不使用 |
-| 登陆点 emissive 强度 | `CABLE_CONFIG.landingPoint.emissiveIntensity` | `0.5` | 兼容旧球体材质;sprite 不使用 |
| 登陆点透明度 | `CABLE_CONFIG.landingPoint.opacity` | `1.0` | `SpriteMaterial.opacity` |
-| 登陆点 renderOrder | `CABLE_CONFIG.landingPoint.renderOrder` | `4.5` | 对齐算力中心地表设施层级 |
+| 登陆点 renderOrder | `CABLE_CONFIG.landingPoint.renderOrder` | `1` | 与海缆线同层;`depthTest: false` 保持球体完整,背面通过相机到球心的球体遮挡判断隐藏 |
| 登陆点 dim 亮度系数 | `landingPointVisual.dimBrightness` | `0.62` | dim 状态颜色乘数 |
| 相关登陆点高亮 opacity | `landingPointVisual.related.opacityBase / opacityPulse` | `0.8 / 0.2` | 高亮脉冲 |
| 非相关登陆点颜色 | `landingPointVisual.dimmed.colorRGB` | `{ r: 180, g: 116, b: 28 }` | dim 状态颜色,避免黑色基座透出成暗洞 |
-| 非相关登陆点 emissive | `landingPointVisual.dimmed.emissive` | `0x3a2200` | dim 状态弱琥珀自发光 |
-| 非相关登陆点 emissive 强度 | `landingPointVisual.dimmed.emissiveIntensity` | `0.18` | dim 状态弱发光强度 |
| 非相关登陆点 opacity | `landingPointVisual.dimmed.opacity` | `0.78` | dim 状态透明度,不再用低 alpha 混黑底 |
## 卫星、轨迹和 footprint
@@ -212,11 +209,13 @@ AIS 船只普通态使用批量 `THREE.Points`,不是逐船 `THREE.Sprite`。
| 正式名称 | 变量名 | 当前值 | 使用位置 / 说明 |
| --- | --- | --- | --- |
| 算力中心半径偏移 | `COMPUTE_CENTER_CONFIG.altitudeOffset` | `0.48` | marker 位置 |
-| 算力中心基础透明度 | `COMPUTE_CENTER_CONFIG.marker.baseOpacity` | `0.88` | `SpriteMaterial.opacity` |
-| 超算 marker 缩放 | `COMPUTE_CENTER_CONFIG.marker.supercomputerScale` | `12` | 超算 marker |
-| GPU 集群 marker 缩放 | `COMPUTE_CENTER_CONFIG.marker.gpuClusterScale` | `12` | GPU marker |
-| hover 缩放 | `COMPUTE_CENTER_CONFIG.marker.hoverScale` | `1.16` | hover 状态 |
-| locked 缩放 | `COMPUTE_CENTER_CONFIG.marker.lockedScale` | `1.22` | locked 状态 |
+| 算力中心点像素尺寸 | local `COMPUTE_CENTER_POINT_SIZE` | `36` | `Interactable` 普通 marker 与 hover / locked overlay 共享基准尺寸 |
+| 算力中心 asset fit size | local `COMPUTE_CENTER_ICON_FIT_SIZE` | `60` | SVG asset 在 `128x128` atlas canvas 内的最大绘制尺寸,由 `icon.fitSize` 控制 |
+| 算力中心基础透明度 | `COMPUTE_CENTER_CONFIG.marker.baseOpacity` | `0.88` | 普通 `PointsMaterial.opacity` |
+| 超算 marker 缩放 | `COMPUTE_CENTER_CONFIG.marker.supercomputerScale` | `12` | 旧 Sprite 缩放参数;当前 Interactable 路径不再直接使用 |
+| GPU 集群 marker 缩放 | `COMPUTE_CENTER_CONFIG.marker.gpuClusterScale` | `12` | 旧 Sprite 缩放参数;当前 Interactable 路径不再直接使用 |
+| hover 缩放 | `COMPUTE_CENTER_CONFIG.marker.hoverScale` | `1.16` | hover overlay 尺寸倍率 |
+| locked 缩放 | `COMPUTE_CENTER_CONFIG.marker.lockedScale` | `1.22` | locked overlay 尺寸倍率,并叠加 pulse |
| dimmed 缩放 / 透明度 | `dimmedScale / dimmedOpacity` | `0.82 / 0.34` | dim 状态 |
| 超算颜色 | `COMPUTE_CENTER_CONFIG.colors.supercomputer` | `"#38bdf8"` | marker texture |
| GPU 集群颜色 | `COMPUTE_CENTER_CONFIG.colors.gpu_cluster` | `"#2dd4bf"` | marker texture |
@@ -227,12 +226,16 @@ AIS 船只普通态使用批量 `THREE.Points`,不是逐船 `THREE.Sprite`。
| 正式名称 | 变量名 | 当前值 | 使用位置 / 说明 |
| --- | --- | --- | --- |
-| BGP 事件半径偏移 | `BGP_CONFIG.altitudeOffset` | `2.1` | anomaly marker |
-| BGP collector 半径偏移 | `BGP_CONFIG.collectorAltitudeOffset` | `1.6` | collector marker |
-| 事件基础缩放 | `BGP_CONFIG.marker.eventBaseScale` | `6.2` | anomaly sprite |
-| collector 基础缩放 | `BGP_CONFIG.marker.collectorBaseScale` | `7.4` | collector plane |
+| BGP 事件半径偏移 | `BGP_CONFIG.altitudeOffset` | `0.48` | BGP 事件 Interactable marker |
+| BGP collector 半径偏移 | `BGP_CONFIG.collectorAltitudeOffset` | `0.2` | BGP 观测站 Interactable marker,与船只同层贴地 |
+| BGP 事件点像素尺寸 | local `BGP_EVENT_POINT_SIZE` | `34` | 事件 icon 的 Interactable 基准尺寸,按严重级别通过 `getPointSizeMultiplier()` 调整 |
+| BGP 事件符号绘制尺寸 | local `BGP_EVENT_SYMBOL_SIZE` | `60` | 事件 canvas 符号在 `128x128` atlas 中的绘制尺寸 |
+| BGP collector 点像素尺寸 | local `BGP_COLLECTOR_POINT_SIZE` | `36` | 观测站 Interactable 基准尺寸,按活跃度通过 `getPointSizeMultiplier()` 调整 |
+| BGP collector asset fit size | local `BGP_COLLECTOR_ICON_FIT_SIZE` | `60` | `bgp-broadcast-pin.svg` 在 atlas canvas 内的最大绘制尺寸 |
+| 事件基础缩放 | `BGP_CONFIG.marker.eventBaseScale` | `6.2` | 事件扩散圈锚点 |
+| collector 基础缩放 | `BGP_CONFIG.marker.collectorBaseScale` | `7.4` | 观测站 halo / 覆盖动画锚点 |
| hover / dim 缩放 | `hoverScale / dimmedScale` | `1.16 / 0.92` | 交互状态 |
-| 普通事件透明度 | `BGP_CONFIG.opacity.normal` | `0.78` | anomaly sprite |
+| 普通事件透明度 | `BGP_CONFIG.opacity.normal` | `0.78` | BGP 事件 Interactable 普通态 |
| hover 透明度 | `BGP_CONFIG.opacity.hover` | `1.0` | hover 状态 |
| dimmed 透明度 | `BGP_CONFIG.opacity.dimmed` | `0.24` | dim 状态 |
| collector 透明度 | `BGP_CONFIG.opacity.collector` | `0.62` | collector 状态 |
@@ -244,8 +247,8 @@ AIS 船只普通态使用批量 `THREE.Points`,不是逐船 `THREE.Sprite`。
| region 色 | `BGP_CONFIG.regionColor` | `0x2dd4bf` | 区域覆盖 |
| BGP ring 缩放 | `BGP_CONFIG.ring.scaleA / scaleB` | `2.5 / 3.4` | anomaly ring |
| BGP ring 透明度 | `BGP_CONFIG.ring.opacity` | `0.5` | anomaly ring |
-| collector marker renderOrder | inline | `3` | `marker.renderOrder` |
-| anomaly marker renderOrder | inline | `5` normal, `7` active | `marker.renderOrder` |
+| collector marker renderOrder | local `BGP_COLLECTOR_RENDER_ORDER` | `4.4` | 观测站主图标,与船只同层 |
+| anomaly marker renderOrder | local `BGP_EVENT_RENDER_ORDER` | `4.5` | BGP 事件主图标,与算力中心同层 |
## 天体与星空
diff --git a/docs/technical/zh/earth-render-layer-order.md b/docs/technical/zh/earth-render-layer-order.md
index d83485d6..2e62d766 100644
--- a/docs/technical/zh/earth-render-layer-order.md
+++ b/docs/technical/zh/earth-render-layer-order.md
@@ -21,17 +21,17 @@
| 0.86 | 海陆基座填充 | `country-boundaries.js` | `landAltitudeOffset`; 海洋 `#010609`,陆地 `#080f1b` | 禁用 raycast | 即使国界线关闭,基座地图仍保持可用。 |
| 0.96 | 高清 Earth 材质 | `earth.js` | `textureOverlayAltitudeOffset` | 可见时作为地表拾取目标 | 高清材质始终压过海陆基座填充。 |
| 1 | 大气辉光和云图 | `earth.js` | 大气 / 云层球 | 不走普通对象选择路径 | 云图由“大气云图”图层开关控制。 |
-| 1 | 海缆 | `cables.js` | `CABLE_CONFIG.line.renderOrder` | 海缆拾取路径 | 保持现有海缆层级。 |
+| 1 | 海缆 / 登陆点 | `cables.js` | 海缆线和登陆点都使用 `renderOrder = 1`;半径偏移都为 `0.2`;登陆点是专用 `THREE.Sprite` 黄色扁平球 | 海缆走海缆拾取路径;登陆点 `depthTest: false` 保持球体完整,并用相机到球心的球体遮挡判断避免背面穿透 | 登陆点和海缆同层贴地,避免地表设施层的凌空感。 |
| 1.2 | 真实地形 | `earth.js`, `terrain.js` | `TERRAIN_CONFIG.baseRadiusOffset` 加地形位移 | 禁用 raycast | 地形压过高清材质;高清材质关闭时临时隐藏,重新开启后恢复原状态。 |
| 2.05 | 经纬线 | `earth.js` | `CONFIG.earthRadius + 0.14` | 禁用 raycast | 低透明度显示在高清材质上。 |
| 2.2 | 国界线 | `country-boundaries.js` | `lineAltitudeOffset = 0.115` | `depthTest: true`,禁用 raycast | 略高于高清材质 `0.10`,低于地形基准 `0.16`,减少悬浮感;地形 `depthWrite: false`,所以地形开启时仍可见。 |
| 2.29 | 国界 hover 光晕 | `country-boundaries.js` | hover 半径加 glow 偏移 | `depthTest: false`,禁用 raycast | 用 additive 光晕增强交界边和地形开启时的 hover 可见性。 |
| 2.3 | 国界 hover 实线 | `country-boundaries.js` | `hoverAltitudeOffset = 0.14` | `depthTest: false`,禁用 raycast | 霓虹红橘 hover 线;中国和中国(台湾)共享高亮组。 |
| 3 | 卫星 footprint 填充 | `satellites.js` | `GROUND_FOOTPRINT_RENDER_ORDER` | depth-tested,Group renderOrder 保持 0 | Footprint 在国界线之上,但在算力中心和卫星之下。 |
-| 3-5 | BGP 标记和覆盖层 | `bgp.js` | 各 marker 自身 renderOrder | BGP 拾取路径 | 保持现有 BGP 视觉层级。 |
+| 3-4.5 | BGP 观测站、事件扩散圈和事件 marker | `bgp.js`, `interactable.js` | BGP 观测站和事件 marker 均使用 `Interactable` 批量 `THREE.Points`;事件 marker 使用 `BGP_EVENT_RENDER_ORDER = 4.5`;观测站主图标使用 `BGP_COLLECTOR_RENDER_ORDER = 4.4` 和 `BGP_CONFIG.collectorAltitudeOffset = 0.2`;事件 overlay 进入 `bgp-event-overlay-layer`;观测站 halo 和覆盖扇形进入 `bgp-collector-radar-layer` | BGP 事件和观测站都通过 `Interactable` 屏幕空间 picking,并参与同坐标避让 | BGP 观测站主图标与船只同层;BGP 事件与算力中心同层;向外扩散圈、观测站雷达/覆盖动画继续由 BGP 业务逻辑驱动。 |
| 4.3 | AIS 船只轨迹线 | `vessels.js` | `VESSEL_RENDER_ORDER - 0.1`;`CONFIG.earthRadius + VESSEL_CONFIG.track.altitudeOffset` | 跟随船只显隐,不单独参与拾取 | 选中船只后显示最近轨迹,低于船只 marker。 |
-| 4.4 | AIS 船只 marker | `vessels.js` | `VESSEL_RENDER_ORDER`;`CONFIG.earthRadius + VESSEL_CONFIG.altitudeOffset`;普通 marker 为分桶 `THREE.Points`,hover / locked 为单点 `THREE.Points` overlay | `depthTest: true`;`main.js` 使用屏幕空间 picking,只取正面 marker | 航行船只用三角点纹理,停泊/低速用圆点;普通态无 glow,交互态叠加同尺寸 glow;低于算力中心 `4.5`。 |
-| 4.5 | 算力中心 | `compute-centers.js` | `COMPUTE_CENTER_RENDER_ORDER` | 算力中心拾取路径 | 地表设施,保持在卫星下方。 |
+| 4.4 | AIS 船只 marker | `vessels.js`, `interactable.js` | `VESSEL_RENDER_ORDER`;业务高度为 `CONFIG.earthRadius + VESSEL_CONFIG.altitudeOffset`;普通 marker 为分桶 `THREE.Points`,hover / locked 为单点 `THREE.Points` overlay | `depthTest: true`;`main.js` 使用屏幕空间 picking,只取正面 marker;参与 Interactable 同坐标避让 | 航行船只用三角点纹理,停泊/低速用圆点;普通态无 glow,交互态叠加同尺寸 glow;低于算力中心 `4.5`。 |
+| 4.5 | 算力中心 | `compute-centers.js`, `interactable.js` | 使用 `COMPUTE_CENTER_RENDER_ORDER` 并由 `Interactable` 绘制 | 通过 `Interactable` 屏幕空间 picking,参与同坐标避让 | 地表设施层,保持在卫星下方。登陆点已下沉到海缆层。 |
| 5 | 卫星背景点 | `satellites.js` | 固定 renderOrder | 屏幕空间卫星拾取 | 位于卫星点下方。 |
| 6 | 卫星点 | `satellites.js` | 固定 renderOrder | 屏幕空间卫星拾取 | 卫星点压过 footprint 和算力中心。 |
| 12+ | 卫星锁定 ring、halo、预测轨道 | `satellites.js` | `SATELLITE_CONFIG.overlayRenderOrder` 及偏移 | 卫星覆盖层路径 | 用于选中 / 锁定卫星强调。 |
diff --git a/docs/version-history.md b/docs/version-history.md
index 613c4bd8..dc52951d 100644
--- a/docs/version-history.md
+++ b/docs/version-history.md
@@ -16,12 +16,13 @@
## Current Version
- `main` 当前主线历史推导到:`0.16.5`
-- `dev` 当前开发分支历史推导到:`0.45.0`
+- `dev` 当前开发分支历史推导到:`0.46.0`
## Timeline
| Version | Type | Branch | Commit | Summary |
| --- | --- | --- | --- | --- |
+| `0.46.0` | feature | `dev` | `pending` | Earth 新增通用 Interactable 图标层,统一船只、算力中心、BGP 事件/观测站交互图标,并优化登陆点与 toolbar 初始渲染 |
| `0.45.0` | feature | `dev` | `pending` | 新增采集任务 fetching 阶段量化进度,收敛 AI Provider 运行期环境注入和 Docker build context |
| `0.44.2` | bugfix | `dev` | `pending` | 补充 Earth 船只批量渲染、屏幕拾取、图层顺序、样式参考和性能计划状态文档 |
| `0.44.1` | bugfix | `dev` | `pending` | 优化 Earth 船只批量渲染性能,修复拖动卡顿、拾取错位、交互态方向/尺寸和地表压盖问题 |
diff --git a/frontend/package.json b/frontend/package.json
index a79f7de2..67657882 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,6 +1,6 @@
{
"name": "planet-frontend",
- "version": "0.45.0",
+ "version": "0.46.0",
"private": true,
"packageManager": "bun@1",
"dependencies": {
diff --git a/frontend/public/earth/assets/icons/bgp-broadcast-pin.svg b/frontend/public/earth/assets/icons/bgp-broadcast-pin.svg
new file mode 100644
index 00000000..df937883
--- /dev/null
+++ b/frontend/public/earth/assets/icons/bgp-broadcast-pin.svg
@@ -0,0 +1,3 @@
+
diff --git a/frontend/public/earth/assets/icons/compute-gpu-cluster.svg b/frontend/public/earth/assets/icons/compute-gpu-cluster.svg
index 4b68e692..49f9913e 100644
--- a/frontend/public/earth/assets/icons/compute-gpu-cluster.svg
+++ b/frontend/public/earth/assets/icons/compute-gpu-cluster.svg
@@ -1,16 +1,6 @@
-