//
// Created by Lenn on 2025/12/16.
//

#include "glwidget.h"

#include <qevent.h>
#include <qlogging.h>
#include <qminmax.h>
#include <qopenglext.h>
#include <qopenglshaderprogram.h>
#include <qstringliteral.h>
#include <qvectornd.h>

#include <QByteArray>
#include <QDebug>
#include <QFile>
#include <QFontMetrics>
#include <QImage>
#include <QOpenGLShaderProgram>
#include <QPainter>
#include <QPen>
#include <QRectF>
#include <QVector2D>
#include <QVector3D>
#include <QVector4D>
#include <QtMath>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <limits>

#include "globalhelper.h"


// 读取文本文件内容（这里主要用来从 Qt Resource `:/shaders/...` 读取 shader
// 源码）
static QByteArray readFile(const QString& path) {
  QFile f(path);
  if (!f.open(QIODevice::ReadOnly)) return {};
  return f.readAll();
}

// 简单的 4x4 单位矩阵（目前只用于保留的 float[16] 版本）
static void matIdentity(float m[16]) {
  for (int i = 0; i < 16; i++) {
    m[i] = 0;
  }

  m[0] = m[5] = m[10] = m[15] = 1;
}

static QVector3D toColorVec(const QColor& color) {
  return QVector3D(color.redF(), color.greenF(), color.blueF());
}

GLWidget::GLWidget(QWidget* parent) : QOpenGLWidget(parent) {
  setMinimumSize(640, 480);
}

GLWidget::~GLWidget() {
  makeCurrent();
  delete m_bgProg;
  delete m_panelProg;
  delete m_dotsProg;
  delete m_heatmapProg;
  delete m_baseProg;

  if (m_panelIbo) glDeleteBuffers(1, &m_panelIbo);
  if (m_panelVbo) glDeleteBuffers(1, &m_panelVbo);
  if (m_panelVao) glDeleteVertexArrays(1, &m_panelVao);
  if (m_dotsVao) glDeleteVertexArrays(1, &m_dotsVao);
  if (m_dotsVbo) glDeleteBuffers(1, &m_dotsVbo);
  if (m_instanceVbo) glDeleteBuffers(1, &m_instanceVbo);
  if (m_bgVbo) glDeleteBuffers(1, &m_bgVbo);
  if (m_bgVao) glDeleteVertexArrays(1, &m_bgVao);

  if (m_dotTex) glDeleteTextures(1, &m_dotTex);
  if (m_heatmapVao) glDeleteVertexArrays(1, &m_heatmapVao);
  if (m_heatmapVbo) glDeleteBuffers(1, &m_heatmapVbo);
  if (m_heatmapIbo) glDeleteBuffers(1, &m_heatmapIbo);
  if (m_baseVao) glDeleteVertexArrays(1, &m_baseVao);
  if (m_baseVbo) glDeleteBuffers(1, &m_baseVbo);
  if (m_baseIbo) glDeleteBuffers(1, &m_baseIbo);
  if (m_skirtVao) glDeleteVertexArrays(1, &m_skirtVao);
  if (m_skirtVbo) glDeleteBuffers(1, &m_skirtVbo);
  if (m_skirtIbo) glDeleteBuffers(1, &m_skirtIbo);
  if (m_heightTex) glDeleteTextures(1, &m_heightTex);
  doneCurrent();
}

void GLWidget::setPanelSize(float w, float h, float d) {
  m_panelW = w;
  m_panelH = h;
  m_panelD = d;
  m_panelGeometryDirty = true;
  m_heightDirty = true;
  update();
}

void GLWidget::setPanelThickness(float h) {
  if (qFuzzyCompare(m_panelH, h)) return;
  m_panelD = h;
  m_panelGeometryDirty = true;
  m_heightDirty = true;
  update();
}

void GLWidget::setSpec(int rows, int cols, float pitch, float dotRaius) {
  m_rows = qMax(0, rows);
  m_cols = qMax(0, cols);
  m_pitch = qMax(0.0f, pitch);
  m_dotRadius = qMax(0.0f, dotRaius);

  // 自动根据点阵尺寸调整 panel 的尺寸（保证圆点不会越过顶面边界）。
  // 约定：点中心覆盖的范围是 (cols-1)*pitch / (rows-1)*pitch，面板需要额外留出
  // dotRadius 的边缘空间。
  const float gridW = float(qMax(0, m_cols - 1)) * m_pitch;
  const float gridH = float(qMax(0, m_rows - 1)) * m_pitch;
  m_panelW = gridW + 2.0f * m_dotRadius;
  m_panelH = gridH + 2.0f * m_dotRadius;
  m_panelGeometryDirty = true;
  m_dotsGeometryDirty = true;

  QMutexLocker lk(&m_dataMutex);
  m_latestValues.resize(dotCount());
  for (int i = 0; i < m_latestValues.size(); ++i) {
    m_latestValues[i] = m_min;
  }
  m_valuesDirty = true;
  m_hasData = false;
  m_heightDirty = true;
  update();
}

void GLWidget::submitValues(const QVector<float>& values) {
  if (values.size() != dotCount()) return;

  {
    QMutexLocker lk(&m_dataMutex);
    m_latestValues = values;
    m_valuesDirty = true;
    m_hasData = true;
    m_heightDirty = true;
  }

  update();
}

void GLWidget::setRange(int minV, int maxV) {
  m_min = minV;
  m_max = maxV;
  m_heightDirty = true;
  update();
}

void GLWidget::setYaw(float yawDeg) {
  if (qFuzzyCompare(m_camYawDeg, yawDeg)) return;
  m_camYawDeg = yawDeg;
  emit yawChanged();
  update();
}

void GLWidget::setRenderModeString(const QString& mode) {
  RenderMode next = DataViz;
  if (mode.compare(QStringLiteral("realistic"), Qt::CaseInsensitive) == 0) {
    next = Realistic;
  } else if (mode.compare(QStringLiteral("dataViz"), Qt::CaseInsensitive) ==
             0) {
    next = DataViz;
  }

  if (m_renderMode == next) return;
  m_renderMode = next;
  update();
}

void GLWidget::setLabelModeString(const QString& mode) {
  LabelMode next = LabelsOff;
  if (mode.compare(QStringLiteral("hover"), Qt::CaseInsensitive) == 0) {
    next = LabelsHover;
  } else if (mode.compare(QStringLiteral("always"), Qt::CaseInsensitive) == 0) {
    next = LabelsAlways;
  } else if (mode.compare(QStringLiteral("off"), Qt::CaseInsensitive) == 0) {
    next = LabelsOff;
  }

  if (m_labelMode == next) return;
  m_labelMode = next;
  if (m_labelMode == LabelsOff) m_hoveredIndex = -1;
  update();
}

void GLWidget::setLightMode(bool on = true) {
  if (on == m_lightMode) {
    return;
  }

  m_lightMode = on;
  update();
}

void GLWidget::setShowBg(bool on = true) {
  if (on == m_showBg) {
    return;
  }
  m_showBg = on;
  update();
}

void GLWidget::setColorLow(const QColor& color) {
  const QVector3D next = toColorVec(color);
  if (m_colorLow == next) return;
  m_colorLow = next;
  update();
}

void GLWidget::setColorZero(const QColor& color) {
  const QVector3D next = toColorVec(color);
  if (m_colorZero == next) return;
  m_colorZero = next;
  update();
}

void GLWidget::setColorMid(const QColor& color) {
  const QVector3D next = toColorVec(color);
  if (m_colorMid == next) return;
  m_colorMid = next;
  update();
}

void GLWidget::setColorHigh(const QColor& color) {
  const QVector3D next = toColorVec(color);
  if (m_colorHigh == next) return;
  m_colorHigh = next;
  update();
}

void GLWidget::initializeGL() {
  initializeOpenGLFunctions();

  // 基础状态：开启深度测试，否则 3D 物体的遮挡关系会不正确
  glEnable(GL_DEPTH_TEST);
  glDepthFunc(GL_LESS);
  initPrograms_();
  initDotTexture_();
  initBackgroundGeometry_();
  initPanelGeometry_();
  initDotGeometry_();
  initRoomGeometry_();
  initBaseGeometry_();
  initHeatmapGeometry_();
  initSkirtGeometry_();
  updateHeatmapData_();
  initHeightTexture_();
  updateSkirtVertices_();
  m_panelGeometryDirty = false;
  m_dotsGeometryDirty = false;

  matIdentity(m_modelPanel);
  matIdentity(m_view);
  matIdentity(m_proj);
}

