From 01b988fcd7f4bca035e647b4a9cf67d162a63ab2 Mon Sep 17 00:00:00 2001 From: lenn Date: Thu, 29 Jan 2026 09:56:10 +0800 Subject: [PATCH] fix:TextInput support hot update;feat:add zero color and update algorithm --- CMakeLists.txt | 34 +- Configure | 1 - VERSION.txt | 2 +- docs/ARCHITECTURE.md | 39 + i18n/app_en_US.ts | 14 +- i18n/app_zh_CN.ts | 14 +- installer/TactileIpc3D.iss | 9 +- main.cpp | 6 + qml/content/ColorPickerDialog.qml | 9 + qml/content/LeftPanel.qml | 85 + shaders/base.frag | 50 +- shaders/base.vert | 3 + shaders/dots.frag | 4 +- shaders/heatmap.frag | 26 +- src/backend.cpp | 15 + src/backend.h | 11 + src/data_backend.cpp | 2 + src/globalhelper.h | 207 ++- src/glwidget.cpp | 2827 +++++++++++++++-------------- src/serial/serial_backend.cpp | 27 +- src/serial/serial_manager.cpp | 74 +- src/serial/serial_manager.h | 15 +- 22 files changed, 2022 insertions(+), 1452 deletions(-) delete mode 100644 Configure diff --git a/CMakeLists.txt b/CMakeLists.txt index 8c0786e..266dda4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,7 +31,7 @@ include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src) qt_standard_project_setup() -add_executable(TactileIpc3D +add_executable(TactileIpc3D WIN32 main.cpp resources.qrc src/translation_manager.h @@ -57,8 +57,6 @@ add_executable(TactileIpc3D src/serial/serial_qt_transport.h src/serial/serial_qt_transport.cpp src/serial/serial_types.h - src/serial/piezoresistive_a_protocol.h - src/serial/piezoresistive_a_protocol.cpp src/ringbuffer.h src/ringbuffer.cpp src/sparkline_plotitem.h @@ -79,6 +77,19 @@ target_link_libraries(TactileIpc3D PRIVATE Qt6::QuickDialogs2 QXlsx::QXlsx ) + +add_library(PiezoresistiveAPlugin SHARED + plugins/piezoresistive_a/piezoresistive_a_plugin.h + plugins/piezoresistive_a/piezoresistive_a_plugin.cpp + src/serial/piezoresistive_a_protocol.h + src/serial/piezoresistive_a_protocol.cpp +) +target_include_directories(PiezoresistiveAPlugin PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src +) +target_link_libraries(PiezoresistiveAPlugin PRIVATE + Qt6::Core +) target_include_directories(TactileIpc3D PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/3rdpart/OpenCV/include ) @@ -132,8 +143,25 @@ set_target_properties(TactileIpc3D PROPERTIES RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL "${runtime_out_dir}/MinSizeRel" ) +set_target_properties(PiezoresistiveAPlugin PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${runtime_out_dir}/plugins/decoders" + RUNTIME_OUTPUT_DIRECTORY_DEBUG "${runtime_out_dir}/Debug/plugins/decoders" + RUNTIME_OUTPUT_DIRECTORY_RELEASE "${runtime_out_dir}/Release/plugins/decoders" + RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${runtime_out_dir}/RelWithDebInfo/plugins/decoders" + RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL "${runtime_out_dir}/MinSizeRel/plugins/decoders" + LIBRARY_OUTPUT_DIRECTORY "${runtime_out_dir}/plugins/decoders" + LIBRARY_OUTPUT_DIRECTORY_DEBUG "${runtime_out_dir}/Debug/plugins/decoders" + LIBRARY_OUTPUT_DIRECTORY_RELEASE "${runtime_out_dir}/Release/plugins/decoders" + LIBRARY_OUTPUT_DIRECTORY_RELWITHDEBINFO "${runtime_out_dir}/RelWithDebInfo/plugins/decoders" + LIBRARY_OUTPUT_DIRECTORY_MINSIZEREL "${runtime_out_dir}/MinSizeRel/plugins/decoders" +) + include(GNUInstallDirs) install(TARGETS TactileIpc3D RUNTIME DESTINATION bin ) +install(TARGETS PiezoresistiveAPlugin + RUNTIME DESTINATION bin/plugins/decoders + LIBRARY DESTINATION bin/plugins/decoders +) diff --git a/Configure b/Configure deleted file mode 100644 index f1c8738..0000000 --- a/Configure +++ /dev/null @@ -1 +0,0 @@ -== diff --git a/VERSION.txt b/VERSION.txt index 3eefcb9..1d0ba9e 100644 --- a/VERSION.txt +++ b/VERSION.txt @@ -1 +1 @@ -1.0.0 +0.4.0 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 980b2c6..86cfc74 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -461,6 +461,44 @@ classDiagram - `m_protocols`:协议字典。 - `m_activeName`:当前协议名。 +### 协议插件化(`plugins/decoders`) +- 目标:每个协议作为独立 DLL 插件,可按需安装与动态加载。 +- 关键接口: + - `IProtocolPlugin`:暴露 `protocolName()` / `apiVersion()` / `createBundle()`。 + - `ProtocolBundle`:`codec/decoder/format` 三件套。 +- 加载流程: + - `SerialBackend` 启动时调用 `SerialManager::loadPlugins()`。 + - `SerialManager` 扫描 `appDir/plugins/decoders` 目录,按平台过滤后缀(Windows: `.dll`)。 + - 校验 `apiVersion`,创建并注册协议 bundle。 + - 通过 `m_pluginLoaders` 保持插件常驻,避免对象失效。 + +#### 插件 UML (Mermaid) +```mermaid +classDiagram + class IProtocolPlugin { + +protocolName() + +apiVersion() + +createBundle() + } + class ProtocolBundle { + +codec + +decoder + +format + } + class SerialManager { + +loadPlugins(dir) + +registerProtocol() + +setActiveProtocol() + } + class SerialBackend { + +initPlugins() + } + + SerialBackend --> SerialManager + SerialManager --> IProtocolPlugin + IProtocolPlugin --> ProtocolBundle +``` + ### ISerialTransport (`src/serial/serial_transport.h`) - 作用:串口传输抽象层,屏蔽不同平台差异。 - 接口: @@ -633,6 +671,7 @@ classDiagram ## 更新记录 +- 2026-01-20:协议解码插件化(DLL),SerialBackend 启动时动态加载 `plugins/decoders`,安装器支持按组件选择协议插件。 - 2026-01-11:新增 `QtSerialTransport`(`QSerialPort` 传输实现)并设为默认传输;补齐点选拾取逻辑;新增数据流/时序图并补充可视化 TODO 说明。 - 2026-01-12:`LeftPanel` 新增颜色映射参数;`AppBackend`/`GLWidget` 增加颜色与范围接口,shader 使用三色渐变映射数据值。 - 2026-01-11:补充串口配置流程图与配置接口说明(协议/参数/解码器绑定/打开流程)。 diff --git a/i18n/app_en_US.ts b/i18n/app_en_US.ts index 958f138..245ef6c 100644 --- a/i18n/app_en_US.ts +++ b/i18n/app_en_US.ts @@ -184,6 +184,10 @@ 显示网络 Show Grid + + 凹陷表面 + Dented Surface + 显示坐标轴 Show Axes @@ -208,7 +212,15 @@ 选择高色 Select High Color - + + 零色 + Zero Color + + + 选择零色 + Select Zero Color + + OpenFileDialog diff --git a/i18n/app_zh_CN.ts b/i18n/app_zh_CN.ts index d783a0a..555ea93 100644 --- a/i18n/app_zh_CN.ts +++ b/i18n/app_zh_CN.ts @@ -148,6 +148,10 @@ 显示网络 显示网络 + + 凹陷表面 + 凹陷表面 + 显示坐标轴 显示坐标轴 @@ -160,7 +164,15 @@ 导出数据 导出数据 - + + 零色 + 零色 + + + 选择零色 + 选择零色 + + RightPanel diff --git a/installer/TactileIpc3D.iss b/installer/TactileIpc3D.iss index ddc08df..13499c2 100644 --- a/installer/TactileIpc3D.iss +++ b/installer/TactileIpc3D.iss @@ -1,5 +1,5 @@ #define MyAppName "TactileIpc3D" -#define MyAppVersion "0.2.0" +#define MyAppVersion "0.4.0" #define MyAppPublisher "TactileIpc3D" #define MyAppExeName "TactileIpc3D.exe" #define SourceDir "..\\build\Desktop_Qt_6_8_3_MinGW_64_bit-Release\\out\Release" @@ -41,8 +41,13 @@ Name: "english"; MessagesFile: "compiler:Default.isl" [Tasks] Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked +[Components] +Name: "core"; Description: "Core Application"; Types: full compact custom; Flags: fixed +Name: "decoder_pzr_a"; Description: "Piezoresistive A Decoder (PZR-A)"; Types: full + [Files] -Source: "{#SourceDir}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "{#SourceDir}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: core; Excludes: "plugins\\decoders\\*.dll" +Source: "{#SourceDir}\\plugins\\decoders\\libPiezoresistiveAPlugin.dll"; DestDir: "{app}\\plugins\\decoders"; Flags: ignoreversion; Components: decoder_pzr_a [Icons] Name: "{group}\\{#MyAppName}"; Filename: "{app}\\{#MyAppExeName}" diff --git a/main.cpp b/main.cpp index e07d938..11dfa7d 100644 --- a/main.cpp +++ b/main.cpp @@ -116,9 +116,11 @@ int main(int argc, char *argv[]) { glw->setSpec(12, 7, 0.1f, 0.03f); glw->setPanelThickness(0.08f); glw->setRange(backend.rangeMin(), backend.rangeMax()); + glw->setColorZero(backend.colorZero()); glw->setColorLow(backend.colorLow()); glw->setColorMid(backend.colorMid()); glw->setColorHigh(backend.colorHigh()); + glw->setUseHeatmap(backend.useHeatmap()); /* backend.data()->setLiveRenderCallback([glw](const DataFrame& frame) { if (frame.data.size() != glw->dotCount()) @@ -177,12 +179,16 @@ int main(int argc, char *argv[]) { QObject::connect(&backend, &AppBackend::showGridChanged, glw, &GLWidget::setShowGrid); + QObject::connect(&backend, &AppBackend::useHeatmapChanged, glw, + &GLWidget::setUseHeatmap); QObject::connect(&backend, &AppBackend::sensorRowChanged, glw, &GLWidget::setRow); QObject::connect(&backend, &AppBackend::sensorColChanged, glw, &GLWidget::setCol); QObject::connect(&backend, &AppBackend::rangeChanged, glw, &GLWidget::setRange); + QObject::connect(&backend, &AppBackend::colorZeroChanged, glw, + &GLWidget::setColorZero); QObject::connect(&backend, &AppBackend::colorLowChanged, glw, &GLWidget::setColorLow); QObject::connect(&backend, &AppBackend::colorMidChanged, glw, diff --git a/qml/content/ColorPickerDialog.qml b/qml/content/ColorPickerDialog.qml index 6cf8763..044dba5 100644 --- a/qml/content/ColorPickerDialog.qml +++ b/qml/content/ColorPickerDialog.qml @@ -503,11 +503,20 @@ Window { border.color: root.controlBorder } + function applyIfValid() { + const c = root.parseHex(text) + if (c) root.syncFromColor(c) + } + onTextEdited: applyIfValid() onEditingFinished: { const c = root.parseHex(text) if (c) root.syncFromColor(c) else text = root.colorToHexAARRGGBB(root.color) } + onAccepted: { + applyIfValid() + focus = false + } } diff --git a/qml/content/LeftPanel.qml b/qml/content/LeftPanel.qml index e7d9b46..c90a3b4 100644 --- a/qml/content/LeftPanel.qml +++ b/qml/content/LeftPanel.qml @@ -63,6 +63,7 @@ Rectangle { RowLayout { Layout.fillWidth: true + Layout.topMargin: 4 spacing: 8 Label { @@ -167,6 +168,7 @@ Rectangle { text: root.formatHexByte(Backend.serial.deviceAddress) placeholderText: "0x01" inputMethodHints: Qt.ImhPreferUppercase + property bool _internalUpdate: false validator: RegularExpressionValidator { regularExpression: /^(0x|0X)?[0-9a-fA-F]{1,2}$/ } @@ -183,7 +185,9 @@ Rectangle { Connections { target: Backend.serial function onDeviceAddressChanged() { + addrField._internalUpdate = true addrField.text = root.formatHexByte(Backend.serial.deviceAddress) + addrField._internalUpdate = false } } } @@ -423,12 +427,31 @@ Rectangle { color: root.textColor } SpinBox { + id: rangeMinBox Layout.fillWidth: true from: -999999 to: 999999 editable: true value: Backend.rangeMin + function applyTextEdit() { + if (!contentItem) return + const parsed = valueFromText(contentItem.text, locale) + if (isNaN(parsed)) return + const clamped = Math.max(from, Math.min(to, parsed)) + if (Backend.rangeMin !== clamped) { + Backend.rangeMin = clamped + } + } onValueModified: Backend.rangeMin = value + Connections { + target: rangeMinBox.contentItem + function onTextEdited() { rangeMinBox.applyTextEdit() } + function onEditingFinished() { rangeMinBox.applyTextEdit() } + function onAccepted() { + rangeMinBox.applyTextEdit() + rangeMinBox.focus = false + } + } } } @@ -441,12 +464,62 @@ Rectangle { color: root.textColor } SpinBox { + id: rangeMaxBox Layout.fillWidth: true from: -999999 to: 999999 editable: true value: Backend.rangeMax + function applyTextEdit() { + if (!contentItem) return + const parsed = valueFromText(contentItem.text, locale) + if (isNaN(parsed)) return + const clamped = Math.max(from, Math.min(to, parsed)) + if (Backend.rangeMax !== clamped) { + Backend.rangeMax = clamped + } + } onValueModified: Backend.rangeMax = value + Connections { + target: rangeMaxBox.contentItem + function onTextEdited() { rangeMaxBox.applyTextEdit() } + function onEditingFinished() { rangeMaxBox.applyTextEdit() } + function onAccepted() { + rangeMaxBox.applyTextEdit() + rangeMaxBox.focus = false + } + } + } + } + RowLayout { + Layout.fillWidth: true + spacing: 8 + Label { + text: root.tr("零色") + Layout.preferredWidth: 90 + color: root.textColor + } + Rectangle { + width: 22 + height: 22 + radius: 4 + color: Backend.colorZero + border.width: 1 + border.color: Qt.rgba(0, 0, 0, 0.2) + Layout.alignment: Qt.AlignVCenter + MouseArea { + anchors.fill: parent + onClicked: { + zeroColorDialog.openWith(Backend.colorZero) + } + } + } + Button { + text: root.tr("选择") + Layout.fillWidth: true + onClicked: { + zeroColorDialog.openWith(Backend.colorZero) + } } } @@ -557,6 +630,11 @@ Rectangle { checked: Backend.showGrid onToggled: Backend.showGrid = checked } + CheckBox { + text: root.tr("凹陷表面") + checked: Backend.useHeatmap + onToggled: Backend.useHeatmap = checked + } CheckBox { text: root.tr("显示坐标轴") checked: false @@ -589,6 +667,13 @@ Rectangle { Item { Layout.fillHeight: true } } } + + ColorPickerDialog { + id: zeroColorDialog + title: root.tr("选择零色") + onAccepted: Backend.colorZero = c + } + ColorPickerDialog { id: lowColorDialog diff --git a/shaders/base.frag b/shaders/base.frag index 1ecd2ea..57b5f72 100644 --- a/shaders/base.frag +++ b/shaders/base.frag @@ -2,14 +2,51 @@ in vec3 vNormal; in vec3 vWorldPos; +in vec2 vUV; out vec4 FragColor; uniform vec3 uCameraPos; uniform vec3 uLightDir; -uniform vec3 uColor; +uniform sampler2D uHeightTex; +uniform float uMinV; +uniform float uMaxV; +uniform vec2 uTexelSize; +uniform vec3 uColorZero; +uniform vec3 uColorLow; +uniform vec3 uColorMid; +uniform vec3 uColorHigh; float saturate(float x) { return clamp(x, 0.0, 1.0); } +float value01(float v) { + return saturate((v - uMinV) / max(uMaxV - uMinV, 1e-6)); +} + +vec3 colorRamp(float t) { + if (t < 0.5) { + return mix(uColorLow, uColorMid, t / 0.5); + } + return mix(uColorMid, uColorHigh, (t - 0.5) / 0.5); +} + +float maxNeighborValue(vec2 uv) { + vec2 texSizeF = 1.0 / uTexelSize; + ivec2 texSizeI = ivec2(max(vec2(1.0), floor(texSizeF + 0.5))); + ivec2 maxCoord = texSizeI - ivec2(1); + + vec2 texelF = clamp(uv * texSizeF, vec2(0.0), vec2(maxCoord)); + ivec2 base = ivec2(floor(texelF)); + ivec2 base10 = min(base + ivec2(1, 0), maxCoord); + ivec2 base01 = min(base + ivec2(0, 1), maxCoord); + ivec2 base11 = min(base + ivec2(1, 1), maxCoord); + + float v00 = texelFetch(uHeightTex, base, 0).r; + float v10 = texelFetch(uHeightTex, base10, 0).r; + float v01 = texelFetch(uHeightTex, base01, 0).r; + float v11 = texelFetch(uHeightTex, base11, 0).r; + return max(max(v00, v10), max(v01, v11)); +} + void main() { vec3 N = normalize(vNormal); vec3 L = normalize(uLightDir); @@ -19,7 +56,16 @@ void main() { float diff = saturate(dot(N, L)); float spec = pow(saturate(dot(N, H)), 24.0); - vec3 col = uColor * (0.35 + 0.65 * diff); + float vC = texture(uHeightTex, vUV).r; + bool isZero = abs(vC) <= 1e-6; + float t = value01(vC); + float m = maxNeighborValue(vUV); + float eps = max(1e-4, 0.001 * (uMaxV - uMinV)); + float force = smoothstep(uMaxV - eps, uMaxV, m); + t = max(t, force); + vec3 base = isZero ? uColorZero : colorRamp(t); + + vec3 col = base * (0.35 + 0.65 * diff); col += vec3(1.0) * spec * 0.10; FragColor = vec4(clamp(col, 0.0, 1.0), 1.0); } diff --git a/shaders/base.vert b/shaders/base.vert index 6ebd6c2..a0e7b21 100644 --- a/shaders/base.vert +++ b/shaders/base.vert @@ -2,14 +2,17 @@ layout(location = 0) in vec3 aPos; layout(location = 1) in vec3 aNormal; +layout(location = 2) in vec2 aUV; out vec3 vNormal; out vec3 vWorldPos; +out vec2 vUV; uniform mat4 uMVP; void main() { vNormal = aNormal; vWorldPos = aPos; + vUV = aUV; gl_Position = uMVP * vec4(aPos, 1.0); } diff --git a/shaders/dots.frag b/shaders/dots.frag index 6cdaaed..0e39f42 100644 --- a/shaders/dots.frag +++ b/shaders/dots.frag @@ -6,6 +6,7 @@ out vec4 FragColor; uniform float uMinV; uniform float uMaxV; +uniform vec3 uColorZero; uniform vec3 uColorLow; uniform vec3 uColorMid; uniform vec3 uColorHigh; @@ -112,7 +113,8 @@ void main() { // data color directly (no remaining gold tint), while preserving depth cues. vec3 metalBase = vec3(0.98, 0.82, 0.30); float value01 = clamp((vValue - uMinV) / max(1e-6, (uMaxV - uMinV)), 0.0, 1.0); - vec3 dataCol = dataColorRamp(value01); + bool isZero = abs(vValue) <= 1e-6; + vec3 dataCol = isZero ? uColorZero : dataColorRamp(value01); bool hasData = (uHasData != 0); vec3 baseColor = hasData ? dataCol : metalBase; diff --git a/shaders/heatmap.frag b/shaders/heatmap.frag index 2205794..af4a260 100644 --- a/shaders/heatmap.frag +++ b/shaders/heatmap.frag @@ -12,6 +12,7 @@ uniform vec2 uTexelSize; uniform vec2 uPlaneSize; uniform vec3 uCameraPos; uniform vec3 uLightDir; +uniform vec3 uColorZero; uniform vec3 uColorLow; uniform vec3 uColorMid; uniform vec3 uColorHigh; @@ -22,6 +23,24 @@ float value01(float v) { return saturate((v - uMinV) / max(uMaxV - uMinV, 1e-6)); } +float maxNeighborValue(vec2 uv) { + vec2 texSizeF = 1.0 / uTexelSize; + ivec2 texSizeI = ivec2(max(vec2(1.0), floor(texSizeF + 0.5))); + ivec2 maxCoord = texSizeI - ivec2(1); + + vec2 texelF = clamp(uv * texSizeF, vec2(0.0), vec2(maxCoord)); + ivec2 base = ivec2(floor(texelF)); + ivec2 base10 = min(base + ivec2(1, 0), maxCoord); + ivec2 base01 = min(base + ivec2(0, 1), maxCoord); + ivec2 base11 = min(base + ivec2(1, 1), maxCoord); + + float v00 = texelFetch(uHeightTex, base, 0).r; + float v10 = texelFetch(uHeightTex, base10, 0).r; + float v01 = texelFetch(uHeightTex, base01, 0).r; + float v11 = texelFetch(uHeightTex, base11, 0).r; + return max(max(v00, v10), max(v01, v11)); +} + vec3 colorRamp(float t) { if (t < 0.5) { return mix(uColorLow, uColorMid, t / 0.5); @@ -31,8 +50,13 @@ vec3 colorRamp(float t) { void main() { float vC = texture(uHeightTex, vUV).r; + bool isZero = abs(vC) <= 1e-6; float t = value01(vC); - vec3 base = colorRamp(t); + float m = maxNeighborValue(vUV); + float eps = max(1e-4, 0.001 * (uMaxV - uMinV)); + float force = smoothstep(uMaxV - eps, uMaxV, m); + t = max(t, force); + vec3 base = isZero ? uColorZero : colorRamp(t); float vL = texture(uHeightTex, vUV - vec2(uTexelSize.x, 0.0)).r; float vR = texture(uHeightTex, vUV + vec2(uTexelSize.x, 0.0)).r; diff --git a/src/backend.cpp b/src/backend.cpp index b8e4073..573d1cf 100644 --- a/src/backend.cpp +++ b/src/backend.cpp @@ -5,6 +5,7 @@ #include "backend.h" #include "data_backend.h" #include "serial/serial_backend.h" +#include #include AppBackend::AppBackend(QObject* parent) @@ -51,6 +52,13 @@ void AppBackend::setShowGrid(bool on) { emit showGridChanged(on); } +void AppBackend::setUseHeatmap(bool on) { + if (m_useHeatmap == on) + return; + m_useHeatmap = on; + emit useHeatmapChanged(on); +} + void AppBackend::setSensorCol(int c) { if (m_serial->connected()) { return; @@ -93,6 +101,13 @@ void AppBackend::setRangeMax(int v) { emit rangeChanged(m_rangeMin, m_rangeMax); } +void AppBackend::setColorZero(const QColor& color) { + if (m_colorZero == color) + return; + m_colorZero = color; + emit colorZeroChanged(m_colorZero); +} + void AppBackend::setColorLow(const QColor& color) { if (m_colorLow == color) return; diff --git a/src/backend.h b/src/backend.h index 828b505..81da272 100644 --- a/src/backend.h +++ b/src/backend.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "data_backend.h" @@ -18,12 +19,14 @@ class AppBackend : public QObject { Q_PROPERTY(QString language READ language WRITE setLanguage NOTIFY languageChanged) Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) Q_PROPERTY(bool showGrid READ showGrid WRITE setShowGrid NOTIFY showGridChanged); + Q_PROPERTY(bool useHeatmap READ useHeatmap WRITE setUseHeatmap NOTIFY useHeatmapChanged); Q_PROPERTY(SerialBackend* serial READ serial CONSTANT) Q_PROPERTY(DataBackend* data READ data CONSTANT) Q_PROPERTY(int sensorCol READ sensorCol WRITE setSensorCol NOTIFY sensorColChanged); Q_PROPERTY(int sensorRow READ sensorRow WRITE setSensorRow NOTIFY sensorRowChanged); Q_PROPERTY(int rangeMin READ rangeMin WRITE setRangeMin NOTIFY rangeMinChanged); Q_PROPERTY(int rangeMax READ rangeMax WRITE setRangeMax NOTIFY rangeMaxChanged); + Q_PROPERTY(QColor colorZero READ colorZero WRITE setColorZero NOTIFY colorZeroChanged); Q_PROPERTY(QColor colorLow READ colorLow WRITE setColorLow NOTIFY colorLowChanged); Q_PROPERTY(QColor colorMid READ colorMid WRITE setColorMid NOTIFY colorMidChanged); Q_PROPERTY(QColor colorHigh READ colorHigh WRITE setColorHigh NOTIFY colorHighChanged); @@ -43,6 +46,8 @@ public: bool showGrid() const { return m_showGrid; } void setShowGrid(bool on); + bool useHeatmap() const { return m_useHeatmap; } + void setUseHeatmap(bool on); int sensorCol() const { qInfo() << "col:" << m_sensorCol; return m_sensorCol; } int sensorRow() const { qInfo() << "row:" << m_sensorRow; return m_sensorRow; } void setSensorRow(int r); @@ -51,9 +56,11 @@ public: int rangeMax() const { return m_rangeMax; } void setRangeMin(int v); void setRangeMax(int v); + QColor colorZero() const { return m_colorZero; } QColor colorLow() const { return m_colorLow; } QColor colorMid() const { return m_colorMid; } QColor colorHigh() const { return m_colorHigh; } + void setColorZero(const QColor& color); void setColorLow(const QColor& color); void setColorMid(const QColor& color); void setColorHigh(const QColor& color); @@ -63,11 +70,13 @@ signals: void languageChanged(); void connectedChanged(); void showGridChanged(bool on); + void useHeatmapChanged(bool on); void sensorColChanged(int c); void sensorRowChanged(int r); void rangeMinChanged(int v); void rangeMaxChanged(int v); void rangeChanged(int minV, int maxV); + void colorZeroChanged(const QColor& color); void colorLowChanged(const QColor& color); void colorMidChanged(const QColor& color); void colorHighChanged(const QColor& color); @@ -78,10 +87,12 @@ private: QString m_language = QStringLiteral("zh_CN"); bool m_showGrid = true; + bool m_useHeatmap = false; int m_sensorRow = 12; int m_sensorCol = 7; int m_rangeMin = 0; int m_rangeMax = 1000; + QColor m_colorZero = QColor::fromRgbF(0.10, 0.75, 1.00); QColor m_colorLow = QColor::fromRgbF(0.10, 0.75, 1.00); QColor m_colorMid = QColor::fromRgbF(0.10, 0.95, 0.35); QColor m_colorHigh = QColor::fromRgbF(1.00, 0.22, 0.10); diff --git a/src/data_backend.cpp b/src/data_backend.cpp index a7b5766..4dce6ef 100644 --- a/src/data_backend.cpp +++ b/src/data_backend.cpp @@ -127,6 +127,8 @@ bool DataBackend::importXlsx(const QString& path) { cells[r][c] = v; } } + + return true; } void DataBackend::startPlayback(int intervalMs) { diff --git a/src/globalhelper.h b/src/globalhelper.h index 79c63a9..6266c24 100644 --- a/src/globalhelper.h +++ b/src/globalhelper.h @@ -5,48 +5,217 @@ #ifndef TACTILEIPC3D_GLOBALHELPER_H #define TACTILEIPC3D_GLOBALHELPER_H +#include #include #include -#include +#include +#include +#include +#include + +const QString APP_VERSION = "0.4.0"; class GlobalHelper { -public: const std::string fuck = "fuck you lsp!!!"; - static GlobalHelper* Instance() { + +public : + static GlobalHelper *Instance() { static GlobalHelper ins; return &ins; } - static void transToMultiMatrix(const cv::Mat& raw_data, float min_threshold, - float max_range, cv::Size display_res, cv::Mat& out_matrix) { + static void transToMultiMatrix(const cv::Mat &raw_data, float min_threshold, + float max_range, cv::Size display_res, + cv::Mat &out_matrix) { CV_Assert(raw_data.type() == CV_32F); + const float safe_max = std::max(max_range, 1e-6f); + const float safe_span = std::max(safe_max - min_threshold, 1e-6f); + + cv::Mat saturate_mask_u8; + cv::compare(raw_data, max_range, saturate_mask_u8, cv::CMP_GE); + cv::Mat zero_mask_u8; + cv::compare(raw_data, 0.0f, zero_mask_u8, cv::CMP_LE); + cv::Mat raw = raw_data.clone(); - raw.setTo(0.0f, raw < min_threshold); + cv::min(raw, safe_max, raw); - double maxVal = 0.0; - cv::minMaxLoc(raw, nullptr, &maxVal); - if (maxVal > 0.0) { - cv::GaussianBlur(raw, raw, cv::Size(3, 3), 0.8, 0.8, cv::BORDER_DEFAULT); - } + cv::Mat peak_norm = (raw - min_threshold) / safe_span; + cv::max(peak_norm, 0.0f, peak_norm); + cv::min(peak_norm, 1.0f, peak_norm); - const float safe_range = std::max(max_range, 1e-6f); - cv::Mat norm_data = raw / safe_range; - cv::min(norm_data, 1.0f, norm_data); + cv::medianBlur(raw, raw, 3); + cv::GaussianBlur(raw, raw, cv::Size(5, 5), 1.2, 1.2, cv::BORDER_DEFAULT); + + cv::Mat norm_data = (raw - min_threshold) / safe_span; cv::max(norm_data, 0.0f, norm_data); + cv::min(norm_data, 1.0f, norm_data); - cv::pow(norm_data, 0.7, norm_data); + cv::pow(norm_data, 1.4, norm_data); cv::Mat smoothed; cv::resize(norm_data, smoothed, display_res, 0.0, 0.0, cv::INTER_CUBIC); + cv::max(smoothed, 0.0f, smoothed); + cv::min(smoothed, 1.0f, smoothed); - cv::GaussianBlur(smoothed, smoothed, cv::Size(31, 31), 0.0, 0.0, cv::BORDER_DEFAULT); + const int min_dim = + std::max(1, std::min(display_res.width, display_res.height)); + const double sigma = std::max(1.0, 0.02 * min_dim); + int ksize = std::max(3, (static_cast(std::ceil(sigma * 6)) | 1)); + cv::GaussianBlur(smoothed, smoothed, cv::Size(ksize, ksize), sigma, sigma, + cv::BORDER_DEFAULT); + cv::max(smoothed, 0.0f, smoothed); + cv::min(smoothed, 1.0f, smoothed); - smoothed *= safe_range; + cv::Mat peak_resized; + cv::resize(peak_norm, peak_resized, display_res, 0.0, 0.0, + cv::INTER_CUBIC); + cv::max(smoothed, peak_resized, smoothed); + + if (!saturate_mask_u8.empty()) { + cv::Mat mask_f; + saturate_mask_u8.convertTo(mask_f, CV_32F, 1.0 / 255.0); + cv::Mat mask_resized; + cv::resize(mask_f, mask_resized, display_res, 0.0, 0.0, + cv::INTER_CUBIC); + // Soften the threshold to avoid blocky plateaus. + const double mask_sigma = std::max(0.5, 0.008 * min_dim); + int mask_ksize = + std::max(3, (static_cast(std::ceil(mask_sigma * 6)) | 1)); + cv::GaussianBlur(mask_resized, mask_resized, + cv::Size(mask_ksize, mask_ksize), mask_sigma, + mask_sigma, cv::BORDER_DEFAULT); + cv::max(mask_resized, 0.0f, mask_resized); + cv::min(mask_resized, 1.0f, mask_resized); + smoothed = cv::max(smoothed, mask_resized); + } + + if (!zero_mask_u8.empty()) { + cv::Mat zero_f; + zero_mask_u8.convertTo(zero_f, CV_32F, 1.0 / 255.0); + cv::Mat zero_resized; + cv::resize(zero_f, zero_resized, display_res, 0.0, 0.0, + cv::INTER_CUBIC); + const double zero_sigma = std::max(0.5, 0.006 * min_dim); + int zero_ksize = + std::max(3, (static_cast(std::ceil(zero_sigma * 6)) | 1)); + cv::GaussianBlur(zero_resized, zero_resized, + cv::Size(zero_ksize, zero_ksize), zero_sigma, + zero_sigma, cv::BORDER_DEFAULT); + cv::max(zero_resized, 0.0f, zero_resized); + cv::min(zero_resized, 1.0f, zero_resized); + cv::Mat zero_bin; + cv::threshold(zero_resized, zero_bin, 0.5, 255.0, + cv::THRESH_BINARY); + zero_bin.convertTo(zero_bin, CV_8U); + smoothed.setTo(0.0f, zero_bin); + } + + smoothed *= safe_max; out_matrix = smoothed; } + + static void SaveGridConfig(bool b) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + settings.setValue("display/grid", b); + } + + static void SaveAxisConfig(bool b) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + settings.setValue("display/axis", b); + } + + static void SaveSurfaceConfig(bool b) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + settings.setValue("display/surface", b); + } + + static void GetGridConfig(bool& b) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + b = settings.value("display/grid", b).toBool(); + } + + static void GetAxisConfig(bool& b) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + b = settings.value("display/axis", b).toBool(); + } + + static void GetSurfaceConfig(bool b) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + b = settings.value("display/surface", b).toBool(); + } + + static void SaveLastRow(int value) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + settings.setValue("spec/row", value); + } + + static void GetLastRow(int& value) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + value = settings.value("spec/row", value).toInt(); + } + + static void GetLastCol(int value) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + settings.setValue("spec/col", value); + } + + static void GetLastCol(int& value) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + value = settings.value("spec/col", value).toInt(); + } + + static void SaveZeroColor(QString color) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + settings.setValue("color/zero", color); + } + + static void GetZeroColor(QString& color) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + color = settings.value("color/zero", color).toString(); + } + + static void SaveLowColor(QString color) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + settings.setValue("color/low", color); + } + + static void GetLowColor(QString& color) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + color = settings.value("color/low", color).toString(); + } + + static void SaveMidColor(QString color) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + settings.setValue("color/mid", color); + } + + static void GetMidColor(QString& color) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + color = settings.value("color/mid", color).toString(); + } + + static void SaveHighColor(QString color) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + settings.setValue("color/high", color); + } + + static void GetHighColor(QString& color) { + QSettings settings(GlobalHelper::GetConfigFilePath(), QSettings::IniFormat); + color = settings.value("color/high", color).toString(); + } + + static QString GetConfigFilePath() { + return QCoreApplication::applicationDirPath() + QDir::separator() + "conf.ini"; + } + + static QString GetAppVersion() { + return APP_VERSION; + } + private: - GlobalHelper() {} + GlobalHelper() { + } }; -#endif //TACTILEIPC3D_GLOBALHELPER_H +#endif // TACTILEIPC3D_GLOBALHELPER_H diff --git a/src/glwidget.cpp b/src/glwidget.cpp index a6bc530..166f056 100644 --- a/src/glwidget.cpp +++ b/src/glwidget.cpp @@ -3,11 +3,7 @@ // #include "glwidget.h" -#include -#include -#include -#include -#include + #include #include #include @@ -15,1688 +11,1713 @@ #include #include #include -#include -#include -#include -#include -#include + +#include +#include +#include #include +#include +#include +#include #include +#include +#include +#include +#include +#include #include -#include -#include #include +#include +#include + #include "globalhelper.h" -// 读取文本文件内容(这里主要用来从 Qt Resource `:/shaders/...` 读取 shader 源码) + +// 读取文本文件内容(这里主要用来从 Qt Resource `:/shaders/...` 读取 shader +// 源码) static QByteArray readFile(const QString& path) { - QFile f(path); - if (!f.open(QIODevice::ReadOnly)) return {}; - return f.readAll(); + 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; - } + for (int i = 0; i < 16; i++) { + m[i] = 0; + } - m[0] = m[5] = m[10] = m[15] = 1; + m[0] = m[5] = m[10] = m[15] = 1; } static QVector3D toColorVec(const QColor& color) { - return QVector3D(color.redF(), color.greenF(), color.blueF()); + return QVector3D(color.redF(), color.greenF(), color.blueF()); } -GLWidget::GLWidget(QWidget *parent) - : QOpenGLWidget(parent) { - setMinimumSize(640, 480); +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; + 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_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(); + 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(); + 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(); + 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); + 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; + // 自动根据点阵尺寸调整 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(); + 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 &values) { - if (values.size() != dotCount()) return; +void GLWidget::submitValues(const QVector& values) { + if (values.size() != dotCount()) return; - { - QMutexLocker lk(&m_dataMutex); - m_latestValues = values; - m_valuesDirty = true; - m_hasData = true; - m_heightDirty = true; - } + { + QMutexLocker lk(&m_dataMutex); + m_latestValues = values; + m_valuesDirty = true; + m_hasData = true; + m_heightDirty = true; + } - update(); + update(); } void GLWidget::setRange(int minV, int maxV) { - m_min = minV; - m_max = maxV; - m_heightDirty = true; - update(); + 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(); + 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; - } +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(); + 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; - } +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(); + 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; - } + if (on == m_lightMode) { + return; + } - m_lightMode = on; - update(); + m_lightMode = on; + update(); } void GLWidget::setShowBg(bool on = true) { - if (on == m_showBg) { - return; - } - m_showBg = on; - update(); + 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(); + 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(); + 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(); + const QVector3D next = toColorVec(color); + if (m_colorHigh == next) return; + m_colorHigh = next; + update(); } void GLWidget::initializeGL() { - initializeOpenGLFunctions(); + 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; + // 基础状态:开启深度测试,否则 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); + matIdentity(m_modelPanel); + matIdentity(m_view); + matIdentity(m_proj); } void GLWidget::initGeometry_() { - initDotTexture_(); - initBackgroundGeometry_(); + 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_(); - initDotGeometry_(); - initRoomGeometry_(); 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_(); - initHeightTexture_(); + 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 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::resizeGL(int w, int h) { - glViewport(0, 0, w, h); -} - -void GLWidget::paintGL() { - 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_(); +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) { - updateHeatmapData_(); - uploadHeightTexture_(); - updateSkirtVertices_(); - renderBase_(); - renderSkirt_(); - renderHeatmap_(); - } else { - // 2) 如果外部提交了新数据,就把 CPU 生成的 instance 数据更新到 GPU - updateInstanceBufferIfNeeded_(); + event->accept(); + return; } - - 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(); + 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); } - - 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("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(); - } - - if (!m_useHeatmap && m_labelMode != LabelsOff && dotCount() > 0) { - QVector 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; - - QPainter painter(this); - painter.setRenderHint(QPainter::Antialiasing, true); - painter.setRenderHint(QPainter::TextAntialiasing, true); - - 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); - } - } - } - } - - QPainter uiPainter(this); - drawToggleButton_(&uiPainter); + } + QOpenGLWidget::mousePressEvent(event); } -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_toggleRect.contains(event->pos())) { - m_useHeatmap = !m_useHeatmap; - update(); - event->accept(); - return; - } - 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(); -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::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); + 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; + } -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; + 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; -} - -int GLWidget::pickDotIndex_(const QPoint& pos, QVector3D* worldOut) const { - if (dotCount() <= 0) - return -1; + 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::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 - ); + 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; - if (!projectToScreen_(worldCenter, ¢er)) - continue; + 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; + 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 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; - } + 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 >= 0 && worldOut) - *worldOut = bestWorld; - return best; + 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::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, ¢er)) 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; - } + 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, - }; + // 全屏 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); + 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); + 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); + glEnableVertexAttribArray(0); + glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0); - glBindVertexArray(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; - } + // 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; + 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 + 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 + // +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 + // -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 + // -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}, // 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 - }; + // +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 - }; + 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; + m_panelIndexCount = 36; - glGenVertexArrays(1, &m_panelVao); - glBindVertexArray(m_panelVao); + 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_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); + 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); + // 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; - } + // 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}, - }; + 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); + 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))); + // 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); + // 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 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); + // 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); + glBindVertexArray(0); } void GLWidget::initDotTexture_() { - if (m_dotTex) { - glDeleteTextures(1, &m_dotTex); - m_dotTex = 0; - } + 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; - } + 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); + 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; - } + 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; - }; + 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}, + 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}, + // 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}, + // 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}, + // 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}, + // 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; + // 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); + 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_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); + 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))); + 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); + 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; - } + 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; + 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; - }; - V verts[4] = { - {-hw, -hh, z, 0, 0, -1}, - {+hw, -hh, z, 0, 0, -1}, - {+hw, +hh, z, 0, 0, -1}, - {-hw, +hh, z, 0, 0, -1}, - }; - unsigned int idx[6] = {0, 1, 2, 0, 2, 3}; - m_baseIndexCount = 6; + 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); + 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_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); + 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(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); + glBindVertexArray(0); } void GLWidget::initHeatmapGeometry_() { - if (m_heatmapIbo) { - glDeleteBuffers(1, &m_heatmapIbo); - m_heatmapIbo = 0; + 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 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); } - if (m_heatmapVbo) { - glDeleteBuffers(1, &m_heatmapVbo); - m_heatmapVbo = 0; - } - if (m_heatmapVao) { - glDeleteVertexArrays(1, &m_heatmapVao); - m_heatmapVao = 0; + } + + QVector 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()); - const float aspect = (m_panelW > 1e-6f) ? (m_panelH / m_panelW) : 1.0f; - const int cols = qMax(2, qMin(m_cols * m_upscale, 240)); - int rows = qMax(2, int(cols * aspect + 0.5f)); - rows = qMax(2, qMin(rows, 240)); - m_heatmapCols = cols; - m_heatmapRows = rows; + glGenVertexArrays(1, &m_heatmapVao); + glBindVertexArray(m_heatmapVao); - const int vertex_count = cols * rows; - QVector verts; - verts.reserve(vertex_count * 5); + glGenBuffers(1, &m_heatmapVbo); + glBindBuffer(GL_ARRAY_BUFFER, m_heatmapVbo); + glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(float), verts.constData(), + GL_STATIC_DRAW); - 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); - } - } + 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); - QVector 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()); + 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))); - 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); + 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; - } + 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(); + m_skirtUVs.clear(); + m_skirtNormals.clear(); - const int cols = qMax(2, m_heatmapCols); - const int rows = qMax(2, m_heatmapRows); + 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); - }; + 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)); - } + 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 * 6); + const int border_count = m_skirtUVs.size(); + m_skirtVertices.fill(0.0f, border_count * 2 * 8); - QVector 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); + QVector 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()); + 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); + 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_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); + 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, 6 * sizeof(float), (void*)0); - glEnableVertexAttribArray(1); - glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float))); + 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); + 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); + 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); + 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 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; + 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; - } + if (!m_heightDirty && !m_heightValues.isEmpty()) { + return; + } + if (m_rows <= 0 || m_cols <= 0) { + return; + } - QVector valuesCopy; - { - QMutexLocker lk(&m_dataMutex); - valuesCopy = m_latestValues; - } + QVector 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(r, c) = v; - } + 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(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 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; - } + 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); + 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(0), count * sizeof(float)); - } else { - for (int r = 0; r < m_heightH; ++r) { - const float* rowPtr = out.ptr(r); - std::memcpy(m_heightValues.data() + r * m_heightW, rowPtr, m_heightW * sizeof(float)); - } + if (out.isContinuous()) { + const size_t count = size_t(m_heightW) * size_t(m_heightH); + std::memcpy(m_heightValues.data(), out.ptr(0), + count * sizeof(float)); + } else { + for (int r = 0; r < m_heightH; ++r) { + const float* rowPtr = out.ptr(r); + std::memcpy(m_heightValues.data() + r * m_heightW, rowPtr, + m_heightW * sizeof(float)); } - m_heightDirty = false; + } + 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; - } + 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); + 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); + 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 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 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 topIndex = i * 2; + const int botIndex = i * 2 + 1; - const int topBase = topIndex * 6; - 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(); + 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 * 6; - 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(); - } + 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()); + 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) { - 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("uColor", m_colorLow); + 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); - glBindVertexArray(m_baseVao); - glDrawElements(GL_TRIANGLES, m_baseIndexCount, GL_UNSIGNED_INT, nullptr); - glBindVertexArray(0); - m_baseProg->release(); + 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) { - 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("uColor", m_colorLow); + 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); - glBindVertexArray(m_skirtVao); - glDrawElements(GL_TRIANGLES, m_skirtIndexCount, GL_UNSIGNED_INT, nullptr); - glBindVertexArray(0); - m_baseProg->release(); + 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)); - 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("uColorLow", m_colorLow); - m_heatmapProg->setUniformValue("uColorMid", m_colorMid); - m_heatmapProg->setUniformValue("uColorHigh", m_colorHigh); - m_heatmapProg->setUniformValue("uHeightTex", 0); + 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); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, m_heightTex); - glBindVertexArray(m_heatmapVao); - glDrawElements(GL_TRIANGLES, m_heatmapIndexCount, GL_UNSIGNED_INT, nullptr); - glBindVertexArray(0); + glBindVertexArray(m_heatmapVao); + glDrawElements(GL_TRIANGLES, m_heatmapIndexCount, GL_UNSIGNED_INT, nullptr); + glBindVertexArray(0); - glBindTexture(GL_TEXTURE_2D, 0); - m_heatmapProg->release(); + glBindTexture(GL_TEXTURE_2D, 0); + m_heatmapProg->release(); } -void GLWidget::drawToggleButton_(QPainter* painter) { - if (!painter) { - return; - } - const QRectF rect(12.0, 12.0, 160.0, 30.0); - m_toggleRect = rect; - - painter->save(); - painter->setRenderHint(QPainter::Antialiasing, true); - QColor bg = m_useHeatmap ? QColor(0, 150, 136, 220) : QColor(45, 45, 50, 220); - QColor border(255, 255, 255, 80); - painter->setPen(QPen(border, 1.0)); - painter->setBrush(bg); - painter->drawRoundedRect(rect, 6.0, 6.0); - - painter->setPen(QColor(255, 255, 255, 235)); - painter->drawText(rect, Qt::AlignCenter, m_useHeatmap ? QStringLiteral("Heatmap") : QStringLiteral("Dots")); - painter->restore(); -} 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"); + // 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(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; } - { - 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; + 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; } - { - 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; + 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; } - { - 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; + 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; } - { - 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; + 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; } - { - 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; + 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; + if (dotCount() <= 0) { + m_instanceCount = 0; + return; + } + + QVector valuesCopy; + bool dirty = false; + { + QMutexLocker lk(&m_dataMutex); + dirty = m_valuesDirty; + if (dirty) { + valuesCopy = m_latestValues; + m_valuesDirty = false; } + } + if (!dirty) return; - QVector 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 inst; + // 每个点 3 个 float:offsetX, offsetZ, value(对应 dots.vert: iOffsetXZ + + // iValue) + inst.resize(n * 3); - const int n = dotCount(); - m_instanceCount = n; - QVector 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; - 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; - // 把点阵居中到原点附近(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; - 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; + } - 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()); + // 把 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; - } + // 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); + 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); + // 让相机看向 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); + // 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; + 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 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)); + 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; + m_mvp = proj * view; } -void GLWidget::updateRoom_(){ - if (!m_roomProg || !room_vao || room_index_count <= 0) { - return; - } +void GLWidget::updateRoom_() { + if (!m_roomProg || !room_vao || room_index_count <= 0) { + return; + } - m_roomProg->bind(); + 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 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; + 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); + 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(); + 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; + if (m_showGrid == on) return; - m_showGrid = on; - update(); + 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); + 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); + col = qMax(0, col); + if (m_cols == col) return; + setSpec(m_rows, col, m_pitch, m_dotRadius); } diff --git a/src/serial/serial_backend.cpp b/src/serial/serial_backend.cpp index ae23b3d..55a39c9 100644 --- a/src/serial/serial_backend.cpp +++ b/src/serial/serial_backend.cpp @@ -1,9 +1,11 @@ #include "serial_backend.h" -#include "piezoresistive_a_protocol.h" #include "serial_qt_transport.h" #include +#include +#include +#include #include #include #include @@ -14,13 +16,20 @@ SerialBackend::SerialBackend(QObject *parent) : QObject(parent), m_packetQueue(2048), m_frameQueue(2048), m_readThread(&m_packetQueue), m_decodeThread(&m_packetQueue, &m_frameQueue) { m_request.dataLength = 24; - m_spec.model = QStringLiteral("PZR-A"); - auto codec = std::make_shared(); - auto decoder = std::make_shared(); - auto format = std::make_shared(); - m_manager.registerProtocol(codec->name(), {codec, decoder, format}); - m_manager.setActiveProtocol(codec->name()); + const QString pluginDir = QDir(QCoreApplication::applicationDirPath()) + .filePath(QStringLiteral("plugins/decoders")); + QStringList errors; + const QStringList loaded = m_manager.loadPlugins(pluginDir, &errors); + for (const QString& err : errors) { + qWarning().noquote() << err; + } + if (!loaded.isEmpty()) { + m_spec.model = m_manager.activeProtocol(); + } else { + qWarning() << "No protocol plugins loaded."; + m_spec.model = QStringLiteral("UNKNOWN"); + } m_sendWorker = new SerialSendWorker(); m_sendWorker->moveToThread(&m_sendThread); @@ -175,6 +184,10 @@ void SerialBackend::setProtocol(const QString &name) if (!m_manager.setActiveProtocol(name)) return; updateProtocolBindings_(); + if (m_spec.model != name) { + m_spec.model = name; + emit sensorModelChanged(); + } emit protocolChanged(); } diff --git a/src/serial/serial_manager.cpp b/src/serial/serial_manager.cpp index 53ea384..738895e 100644 --- a/src/serial/serial_manager.cpp +++ b/src/serial/serial_manager.cpp @@ -1,4 +1,8 @@ #include "serial_manager.h" +#include "protocol_plugin.h" + +#include +#include void SerialManager::registerProtocol(const QString& name, const ProtocolBundle& bundle) { if (name.isEmpty()) @@ -15,8 +19,76 @@ bool SerialManager::setActiveProtocol(const QString& name) { return true; } -SerialManager::ProtocolBundle SerialManager::activeBundle() const { +ProtocolBundle SerialManager::activeBundle() const { if (!m_protocols.contains(m_activeName)) return {}; return m_protocols.value(m_activeName); } + +QStringList SerialManager::loadPlugins(const QString& dirPath, QStringList* errors) { + QStringList loaded; + if (!m_pluginLoaders.empty()) { + return loaded; + } + + QDir dir(dirPath); + if (!dir.exists()) { + if (errors) + errors->append(QStringLiteral("Plugin dir not found: %1").arg(dirPath)); + return loaded; + } + + QStringList filters; +#if defined(Q_OS_WIN) + filters << QStringLiteral("*.dll"); +#elif defined(Q_OS_MAC) + filters << QStringLiteral("*.dylib"); +#else + filters << QStringLiteral("*.so"); +#endif + + const QStringList entries = dir.entryList(filters, QDir::Files); + for (const QString& fileName : entries) { + const QString path = dir.absoluteFilePath(fileName); + auto loader = std::make_unique(path); + QObject* instance = loader->instance(); + if (!instance) { + if (errors) + errors->append(QStringLiteral("Load failed: %1 (%2)").arg(path, loader->errorString())); + continue; + } + + auto* plugin = qobject_cast(instance); + if (!plugin) { + if (errors) + errors->append(QStringLiteral("Invalid plugin: %1").arg(path)); + loader->unload(); + continue; + } + if (plugin->apiVersion() != kProtocolPluginApiVersion) { + if (errors) + errors->append(QStringLiteral("API mismatch: %1").arg(path)); + loader->unload(); + continue; + } + + const ProtocolBundle bundle = plugin->createBundle(); + if (!bundle.codec || !bundle.decoder || !bundle.format) { + if (errors) + errors->append(QStringLiteral("Missing components: %1").arg(path)); + loader->unload(); + continue; + } + + const QString name = plugin->protocolName(); + if (!m_protocols.contains(name)) { + registerProtocol(name, bundle); + loaded.append(name); + m_pluginLoaders.push_back(std::move(loader)); + } else { + loader->unload(); + } + } + + return loaded; +} diff --git a/src/serial/serial_manager.h b/src/serial/serial_manager.h index e8a8f8c..2d70d7a 100644 --- a/src/serial/serial_manager.h +++ b/src/serial/serial_manager.h @@ -3,22 +3,18 @@ #include #include +#include +#include +#include #include -#include "serial_codec.h" -#include "serial_decoder.h" -#include "serial_format.h" +#include "protocol_bundle.h" class SerialManager { public: - struct ProtocolBundle { - std::shared_ptr codec; - std::shared_ptr decoder; - std::shared_ptr format; - }; - void registerProtocol(const QString& name, const ProtocolBundle& bundle); bool setActiveProtocol(const QString& name); + QStringList loadPlugins(const QString& dirPath, QStringList* errors = nullptr); QString activeProtocol() const { return m_activeName; } ProtocolBundle activeBundle() const; @@ -26,6 +22,7 @@ public: private: QHash m_protocols; QString m_activeName; + std::vector> m_pluginLoaders; }; #endif // TACTILEIPC3D_SERIAL_MANAGER_H