Threejs,地图标签绘制,碰撞检测逻辑

地图标签排布与碰撞检测逻辑文档

本文档详细描述了 3D 地图场景中 HTML 标签(CSS2DObject)的自动排布、碰撞检测及响应式适配逻辑。


1. 核心设计思想

地图标签系统旨在解决高密度点位下的可视化问题。当多个场站图标聚集在一起时,系统通过**"就近探测""边缘重分布"**两套策略,确保标签既不互相重叠,也不遮挡地图关键点位,并自动在小屏幕下切换紧凑布局。


2. 响应式与全局配置

系统通过文件顶部的配置对象进行全局控制:

2.1 RESPONSIVE_CONFIG (响应式配置)

根据 window.innerWidth < 2000px (1K屏幕) 自动切换参数:

  • labelHeight / labelBaseWidth:定义标签在碰撞计算中占用的矩形尺寸。
  • visualGap:视觉上的偏移间距(标签离图标多远)。
  • collisionGap:碰撞检测时的敏感间距(用于判定是否重叠)。
  • marginX / marginY:边缘分布时,标签离地图包围盒的距离(即连线长度)。
  • getStep():动态计算标签排队的步长,标签越多,间距越紧凑。

2.2 COLLISION_CONFIG (碰撞策略)

  • checkIconCollision:布尔值。若开启,标签会避开地图上的图标;若关闭,标签只在互相之间避让,从而减少连线外排的情况。

3. 标签排布流程

第一阶段:多方位就近尝试 (Local Probing)

对于每一个场站,程序按以下优先级尝试四个方位:

  1. 右侧 -> 2. 左侧 -> 3. 下方 -> 4. 上方

检测逻辑

  • 在每个方位构建一个虚拟的"探测矩形"(基于 collisionGap)。
  • 执行碰撞检测(见第 4 节)。
  • 若不冲突:直接在此方位渲染标签,不显示指引连线。
  • 若全部冲突:进入第二阶段。

第二阶段:边缘重分布 (Edge Redistribution)

当就近位置均不可用时,标签会被分配到地图的四个边缘区域:

  1. 象限划分:根据场站相对于地图中心的角度,将其划分为 左、右、上、下 四个组。
  2. 抗交叉排序:对每个组内的标签按坐标排序,确保指引连线在从内向外拉伸时互不交叉。
  3. 对齐渲染 :按照 getStep() 计算出的动态间距,将标签均匀排列在 marginX/marginY 划定的边界线上,并绘制折线。

4. 碰撞检测算法 (Collision Algorithm)

系统采用 AABB 矩形碰撞检测 算法。判定冲突的条件如下:

  1. 标签 vs 图标:当前标签矩形是否覆盖了地图上其他任何一个场站图标的"敏感区域"。
  2. 标签 vs 标签:当前标签矩形是否与已经确定位置的(已渲染的)其他标签矩形重叠。

5. 交互与同步

  • 自动重绘 (Resize) :系统监听 window.resize 事件,采用 200ms 防抖,在窗口大小变化后自动重新计算全图标签的排布。
  • 视觉微调 :通过 CSS transform 进行 1px 的向上微调,以在视觉上修正由于文字垂直重心导致的对齐偏差,该微调不参与物理碰撞计算。

6. 参数微调指南

若需调整表现,建议按以下顺序操作:

  • 若连线太长 :减小 marginXmarginY
  • 若标签排得太挤 :调大 getStep 中的 base 值。
  • 若想让更多标签留在地图内
    • 调小 collisionGap
    • checkIconCollision 设为 false
    • 调小 RESPONSIVE_CONFIG 中的 labelHeight

7. 核心逻辑实现 (伪代码)

以下展示了 setLabel 方法内部的核心处理逻辑:

typescript 复制代码
function setLabel(data) {
  // 1. 初始化与投影
  const projectedPoints = data.map(p => project(p.position));
  const occupiedRects = []; // 存储已确定的标签位置
  const iconRects = projectedPoints.map(p => getIconRect(p));

  // 2. 遍历所有点位进行排布尝试
  projectedPoints.forEach(p => {
    const labelSize = getResponsiveLabelSize(p.name);
    let found = false;

    // 策略 A: 多方位就近尝试 (右、左、下、上)
    const probeDirections = [
      { dx: visualGap + labelW/2, dy: 0, collisionDx: collisionGap + ... },
      { dx: -(visualGap + labelW/2), dy: 0, ... },
      ...
    ];

    for (const dir of probeDirections) {
      const tryRect = createRect(p.x + dir.collisionDx, p.y + dir.collisionDy, labelSize);
      
      // 执行碰撞检测
      if (!isColliding(tryRect, iconRects) && !isColliding(tryRect, occupiedRects)) {
        renderLabel(p, dir.offset); // 原地渲染
        occupiedRects.push(tryRect);
        found = true;
        break;
      }
    }

    // 策略 B: 若就近全部碰撞,则存入待外排列表
    if (!found) fallbackPoints.push(p);
  });

  // 3. 处理外排标签 (边缘分布逻辑)
  const quadrants = splitToQuadrants(fallbackPoints); // 划分为 左/右/上/下 四个组
  
  quadrants.forEach(group => {
    group.sort(); // 排序防止连线交叉
    const step = getDynamicStep(group.length); // 获取压缩后的间距
    
    group.forEach((p, index) => {
      const finalPos = calculateEdgePos(index, step);
      renderLabelWithLine(p, finalPos); // 渲染带指引线的标签
    });
  });
}

/**
 * 矩形碰撞检测 AABB
 */
function isColliding(r1, r2) {
  return !(r1.x + r1.w < r2.x || r2.x + r2.w < r1.x || 
           r1.y + r1.h < r2.y || r2.y + r2.h < r1.y);
}
相关推荐
qq_12084093712 小时前
Three.js 工程向:GPU Overdraw 诊断与前端渲染优化
前端
纯爱掌门人2 小时前
聊聊 HarmonyOS 上的应用内通知授权弹窗
前端·harmonyos·arkts
Cdlblbq2 小时前
搜索会员中心 创作中心Vue2项目一键打包成桌面应用
前端·javascript·vue.js·electron
eason_fan2 小时前
前端避坑指南:一文吃透 npm 幽灵依赖(Phantom Dependency)
前端·前端工程化
前端小万2 小时前
2026年3月面20个前端
前端
葡萄城技术团队3 小时前
智慧表格(SpreadJS + AI):拥抱 Web 端对话式办公新时代
前端·人工智能
OpenTiny社区3 小时前
电商系统集成GenUI SDK实操指南
前端·开源·ai编程
A_nanda3 小时前
vue实现后端传输逐帧图像数据
前端·javascript·vue.js
YGY顾n凡3 小时前
我开源了一个项目:一句话创造一个AI世界!
前端·后端·aigc