void GLWidget::initGeometry_() {
  initDotTexture_();
  initBackgroundGeometry_();
  initPanelGeometry_();
  initDotGeometry_();
  initRoomGeometry_();
  initBaseGeometry_();
  initHeatmapGeometry_();
  initSkirtGeometry_();
  updateHeatmapData_();
  initHeightTexture_();
  updateSkirtVertices_();
}

void GLWidget::resizeGL(int w, int h) { glViewport(0, 0, w, h); }

void GLWidget::paintGL() {
  QPainter painter(this);
  painter.setRenderHint(QPainter::Antialiasing, true);
  painter.setRenderHint(QPainter::TextAntialiasing, true);

  painter.beginNativePainting();

  glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

  // 如果点阵规格/面板尺寸发生变化，需要在有 GL 上下文时重建几何体 buffer。
  if (m_panelGeometryDirty) {
    initPanelGeometry_();
    initBaseGeometry_();
    initHeatmapGeometry_();
    initSkirtGeometry_();
    m_heightDirty = true;
    m_panelGeometryDirty = false;
  }
  if (m_dotsGeometryDirty) {
    initDotGeometry_();
    m_dotsGeometryDirty = false;
    m_valuesDirty = true;  // instanceVBO 重新分配后需要重新上传数据
  }

  // --- 背景：屏幕空间网格（不随相机旋转）---
  if (m_bgProg && m_bgVao && m_showBg) {
    const float dpr = devicePixelRatioF();
    const QVector2D viewport(float(width()) * dpr, float(height()) * dpr);

    glDisable(GL_DEPTH_TEST);
    glDepthMask(GL_FALSE);

    m_bgProg->bind();
    m_bgProg->setUniformValue("uViewport", viewport);
    m_bgProg->setUniformValue("uMinorStep", 24.0f * dpr);
    m_bgProg->setUniformValue("uMajorStep", 120.0f * dpr);

    glBindVertexArray(m_bgVao);
    glDrawArrays(GL_TRIANGLES, 0, 6);
    glBindVertexArray(0);

    m_bgProg->release();

    glDepthMask(GL_TRUE);
    glEnable(GL_DEPTH_TEST);
  }

  // 1) 更新相机/投影矩阵（MVP），决定如何把 3D 世界投影到屏幕
  updateMatrices_();
  updateRoom_();
  if (m_useHeatmap) {
    updateHeatmapData_();
    uploadHeightTexture_();
    updateSkirtVertices_();
    renderBase_();
    renderSkirt_();
    renderHeatmap_();
  } else {
    // 2) 如果外部提交了新数据，就把 CPU 生成的 instance 数据更新到 GPU
    updateInstanceBufferIfNeeded_();
  }

  if (!m_useHeatmap && m_panelProg) {
    m_panelProg->bind();
    // uniforms：每次 draw 前设置的一组“常量参数”（对当前 draw call
    // 的所有顶点/片元都一致） uMVP: Model-View-Projection
    // 矩阵（把顶点从世界坐标 -> 裁剪空间；gl_Position 必须输出裁剪空间坐标）
    m_panelProg->setUniformValue("uMVP", m_mvp);
    m_panelProg->setUniformValue("uCameraPos", m_cameraPos);
    m_panelProg->setUniformValue("uPanelW", m_panelW);
    m_panelProg->setUniformValue("uPanelH", m_panelH);
    m_panelProg->setUniformValue("uPanelD", m_panelD);
    m_panelProg->setUniformValue("uRows", m_rows);
    m_panelProg->setUniformValue("uCols", m_cols);
    m_panelProg->setUniformValue("uPitch", m_pitch);
    m_panelProg->setUniformValue("uDotRadius", m_dotRadius);
    m_panelProg->setUniformValue("uRenderMode", 1);
    m_panelProg->setUniformValue("uLightMode", m_lightMode);
    glBindVertexArray(m_panelVao);
    glDrawElements(GL_TRIANGLES, m_panelIndexCount, GL_UNSIGNED_INT, nullptr);
    glBindVertexArray(0);
    m_panelProg->release();
  }

  if (!m_useHeatmap && m_dotsProg) {
    m_dotsProg->bind();
    // uniforms：每次 draw 前设置的一组“常量参数”（对当前 draw call 的所有
    // instance 都一致） uMVP: 同上；用于把每个 dot 的世界坐标变换到屏幕
    m_dotsProg->setUniformValue("uMVP", m_mvp);
    m_dotsProg->setUniformValue("uRenderMode", 1);
    // uDotRadius: dot 的半径（世界坐标单位）
    m_dotsProg->setUniformValue("uDotRadius", m_dotRadius);
    // uBaseY: dot 的高度（放在 panel 顶面上方一点点，避免 z-fighting/闪烁）
    m_dotsProg->setUniformValue("uBaseZ", -(m_panelD * 0.5f) - 0.001f);
    // uMinV/uMaxV: 传感值范围，用于 fragment shader 把 value 映射成颜色
    m_dotsProg->setUniformValue("uMinV", float(m_min));
    m_dotsProg->setUniformValue("uMaxV", float(m_max));
    m_dotsProg->setUniformValue("uColorZero", m_colorZero);
    m_dotsProg->setUniformValue("uColorLow", m_colorLow);
    m_dotsProg->setUniformValue("uColorMid", m_colorMid);
    m_dotsProg->setUniformValue("uColorHigh", m_colorHigh);
    const int hasData = (m_hasData.load() || m_dotTex == 0) ? 1 : 0;
    m_dotsProg->setUniformValue("uHasData", hasData);
    m_dotsProg->setUniformValue("uCameraPos", m_cameraPos);
    m_dotsProg->setUniformValue("uDotTex", 0);
    if (m_dotTex) {
      glActiveTexture(GL_TEXTURE0);
      glBindTexture(GL_TEXTURE_2D, m_dotTex);
    }

    glBindVertexArray(m_dotsVao);
    glDrawArraysInstanced(GL_TRIANGLES, 0, 6, m_instanceCount);
    glBindVertexArray(0);
    if (m_dotTex) {
      glActiveTexture(GL_TEXTURE0);
      glBindTexture(GL_TEXTURE_2D, 0);
    }

    // m_dotsProg->release();
  }

  painter.endNativePainting();

  if (!m_useHeatmap && m_labelMode != LabelsOff && dotCount() > 0) {
    QVector<float> valuesCopy;
    {
      QMutexLocker lk(&m_dataMutex);
      valuesCopy = m_latestValues;
    }

    auto projectToScreen = [&](const QVector3D& world, QPointF& out) -> bool {
      const QVector4D clip = m_mvp * QVector4D(world, 1.0f);
      if (clip.w() <= 1e-6f) return false;
      const QVector3D ndc = clip.toVector3D() / clip.w();
      if (ndc.z() < -1.2f || ndc.z() > 1.2f) return false;

      out.setX((ndc.x() * 0.5f + 0.5f) * float(width()));
      out.setY((1.0f - (ndc.y() * 0.5f + 0.5f)) * float(height()));
      return true;
    };

    const float baseY = (m_panelH * 0.5f) + 0.001f;
    const float w = (m_cols - 1) * m_pitch;
    const float h = (m_rows - 1) * m_pitch;

    QFont font = painter.font();
    font.setPixelSize(11);
    painter.setFont(font);
    const QFontMetrics fm(font);

    auto drawLabel = [&](const QPointF& anchor, const QPointF& offset,
                         const QString& text) {
      QRectF box = fm.boundingRect(text);
      box.adjust(-6.0, -3.0, 6.0, 3.0);
      box.moveCenter(anchor + offset);

      const float margin = 4.0f;
      if (box.left() < margin) box.moveLeft(margin);
      if (box.top() < margin) box.moveTop(margin);
      if (box.right() > width() - margin) box.moveRight(width() - margin);
      if (box.bottom() > height() - margin) box.moveBottom(height() - margin);

      painter.setPen(Qt::NoPen);
      painter.setBrush(QColor(0, 0, 0, 150));
      painter.drawRoundedRect(box, 4.0, 4.0);
      painter.setPen(QColor(255, 255, 255, 235));
      painter.drawText(box, Qt::AlignCenter, text);
    };

    auto dotWorldCenter = [&](int i) -> QVector3D {
      const int rr = (m_cols > 0) ? (i / m_cols) : 0;
      const int cc = (m_cols > 0) ? (i % m_cols) : 0;
      const float x = (cc * m_pitch) - w * 0.5f;
      const float z = (rr * m_pitch) - h * 0.5f;
      return QVector3D(x, baseY, z);
    };

    auto dotRadiusPx = [&](const QVector3D& worldCenter,
                           const QPointF& centerPx) -> float {
      QPointF edge;
      if (!projectToScreen(worldCenter + QVector3D(m_dotRadius, 0.0f, 0.0f),
                           edge))
        return 0.0f;
      const float dx = float(edge.x() - centerPx.x());
      const float dy = float(edge.y() - centerPx.y());
      return std::sqrt(dx * dx + dy * dy);
    };

    if (m_labelMode == LabelsAlways && valuesCopy.size() == dotCount()) {
      painter.setPen(QColor(255, 255, 255, 220));
      for (int i = 0; i < dotCount(); ++i) {
        QPointF centerPx;
        const QVector3D world = dotWorldCenter(i);
        if (!projectToScreen(world, centerPx)) continue;

        const float rPx = dotRadiusPx(world, centerPx);
        if (rPx < 12.0f) continue;

        const QString text = QString::number(int(valuesCopy[i] + 0.5f));
        drawLabel(centerPx, QPointF(0.0, 0.0), text);
      }
    }

    if (m_hoveredIndex >= 0 && m_hoveredIndex < dotCount() &&
        valuesCopy.size() == dotCount()) {
      QPointF centerPx;
      const QVector3D world = dotWorldCenter(m_hoveredIndex);
      if (projectToScreen(world, centerPx)) {
        const float rPx = dotRadiusPx(world, centerPx);

        painter.setBrush(Qt::NoBrush);
        painter.setPen(QPen(QColor(255, 255, 255, 210), 1.5));
        painter.drawEllipse(centerPx, rPx * 1.08f, rPx * 1.08f);

        if (m_labelMode == LabelsHover) {
          const QString text =
              QString::number(int(valuesCopy[m_hoveredIndex] + 0.5f));
          drawLabel(centerPx, QPointF(0.0, -rPx - 12.0f), text);
        }
      }
    }
  }

}

void GLWidget::mousePressEvent(QMouseEvent* event) {
  m_lastPos = event->pos();
  if (event->button() == Qt::RightButton) {
    m_rightDown = true;
    event->accept();
    return;
  }
  if (event->button() == Qt::LeftButton) {
    if (m_useHeatmap) {
      event->accept();
      return;
    }
    QVector3D world;
    const int index = pickDotIndex_(event->pos(), &world);
    qInfo() << "clicked index: " << index;
    if (index >= 0) {
      float value = 0.0f;
      int row = 0;
      int col = 0;
      if (m_cols > 0) {
        row = index / m_cols;
        col = index % m_cols;
      }
      {
        QMutexLocker lk(&m_dataMutex);
        if (index < m_latestValues.size()) value = m_latestValues[index];
      }
      emit dotClicked(index, row, col, value);
    }
  }
  QOpenGLWidget::mousePressEvent(event);
}

void GLWidget::mouseMoveEvent(QMouseEvent* event) {
  const QPoint delta = event->pos() - m_lastPos;
  m_lastPos = event->pos();

  if (m_rightDown) {
    m_camYawDeg += delta.x() * 0.3f;
    m_camPitchDeg = qBound(-89.0f, m_camPitchDeg + delta.y() * 0.3f, 89.0f);
    emit yawChanged();
    update();
    event->accept();
    return;
  }

  if (!m_useHeatmap && m_labelMode != LabelsOff && dotCount() > 0) {
    auto projectToScreen = [&](const QVector3D& world, QPointF& out) -> bool {
      const QVector4D clip = m_mvp * QVector4D(world, 1.0f);
      if (clip.w() <= 1e-6f) return false;
      const QVector3D ndc = clip.toVector3D() / clip.w();
      if (ndc.z() < -1.2f || ndc.z() > 1.2f) return false;

      out.setX((ndc.x() * 0.5f + 0.5f) * float(width()));
      out.setY((1.0f - (ndc.y() * 0.5f + 0.5f)) * float(height()));
      return true;
    };

    const QPoint mousePos = event->pos();
    const float baseY = (m_panelH * 0.5f) + 0.001f;
    const float w = (m_cols - 1) * m_pitch;
    const float h = (m_rows - 1) * m_pitch;

    int best = -1;
    float bestDist2 = std::numeric_limits<float>::infinity();
    for (int i = 0; i < dotCount(); ++i) {
      const int rr = (m_cols > 0) ? (i / m_cols) : 0;
      const int cc = (m_cols > 0) ? (i % m_cols) : 0;
      const float x = (cc * m_pitch) - w * 0.5f;
      const float z = (rr * m_pitch) - h * 0.5f;

      QPointF center;
      const QVector3D worldCenter(x, baseY, z);
      if (!projectToScreen(worldCenter, center)) continue;

      QPointF edge;
      if (!projectToScreen(worldCenter + QVector3D(m_dotRadius, 0.0f, 0.0f),
                           edge))
        continue;

      const float radDx = float(edge.x() - center.x());
      const float radDy = float(edge.y() - center.y());
      const float radPx = std::sqrt(radDx * radDx + radDy * radDy);
      const float threshold = radPx + 6.0f;

      const float dx = float(mousePos.x()) - float(center.x());
      const float dy = float(mousePos.y()) - float(center.y());
      const float dist2 = dx * dx + dy * dy;
      if (dist2 <= threshold * threshold && dist2 < bestDist2) {
        best = i;
        bestDist2 = dist2;
      }
    }

    if (best != m_hoveredIndex) {
      m_hoveredIndex = best;
      update();
    }
  }

  QOpenGLWidget::mouseMoveEvent(event);
}

void GLWidget::mouseReleaseEvent(QMouseEvent* event) {
  if (event->button() == Qt::RightButton) {
    m_rightDown = false;
    event->accept();
    return;
  }
  QOpenGLWidget::mouseReleaseEvent(event);
}

void GLWidget::wheelEvent(QWheelEvent* event) {
  const float steps = event->angleDelta().y() / 120.0f;
  const float factor = std::pow(0.9f, steps);
  m_zoom_ = qBound(0.2f, m_zoom_ * factor, 90.0f);
  update();
  event->accept();
}

bool GLWidget::projectToScreen_(const QVector3D& world, QPointF* out) const {
  if (!out) return false;
  const QVector4D clip = m_mvp * QVector4D(world, 1.0f);
  if (clip.w() <= 1e-6f) return false;
  const QVector3D ndc = clip.toVector3D() / clip.w();
  if (ndc.z() < -1.2f || ndc.z() > 1.2f) return false;

  out->setX((ndc.x() * 0.5f + 0.5f) * float(width()));
  out->setY((1.0f - (ndc.y() * 0.5f + 0.5f)) * float(height()));
  return true;
}

int GLWidget::pickDotIndex_(const QPoint& pos, QVector3D* worldOut) const {
  if (dotCount() <= 0) return -1;

  const float baseY = (m_panelH * 0.5f) + 0.001f;
  const float w = (m_cols - 1) * m_pitch;
  const float h = (m_rows - 1) * m_pitch;

  int best = -1;
  float bestDist2 = std::numeric_limits<float>::infinity();
  QVector3D bestWorld;

  for (int i = 0; i < dotCount(); ++i) {
    const int rr = (m_cols > 0) ? (i / m_cols) : 0;
    const int cc = (m_cols > 0) ? (i % m_cols) : 0;
    const QVector3D worldCenter((cc * m_pitch) - w * 0.5f, baseY,
                                (rr * m_pitch) - h * 0.5f);

    QPointF center;
    if (!projectToScreen_(worldCenter, &center)) continue;

    QPointF edge;
    if (!projectToScreen_(worldCenter + QVector3D(m_dotRadius, 0.0f, 0.0f),
                          &edge))
      continue;

    const float radDx = float(edge.x() - center.x());
    const float radDy = float(edge.y() - center.y());
    const float radPx = std::sqrt(radDx * radDx + radDy * radDy);
    const float threshold = radPx + 6.0f;

    const float dx = float(pos.x()) - float(center.x());
    const float dy = float(pos.y()) - float(center.y());
    const float dist2 = dx * dx + dy * dy;
    if (dist2 <= threshold * threshold && dist2 < bestDist2) {
      best = i;
      bestDist2 = dist2;
      bestWorld = worldCenter;
    }
  }

  if (best >= 0 && worldOut) *worldOut = bestWorld;
  return best;
}

void GLWidget::initBackgroundGeometry_() {
  if (m_bgVbo) {
    glDeleteBuffers(1, &m_bgVbo);
    m_bgVbo = 0;
  }
  if (m_bgVao) {
    glDeleteVertexArrays(1, &m_bgVao);
    m_bgVao = 0;
  }

  // 全屏 quad（两个三角形），坐标直接是裁剪空间 NDC [-1,1]
  const float verts[] = {
      -1.0f, -1.0f, 1.0f, -1.0f, 1.0f,  1.0f,
      -1.0f, -1.0f, 1.0f, 1.0f,  -1.0f, 1.0f,
  };

  glGenVertexArrays(1, &m_bgVao);
  glBindVertexArray(m_bgVao);

  glGenBuffers(1, &m_bgVbo);
  glBindBuffer(GL_ARRAY_BUFFER, m_bgVbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);

  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0);

  glBindVertexArray(0);
}

void GLWidget::initPanelGeometry_() {
  // panel：目前只画“顶面矩形”，y 固定在 +panelH/2
  if (m_panelIbo) {
    glDeleteBuffers(1, &m_panelIbo);
    m_panelIbo = 0;
  }
  if (m_panelVbo) {
    glDeleteBuffers(1, &m_panelVbo);
    m_panelVbo = 0;
  }
  if (m_panelVao) {
    glDeleteVertexArrays(1, &m_panelVao);
    m_panelVao = 0;
  }

  const float y = m_panelH * 0.5f;
  const float hw = m_panelW * 0.5;
  const float hd = m_panelD * 0.5f;

  struct V {
    float x, y, z;
    float nx, ny, nz;
  };
  V verts[24] = {
      // +Y 顶面 (normal 0, +1, 0)
      {-hw, +y, -hd, 0, +1, 0},  // 0
      {+hw, +y, -hd, 0, +1, 0},  // 1
      {+hw, +y, +hd, 0, +1, 0},  // 2
      {-hw, +y, +hd, 0, +1, 0},  // 3

      // +Z 前面 (normal 0, 0, +1)
      {-hw, +y, +hd, 0, 0, +1},  // 4
      {+hw, +y, +hd, 0, 0, +1},  // 5
      {+hw, -y, +hd, 0, 0, +1},  // 6
      {-hw, -y, +hd, 0, 0, +1},  // 7

      // -Y 底面 (normal 0, -1, 0)
      {-hw, -y, +hd, 0, -1, 0},  // 8
      {+hw, -y, +hd, 0, -1, 0},  // 9
      {+hw, -y, -hd, 0, -1, 0},  // 10
      {-hw, -y, -hd, 0, -1, 0},  // 11

      // -Z 后面 (normal 0, 0, -1)
      {+hw, +y, -hd, 0, 0, -1},  // 12
      {-hw, +y, -hd, 0, 0, -1},  // 13
      {-hw, -y, -hd, 0, 0, -1},  // 14
      {+hw, -y, -hd, 0, 0, -1},  // 15

      // -X 左面 (normal -1, 0, 0)
      {-hw, +y, -hd, -1, 0, 0},  // 16
      {-hw, +y, +hd, -1, 0, 0},  // 17
      {-hw, -y, +hd, -1, 0, 0},  // 18
      {-hw, -y, -hd, -1, 0, 0},  // 19

      // +X 右面 (normal +1, 0, 0)
      {+hw, +y, +hd, +1, 0, 0},  // 20
      {+hw, +y, -hd, +1, 0, 0},  // 21
      {+hw, -y, -hd, +1, 0, 0},  // 22
      {+hw, -y, +hd, +1, 0, 0},  // 23
  };

  unsigned int idx[36] = {
      0,  1,  2,  0,  2,  3,   // top
      4,  5,  6,  4,  6,  7,   // front
      8,  9,  10, 8,  10, 11,  // bottom
      12, 13, 14, 12, 14, 15,  // back
      16, 17, 18, 16, 18, 19,  // left
      20, 21, 22, 20, 22, 23   // right
  };

  m_panelIndexCount = 36;

  glGenVertexArrays(1, &m_panelVao);
  glBindVertexArray(m_panelVao);

  glGenBuffers(1, &m_panelVbo);
  glBindBuffer(GL_ARRAY_BUFFER, m_panelVbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);

  glGenBuffers(1, &m_panelIbo);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_panelIbo);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(idx), idx, GL_STATIC_DRAW);

  // attribute layout:
  // location 0: position (x,y,z)
  // location 1: normal   (nx,ny,nz) —— 当前 shader 没用到，先留着
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(V), (void*)0);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(V),
                        (void*)(3 * sizeof(float)));
  glBindVertexArray(0);
}

void GLWidget::initDotGeometry_() {
  // dot：使用 instanced rendering 画很多个“圆点”（本质是一个 quad + fragment
  // shader 里 discard 成圆） vertex buffer 里只有一个单位 quad，instance buffer
  // 里存每个点的位置和数值。
  if (m_instanceVbo) {
    glDeleteBuffers(1, &m_instanceVbo);
    m_instanceVbo = 0;
  }
  if (m_dotsVbo) {
    glDeleteBuffers(1, &m_dotsVbo);
    m_dotsVbo = 0;
  }
  if (m_dotsVao) {
    glDeleteVertexArrays(1, &m_dotsVao);
    m_dotsVao = 0;
  }

  struct V {
    float x, y;
    float u, v;
  };
  V quad[6] = {
      {-1, -1, 0, 0}, {1, -1, 1, 0}, {1, 1, 1, 1},
      {-1, -1, 0, 0}, {1, 1, 1, 1},  {-1, 1, 0, 1},
  };

  glGenVertexArrays(1, &m_dotsVao);
  glBindVertexArray(m_dotsVao);
  glGenBuffers(1, &m_dotsVbo);
  glBindBuffer(GL_ARRAY_BUFFER, m_dotsVbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(quad), quad, GL_STATIC_DRAW);

  // attribute layout:
  // location 0: quad 顶点位置 (x,y)，范围 [-1,1]
  // location 1: UV (u,v)，用于 fragment shader 生成圆形
  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, sizeof(V), (void*)0);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, sizeof(V),
                        (void*)(2 * sizeof(float)));

  // instance buffer：每个点 3 个 float（offsetX, offsetZ, value）
  // layout location 2/3 对应 shader 里的 iOffsetXZ / iValue
  glGenBuffers(1, &m_instanceVbo);
  glBindBuffer(GL_ARRAY_BUFFER, m_instanceVbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 3 * qMax(1, dotCount()),
               nullptr, GL_DYNAMIC_DRAW);

  // location 2: vec2 iOffsetXZ（每个 instance 一次，divisor=1）
  glEnableVertexAttribArray(2);
  glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
  glVertexAttribDivisor(2, 1);

  // location 3: float iValue（每个 instance 一次，divisor=1）
  glEnableVertexAttribArray(3);
  glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, 3 * sizeof(float),
                        (void*)(2 * sizeof(float)));
  glVertexAttribDivisor(3, 1);

  glBindVertexArray(0);
}
void GLWidget::initDotTexture_() {
  if (m_dotTex) {
    glDeleteTextures(1, &m_dotTex);
    m_dotTex = 0;
  }

  const QString path = QStringLiteral(":/images/metal.jpeg");
  QImage img(path);
  if (img.isNull()) {
    qWarning() << "dot texture load failed:" << path;
    return;
  }

  QImage rgba = img.convertToFormat(QImage::Format_RGBA8888);
  glGenTextures(1, &m_dotTex);
  glBindTexture(GL_TEXTURE_2D, m_dotTex);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
                  GL_LINEAR_MIPMAP_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, rgba.width(), rgba.height(), 0,
               GL_RGBA, GL_UNSIGNED_BYTE, rgba.constBits());
  glGenerateMipmap(GL_TEXTURE_2D);
  glBindTexture(GL_TEXTURE_2D, 0);
}

void GLWidget::initRoomGeometry_() {
  qInfo() << "initRoomGeometry_()";
  if (room_vao) {
    glDeleteVertexArrays(1, &room_vao);
    room_vao = 0;
  }
  if (room_vbo) {
    glDeleteBuffers(1, &room_vbo);
    room_vbo = 0;
  }
  if (room_ibo) {
    glDeleteBuffers(1, &room_ibo);
    room_ibo = 0;
  }

  using V = struct {
    float x, y, z;
    float nx, ny, nz;
  };

  V verts[24] = {
      // floor (y = -1), normal +Y
      {-1, -1, -1, 0, +1, 0},
      {+1, -1, -1, 0, +1, 0},
      {+1, -1, +1, 0, +1, 0},
      {-1, -1, +1, 0, +1, 0},

      // ceiling (y = +1), normal -Y
      {-1, +1, +1, 0, -1, 0},
      {+1, +1, +1, 0, -1, 0},
      {+1, +1, -1, 0, -1, 0},
      {-1, +1, -1, 0, -1, 0},

      // back wall (z = -1), normal +Z
      {-1, +1, -1, 0, 0, +1},
      {+1, +1, -1, 0, 0, +1},
      {+1, -1, -1, 0, 0, +1},
      {-1, -1, -1, 0, 0, +1},

      // front wall (z = +1), normal -Z
      {+1, +1, +1, 0, 0, -1},
      {-1, +1, +1, 0, 0, -1},
      {-1, -1, +1, 0, 0, -1},
      {+1, -1, +1, 0, 0, -1},

      // left wall (x = -1), normal +X
      {-1, +1, +1, +1, 0, 0},
      {-1, +1, -1, +1, 0, 0},
      {-1, -1, -1, +1, 0, 0},
      {-1, -1, +1, +1, 0, 0},

      // right wall (x = +1), normal -X
      {+1, +1, -1, -1, 0, 0},
      {+1, +1, +1, -1, 0, 0},
      {+1, -1, +1, -1, 0, 0},
      {+1, -1, -1, -1, 0, 0},
  };
  unsigned int idx[36] = {0,  1,  2,  0,  2,  3,  4,  5,  6,  4,  6,  7,
                          8,  9,  10, 8,  10, 11, 12, 13, 14, 12, 14, 15,
                          16, 17, 18, 16, 18, 19, 20, 21, 22, 20, 22, 23};
  room_index_count = 36;

  glGenVertexArrays(1, &room_vao);
  glBindVertexArray(room_vao);

  glGenBuffers(1, &room_vbo);
  glBindBuffer(GL_ARRAY_BUFFER, room_vbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);

  glGenBuffers(1, &room_ibo);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, room_ibo);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(idx), idx, GL_STATIC_DRAW);

  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(V), (void*)0);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(V),
                        (void*)(3 * sizeof(float)));

  glBindVertexArray(0);
}

void GLWidget::initBaseGeometry_() {
  if (m_baseIbo) {
    glDeleteBuffers(1, &m_baseIbo);
    m_baseIbo = 0;
  }
  if (m_baseVbo) {
    glDeleteBuffers(1, &m_baseVbo);
    m_baseVbo = 0;
  }
  if (m_baseVao) {
    glDeleteVertexArrays(1, &m_baseVao);
    m_baseVao = 0;
  }

  const float hw = m_panelW * 0.5f;
  const float hh = m_panelH * 0.5f;
  const float z = m_panelD * 0.5f;

  struct V {
    float x, y, z;
    float nx, ny, nz;
    float u, v;
  };
  V verts[4] = {
      {-hw, -hh, z, 0, 0, -1, 0, 0},
      {+hw, -hh, z, 0, 0, -1, 1, 0},
      {+hw, +hh, z, 0, 0, -1, 1, 1},
      {-hw, +hh, z, 0, 0, -1, 0, 1},
  };
  unsigned int idx[6] = {0, 1, 2, 0, 2, 3};
  m_baseIndexCount = 6;

  glGenVertexArrays(1, &m_baseVao);
  glBindVertexArray(m_baseVao);

  glGenBuffers(1, &m_baseVbo);
  glBindBuffer(GL_ARRAY_BUFFER, m_baseVbo);
  glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW);

  glGenBuffers(1, &m_baseIbo);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_baseIbo);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(idx), idx, GL_STATIC_DRAW);

  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(V), (void*)0);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, sizeof(V),
                        (void*)(3 * sizeof(float)));
  glEnableVertexAttribArray(2);
  glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(V),
                        (void*)(6 * sizeof(float)));

  glBindVertexArray(0);
}

void GLWidget::initHeatmapGeometry_() {
  if (m_heatmapIbo) {
    glDeleteBuffers(1, &m_heatmapIbo);
    m_heatmapIbo = 0;
  }
  if (m_heatmapVbo) {
    glDeleteBuffers(1, &m_heatmapVbo);
    m_heatmapVbo = 0;
  }
  if (m_heatmapVao) {
    glDeleteVertexArrays(1, &m_heatmapVao);
    m_heatmapVao = 0;
  }

  const float aspect = (m_panelW > 1e-6f) ? (m_panelH / m_panelW) : 1.0f;
  const int max_res = 512;
  const int cols = qMax(2, qMin(m_cols * m_upscale, max_res));
  int rows = qMax(2, int(cols * aspect + 0.5f));
  rows = qMax(2, qMin(rows, max_res));
  m_heatmapCols = cols;
  m_heatmapRows = rows;

  const int vertex_count = cols * rows;
  QVector<float> verts;
  verts.reserve(vertex_count * 5);

  for (int r = 0; r < rows; ++r) {
    const float v = (rows > 1) ? float(r) / float(rows - 1) : 0.0f;
    const float y = (v - 0.5f) * m_panelH;
    for (int c = 0; c < cols; ++c) {
      const float u = (cols > 1) ? float(c) / float(cols - 1) : 0.0f;
      const float x = (u - 0.5f) * m_panelW;
      verts.push_back(x);
      verts.push_back(y);
      verts.push_back(0.0f);
      verts.push_back(u);
      verts.push_back(v);
    }
  }

  QVector<unsigned int> idx;
  idx.reserve((cols - 1) * (rows - 1) * 6);
  for (int r = 0; r < rows - 1; ++r) {
    for (int c = 0; c < cols - 1; ++c) {
      const unsigned int i0 = unsigned(r * cols + c);
      const unsigned int i1 = unsigned(r * cols + c + 1);
      const unsigned int i2 = unsigned((r + 1) * cols + c + 1);
      const unsigned int i3 = unsigned((r + 1) * cols + c);
      idx.push_back(i0);
      idx.push_back(i1);
      idx.push_back(i2);
      idx.push_back(i0);
      idx.push_back(i2);
      idx.push_back(i3);
    }
  }
  m_heatmapIndexCount = int(idx.size());

  glGenVertexArrays(1, &m_heatmapVao);
  glBindVertexArray(m_heatmapVao);

  glGenBuffers(1, &m_heatmapVbo);
  glBindBuffer(GL_ARRAY_BUFFER, m_heatmapVbo);
  glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(float), verts.constData(),
               GL_STATIC_DRAW);

  glGenBuffers(1, &m_heatmapIbo);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_heatmapIbo);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx.size() * sizeof(unsigned int),
               idx.constData(), GL_STATIC_DRAW);

  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float),
                        (void*)(3 * sizeof(float)));

  glBindVertexArray(0);
}

void GLWidget::initSkirtGeometry_() {
  if (m_skirtIbo) {
    glDeleteBuffers(1, &m_skirtIbo);
    m_skirtIbo = 0;
  }
  if (m_skirtVbo) {
    glDeleteBuffers(1, &m_skirtVbo);
    m_skirtVbo = 0;
  }
  if (m_skirtVao) {
    glDeleteVertexArrays(1, &m_skirtVao);
    m_skirtVao = 0;
  }

  m_skirtUVs.clear();
  m_skirtNormals.clear();

  const int cols = qMax(2, m_heatmapCols);
  const int rows = qMax(2, m_heatmapRows);

  auto push_border = [&](int c, int r, const QVector3D& n) {
    const float u = (cols > 1) ? float(c) / float(cols - 1) : 0.0f;
    const float v = (rows > 1) ? float(r) / float(rows - 1) : 0.0f;
    m_skirtUVs.push_back(QVector2D(u, v));
    m_skirtNormals.push_back(n);
  };

  for (int c = 0; c < cols; ++c) {
    push_border(c, 0, QVector3D(0.0f, -1.0f, 0.0f));
  }
  for (int r = 1; r < rows - 1; ++r) {
    push_border(cols - 1, r, QVector3D(1.0f, 0.0f, 0.0f));
  }
  for (int c = cols - 1; c >= 0; --c) {
    push_border(c, rows - 1, QVector3D(0.0f, 1.0f, 0.0f));
  }
  for (int r = rows - 2; r >= 1; --r) {
    push_border(0, r, QVector3D(-1.0f, 0.0f, 0.0f));
  }

  const int border_count = m_skirtUVs.size();
  m_skirtVertices.fill(0.0f, border_count * 2 * 8);

  QVector<unsigned int> idx;
  idx.reserve(border_count * 6);
  for (int i = 0; i < border_count; ++i) {
    const int next = (i + 1) % border_count;
    const unsigned int top0 = unsigned(i * 2);
    const unsigned int bot0 = unsigned(i * 2 + 1);
    const unsigned int top1 = unsigned(next * 2);
    const unsigned int bot1 = unsigned(next * 2 + 1);

    idx.push_back(top0);
    idx.push_back(top1);
    idx.push_back(bot1);
    idx.push_back(top0);
    idx.push_back(bot1);
    idx.push_back(bot0);
  }
  m_skirtIndexCount = int(idx.size());

  glGenVertexArrays(1, &m_skirtVao);
  glBindVertexArray(m_skirtVao);

  glGenBuffers(1, &m_skirtVbo);
  glBindBuffer(GL_ARRAY_BUFFER, m_skirtVbo);
  glBufferData(GL_ARRAY_BUFFER, m_skirtVertices.size() * sizeof(float),
               m_skirtVertices.constData(), GL_DYNAMIC_DRAW);

  glGenBuffers(1, &m_skirtIbo);
  glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_skirtIbo);
  glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx.size() * sizeof(unsigned int),
               idx.constData(), GL_STATIC_DRAW);

  glEnableVertexAttribArray(0);
  glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void*)0);
  glEnableVertexAttribArray(1);
  glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float),
                        (void*)(3 * sizeof(float)));
  glEnableVertexAttribArray(2);
  glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float),
                        (void*)(6 * sizeof(float)));

  glBindVertexArray(0);
}

void GLWidget::initHeightTexture_() {
  if (m_heightW <= 0 || m_heightH <= 0 || m_heightValues.isEmpty()) {
    return;
  }
  if (m_heightTex) {
    glDeleteTextures(1, &m_heightTex);
    m_heightTex = 0;
  }
  glGenTextures(1, &m_heightTex);
  glBindTexture(GL_TEXTURE_2D, m_heightTex);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
  glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_R32F, m_heightW, m_heightH, 0, GL_RED,
               GL_FLOAT, m_heightValues.constData());
  m_heightTexW = m_heightW;
  m_heightTexH = m_heightH;
  glBindTexture(GL_TEXTURE_2D, 0);
}

float GLWidget::sampleHeightValue_(float u, float v) const {
  if (m_heightW <= 0 || m_heightH <= 0 || m_heightValues.isEmpty()) {
    return float(m_min);
  }
  const float x = qBound(0.0f, u, 1.0f) * float(m_heightW - 1);
  const float y = qBound(0.0f, v, 1.0f) * float(m_heightH - 1);
  const int x0 = qBound(0, int(std::floor(x)), m_heightW - 1);
  const int y0 = qBound(0, int(std::floor(y)), m_heightH - 1);
  const int x1 = qMin(x0 + 1, m_heightW - 1);
  const int y1 = qMin(y0 + 1, m_heightH - 1);
  const float fx = x - float(x0);
  const float fy = y - float(y0);

  const float v00 = m_heightValues[y0 * m_heightW + x0];
  const float v10 = m_heightValues[y0 * m_heightW + x1];
  const float v01 = m_heightValues[y1 * m_heightW + x0];
  const float v11 = m_heightValues[y1 * m_heightW + x1];

  const float vx0 = v00 + (v10 - v00) * fx;
  const float vx1 = v01 + (v11 - v01) * fx;
  return vx0 + (vx1 - vx0) * fy;
}

void GLWidget::updateHeatmapData_() {
  if (!m_heightDirty && !m_heightValues.isEmpty()) {
    return;
  }
  if (m_rows <= 0 || m_cols <= 0) {
    return;
  }

  QVector<float> valuesCopy;
  {
    QMutexLocker lk(&m_dataMutex);
    valuesCopy = m_latestValues;
  }

  cv::Mat raw(m_rows, m_cols, CV_32F);
  for (int r = 0; r < m_rows; ++r) {
    for (int c = 0; c < m_cols; ++c) {
      const int index = r * m_cols + c;
      const float v =
          (index < valuesCopy.size()) ? valuesCopy[index] : float(m_min);
      raw.at<float>(r, c) = v;
    }
  }

  const int outW = qMax(1, m_cols * m_upscale);
  const int outH = qMax(1, m_rows * m_upscale);
  cv::Mat out;
  GlobalHelper::transToMultiMatrix(raw, float(m_min), float(m_max),
                                   cv::Size(outW, outH), out);
  if (out.empty()) {
    return;
  }
  if (out.type() != CV_32F) {
    out.convertTo(out, CV_32F);
  }

  const int nextW = out.cols;
  const int nextH = out.rows;
  if (nextW <= 0 || nextH <= 0) {
    return;
  }

  m_heightW = nextW;
  m_heightH = nextH;
  m_heightValues.resize(m_heightW * m_heightH);

  if (out.isContinuous()) {
    const size_t count = size_t(m_heightW) * size_t(m_heightH);
    std::memcpy(m_heightValues.data(), out.ptr<float>(0),
                count * sizeof(float));
  } else {
    for (int r = 0; r < m_heightH; ++r) {
      const float* rowPtr = out.ptr<float>(r);
      std::memcpy(m_heightValues.data() + r * m_heightW, rowPtr,
                  m_heightW * sizeof(float));
    }
  }
  m_heightDirty = false;
}

void GLWidget::uploadHeightTexture_() {
  if (m_heightValues.isEmpty() || m_heightW <= 0 || m_heightH <= 0) {
    return;
  }
  if (!m_heightTex || m_heightTexW != m_heightW || m_heightTexH != m_heightH) {
    initHeightTexture_();
  }
  if (!m_heightTex) {
    return;
  }

  glBindTexture(GL_TEXTURE_2D, m_heightTex);
  glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
  glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, m_heightW, m_heightH, GL_RED,
                  GL_FLOAT, m_heightValues.constData());
  glBindTexture(GL_TEXTURE_2D, 0);
}

void GLWidget::updateSkirtVertices_() {
  if (m_skirtVertices.isEmpty() || m_skirtUVs.isEmpty()) {
    return;
  }
  const float baseZ = -m_panelD * 0.5f;
  const float backZ = baseZ + m_panelD;
  const float minV = float(m_min);
  const float maxV = float(m_max);
  const float invRange = 1.0f / qMax(1e-6f, (maxV - minV));
  const float dentMax = qMin(m_dentMax, m_panelD * 0.98f);

  const int border_count = m_skirtUVs.size();
  for (int i = 0; i < border_count; ++i) {
    const float u = m_skirtUVs[i].x();
    const float v = m_skirtUVs[i].y();
    const float value = sampleHeightValue_(u, v);
    const float t = qBound(0.0f, (value - minV) * invRange, 1.0f);
    const float h = t * dentMax;

    const float x = (u - 0.5f) * m_panelW;
    const float y = (v - 0.5f) * m_panelH;
    const QVector3D n = m_skirtNormals[i];

    const int topIndex = i * 2;
    const int botIndex = i * 2 + 1;

    const int topBase = topIndex * 8;
    m_skirtVertices[topBase + 0] = x;
    m_skirtVertices[topBase + 1] = y;
    m_skirtVertices[topBase + 2] = baseZ + h;
    m_skirtVertices[topBase + 3] = n.x();
    m_skirtVertices[topBase + 4] = n.y();
    m_skirtVertices[topBase + 5] = n.z();
    m_skirtVertices[topBase + 6] = u;
    m_skirtVertices[topBase + 7] = v;

    const int botBase = botIndex * 8;
    m_skirtVertices[botBase + 0] = x;
    m_skirtVertices[botBase + 1] = y;
    m_skirtVertices[botBase + 2] = backZ;
    m_skirtVertices[botBase + 3] = n.x();
    m_skirtVertices[botBase + 4] = n.y();
    m_skirtVertices[botBase + 5] = n.z();
    m_skirtVertices[botBase + 6] = u;
    m_skirtVertices[botBase + 7] = v;
  }

  glBindBuffer(GL_ARRAY_BUFFER, m_skirtVbo);
  glBufferSubData(GL_ARRAY_BUFFER, 0, m_skirtVertices.size() * sizeof(float),
                  m_skirtVertices.constData());
}

void GLWidget::renderBase_() {
  if (!m_baseProg || !m_baseVao || !m_heightTex || m_heightW <= 0 ||
      m_heightH <= 0) {
    return;
  }
  m_baseProg->bind();
  m_baseProg->setUniformValue("uMVP", m_mvp);
  m_baseProg->setUniformValue("uCameraPos", m_cameraPos);
  m_baseProg->setUniformValue("uLightDir",
                              QVector3D(-0.25f, 0.35f, -1.0f).normalized());
  m_baseProg->setUniformValue("uMinV", float(m_min));
  m_baseProg->setUniformValue("uMaxV", float(m_max));
  m_baseProg->setUniformValue(
      "uTexelSize",
      QVector2D(1.0f / float(m_heightW), 1.0f / float(m_heightH)));
  m_baseProg->setUniformValue("uColorZero", m_colorZero);
  m_baseProg->setUniformValue("uColorLow", m_colorLow);
  m_baseProg->setUniformValue("uColorMid", m_colorMid);
  m_baseProg->setUniformValue("uColorHigh", m_colorHigh);
  m_baseProg->setUniformValue("uHeightTex", 0);

  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, m_heightTex);

  glBindVertexArray(m_baseVao);
  glDrawElements(GL_TRIANGLES, m_baseIndexCount, GL_UNSIGNED_INT, nullptr);
  glBindVertexArray(0);
  glBindTexture(GL_TEXTURE_2D, 0);
  m_baseProg->release();
}

void GLWidget::renderSkirt_() {
  if (!m_baseProg || !m_skirtVao || m_skirtIndexCount <= 0 || !m_heightTex ||
      m_heightW <= 0 || m_heightH <= 0) {
    return;
  }
  m_baseProg->bind();
  m_baseProg->setUniformValue("uMVP", m_mvp);
  m_baseProg->setUniformValue("uCameraPos", m_cameraPos);
  m_baseProg->setUniformValue("uLightDir",
                              QVector3D(-0.25f, 0.35f, -1.0f).normalized());
  m_baseProg->setUniformValue("uMinV", float(m_min));
  m_baseProg->setUniformValue("uMaxV", float(m_max));
  m_baseProg->setUniformValue(
      "uTexelSize",
      QVector2D(1.0f / float(m_heightW), 1.0f / float(m_heightH)));
  m_baseProg->setUniformValue("uColorZero", m_colorZero);
  m_baseProg->setUniformValue("uColorLow", m_colorLow);
  m_baseProg->setUniformValue("uColorMid", m_colorMid);
  m_baseProg->setUniformValue("uColorHigh", m_colorHigh);
  m_baseProg->setUniformValue("uHeightTex", 0);

  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, m_heightTex);

  glBindVertexArray(m_skirtVao);
  glDrawElements(GL_TRIANGLES, m_skirtIndexCount, GL_UNSIGNED_INT, nullptr);
  glBindVertexArray(0);
  glBindTexture(GL_TEXTURE_2D, 0);
  m_baseProg->release();
}

void GLWidget::renderHeatmap_() {
  if (!m_heatmapProg || !m_heatmapVao || m_heatmapIndexCount <= 0 ||
      !m_heightTex) {
    return;
  }
  m_heatmapProg->bind();
  m_heatmapProg->setUniformValue("uMVP", m_mvp);
  m_heatmapProg->setUniformValue("uMinV", float(m_min));
  m_heatmapProg->setUniformValue("uMaxV", float(m_max));
  m_heatmapProg->setUniformValue("uHeightScale",
                                 qMin(m_dentMax, m_panelD * 0.98f));
  m_heatmapProg->setUniformValue("uBaseZ", -m_panelD * 0.5f);
  m_heatmapProg->setUniformValue(
      "uTexelSize",
      QVector2D(1.0f / float(m_heightW), 1.0f / float(m_heightH)));
  m_heatmapProg->setUniformValue("uPlaneSize", QVector2D(m_panelW, m_panelH));
  m_heatmapProg->setUniformValue("uCameraPos", m_cameraPos);
  m_heatmapProg->setUniformValue("uLightDir",
                                 QVector3D(-0.25f, 0.35f, -1.0f).normalized());
  m_heatmapProg->setUniformValue("uColorZero", m_colorZero);
  m_heatmapProg->setUniformValue("uColorLow", m_colorLow);
  m_heatmapProg->setUniformValue("uColorMid", m_colorMid);
  m_heatmapProg->setUniformValue("uColorHigh", m_colorHigh);
  m_heatmapProg->setUniformValue("uHeightTex", 0);

  glActiveTexture(GL_TEXTURE0);
  glBindTexture(GL_TEXTURE_2D, m_heightTex);

  glBindVertexArray(m_heatmapVao);
  glDrawElements(GL_TRIANGLES, m_heatmapIndexCount, GL_UNSIGNED_INT, nullptr);
  glBindVertexArray(0);

  glBindTexture(GL_TEXTURE_2D, 0);
  m_heatmapProg->release();
}

void GLWidget::initPrograms_() {
  // Qt Resource 里打包的 shader 文件路径：使用 `:/` 前缀（不是文件系统路径）
  const QString vsd_path = QStringLiteral(":/shaders/dots.vert");
  const QString fsd_path = QStringLiteral(":/shaders/dots.frag");
  const QString vsb_path = QStringLiteral(":/shaders/bg.vert");
  const QString fsb_path = QStringLiteral(":/shaders/bg.frag");
  const QString vsp_path = QStringLiteral(":/shaders/panel.vert");
  const QString fsp_path = QStringLiteral(":/shaders/panel.frag");
  const QString vsr_path = QStringLiteral(":/shaders/room.vert");
  const QString fsr_path = QStringLiteral(":/shaders/room.frag");
  const QString vsh_path = QStringLiteral(":/shaders/heatmap.vert");
  const QString fsh_path = QStringLiteral(":/shaders/heatmap.frag");
  const QString vsbase_path = QStringLiteral(":/shaders/base.vert");
  const QString fsbase_path = QStringLiteral(":/shaders/base.frag");

  {
    auto* p = new QOpenGLShaderProgram;
    const bool okV =
        p->addShaderFromSourceCode(QOpenGLShader::Vertex, readFile(vsb_path));
    const bool okF =
        p->addShaderFromSourceCode(QOpenGLShader::Fragment, readFile(fsb_path));
    const bool okL = okV && okF && p->link();
    if (!okL) {
      qWarning() << "bg program build failed:" << vsb_path << fsb_path
                 << p->log();
      delete p;
      p = nullptr;
    }
    m_bgProg = p;
  }
  {
    auto* p = new QOpenGLShaderProgram;
    const bool okV =
        p->addShaderFromSourceCode(QOpenGLShader::Vertex, readFile(vsp_path));
    const bool okF =
        p->addShaderFromSourceCode(QOpenGLShader::Fragment, readFile(fsp_path));
    const bool okL = okV && okF && p->link();
    if (!okL) {
      qWarning() << "panel program build failed:" << vsp_path << fsp_path
                 << p->log();
      delete p;
      p = nullptr;
    }
    m_panelProg = p;
  }
  {
    auto* p = new QOpenGLShaderProgram;
    const bool okV =
        p->addShaderFromSourceCode(QOpenGLShader::Vertex, readFile(vsd_path));
    const bool okF =
        p->addShaderFromSourceCode(QOpenGLShader::Fragment, readFile(fsd_path));
    const bool okL = okV && okF && p->link();
    if (!okL) {
      qWarning() << "dots program build failed:" << vsd_path << fsd_path
                 << p->log();
      delete p;
      p = nullptr;
    }
    m_dotsProg = p;
  }
  {
    auto* p = new QOpenGLShaderProgram;
    const bool okV =
        p->addShaderFromSourceCode(QOpenGLShader::Vertex, readFile(vsr_path));
    const bool okF =
        p->addShaderFromSourceCode(QOpenGLShader::Fragment, readFile(fsr_path));
    const bool okL = okV && okF && p->link();
    if (!okL) {
      qWarning() << "dots program build failed:" << vsr_path << fsr_path
                 << p->log();
      delete p;
      p = nullptr;
    }
    m_roomProg = p;
  }
  {
    auto* p = new QOpenGLShaderProgram;
    const bool okV =
        p->addShaderFromSourceCode(QOpenGLShader::Vertex, readFile(vsh_path));
    const bool okF =
        p->addShaderFromSourceCode(QOpenGLShader::Fragment, readFile(fsh_path));
    const bool okL = okV && okF && p->link();
    if (!okL) {
      qWarning() << "heatmap program build failed:" << vsh_path << fsh_path
                 << p->log();
      delete p;
      p = nullptr;
    }
    m_heatmapProg = p;
  }
  {
    auto* p = new QOpenGLShaderProgram;
    const bool okV = p->addShaderFromSourceCode(QOpenGLShader::Vertex,
                                                readFile(vsbase_path));
    const bool okF = p->addShaderFromSourceCode(QOpenGLShader::Fragment,
                                                readFile(fsbase_path));
    const bool okL = okV && okF && p->link();
    if (!okL) {
      qWarning() << "base program build failed:" << vsbase_path << fsbase_path
                 << p->log();
      delete p;
      p = nullptr;
    }
    m_baseProg = p;
  }
}

void GLWidget::updateInstanceBufferIfNeeded_() {
  if (dotCount() <= 0) {
    m_instanceCount = 0;
    return;
  }

  QVector<float> valuesCopy;
  bool dirty = false;
  {
    QMutexLocker lk(&m_dataMutex);
    dirty = m_valuesDirty;
    if (dirty) {
      valuesCopy = m_latestValues;
      m_valuesDirty = false;
    }
  }
  if (!dirty) return;

  const int n = dotCount();
  m_instanceCount = n;
  QVector<float> inst;
  // 每个点 3 个 float：offsetX, offsetZ, value（对应 dots.vert: iOffsetXZ +
  // iValue）
  inst.resize(n * 3);

  const float w = (m_cols - 1) * m_pitch;
  const float h = (m_rows - 1) * m_pitch;

  // 把点阵居中到原点附近（x/z 以中心对称）
  for (int i = 0; i < n; ++i) {
    const int r = (m_cols > 0) ? (i / m_cols) : 0;
    const int c = (m_cols > 0) ? (i % m_cols) : 0;

    const float x = (c * m_pitch) - w * 0.5f;
    const float z = (r * m_pitch) - h * 0.5f;

    inst[i * 3 + 0] = x;
    inst[i * 3 + 1] = z;
    inst[i * 3 + 2] = (i < valuesCopy.size()) ? valuesCopy[i] : m_min;
  }

  // 把 CPU 生成的 instance 数据上传到 GPU（后续 draw 时由 VAO 里的 attribute
  // 2/3 读取）
  glBindBuffer(GL_ARRAY_BUFFER, m_instanceVbo);
  glBufferSubData(GL_ARRAY_BUFFER, 0, inst.size() * sizeof(float),
                  inst.constData());
}

void GLWidget::updateMatrices_() {
  // MVP = Projection * View * Model
  //
  // OpenGL 的顶点最终要写到
  // gl_Position（裁剪空间），所以我们需要一个矩阵把“世界坐标”变成“屏幕可见”的坐标。
  // 这里为了入门简单：
  // - panel/dots 顶点数据直接当作世界坐标（Model=Identity）
  // - View 用 lookAt 放一个相机
  // - Projection 用透视投影（perspective）
  const int w = width();
  const int h = height();
  if (w <= 0 || h <= 0) {
    m_mvp.setToIdentity();
    return;
  }

  const float aspect = float(w) / float(h);
  const float radius = 0.5f * qSqrt(m_panelW * m_panelW + m_panelD * m_panelD);
  const float distance = qMax(0.5f, radius * 2.5f);

  // 让相机看向 panel 的中心点
  const QVector3D center(0.0f, 0.0f, 0.0f);

  // yaw/pitch 控制相机绕目标点“环绕”（orbit camera）
  const float yawRad = qDegreesToRadians(m_camYawDeg);
  const float pitchRad = qDegreesToRadians(m_camPitchDeg);
  const float cosPitch = qCos(pitchRad);
  const float sinPitch = qSin(pitchRad);

  const QVector3D eye = center + QVector3D(distance * cosPitch * qCos(yawRad),
                                           distance * sinPitch,
                                           distance * cosPitch * qSin(yawRad));
  m_cameraPos = eye;

  QMatrix4x4 view;
  view.lookAt(eye, center, QVector3D(0.0f, 1.0f, 0.0f));

  QMatrix4x4 proj;
  // fov=45°，near=0.01，far=足够大（保证 panel + dots 在裁剪范围内）
  proj.perspective(m_zoom_, aspect, 0.01f, qMax(10.0f, distance * 10.0f));

  m_mvp = proj * view;
}

void GLWidget::updateRoom_() {
  if (!m_roomProg || !room_vao || room_index_count <= 0) {
    return;
  }

  m_roomProg->bind();

  const float base = std::max({m_panelW, m_panelH, m_panelD});
  const QVector3D roomHalfSize(std::max(1.0f, base * 1.5f),
                               std::max(1.0f, base * 1.5f),
                               std::max(1.0f, base * 1.5f));

  const float minorStep = std::max(0.05f, base * 0.25f);
  const float majorStep = minorStep * 5.0f;

  m_roomProg->setUniformValue("uMVP", m_mvp);
  m_roomProg->setUniformValue("uCameraPos", m_cameraPos);
  m_roomProg->setUniformValue("uRoomHalfSize", roomHalfSize);
  m_roomProg->setUniformValue("uMinorStep", minorStep);
  m_roomProg->setUniformValue("uMajorStep", majorStep);
  m_roomProg->setUniformValue("uRenderMode", 1);
  m_roomProg->setUniformValue("uLightMode", m_lightMode);
  m_roomProg->setUniformValue("uShowGrid", m_showGrid);

  glBindVertexArray(room_vao);
  glDrawElements(GL_TRIANGLES, room_index_count, GL_UNSIGNED_INT, nullptr);
  glBindVertexArray(0);
  m_roomProg->release();
}

void GLWidget::setShowGrid(bool on) {
  if (m_showGrid == on) return;

  m_showGrid = on;
  update();
}

void GLWidget::setUseHeatmap(bool on) {
  if (m_useHeatmap == on) return;
  m_useHeatmap = on;
  update();
}

void GLWidget::setRow(int row) {
  row = qMax(0, row);
  if (m_rows == row) return;
  setSpec(row, m_cols, m_pitch, m_dotRadius);
}

void GLWidget::setCol(int col) {
  col = qMax(0, col);
  if (m_cols == col) return;
  setSpec(m_rows, col, m_pitch, m_dotRadius);
}
