From 354552dc887259b625b11ea070c5edf26f55e346 Mon Sep 17 00:00:00 2001 From: lenn Date: Thu, 15 Jan 2026 16:13:36 +0800 Subject: [PATCH] =?UTF-8?q?=E9=A2=9C=E8=89=B2=E6=98=A0=E5=B0=84=E5=9B=BE?= =?UTF-8?q?=E4=BE=8B=EF=BC=8C=E8=A7=84=E6=A0=BC=E5=B0=BA=E5=AF=B8=E4=BF=AE?= =?UTF-8?q?=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 5 + docs/ARCHITECTURE.md | 46 ++++- i18n/app_en_US.ts | 79 +++++++ i18n/app_zh_CN.ts | 4 + main.cpp | 91 ++++++--- qml/content/CollapsiblePanel.qml | 2 +- qml/content/LeftPanel.qml | 209 ++++++++++++++++++- qml/content/Legend.qml | 21 +- qml/content/NavBar.qml | 11 +- qml/content/OpenFileDialog.qml | 291 ++++++++++++++++++++++++++ qml/content/RightPanel.qml | 340 +++++++++++++++++-------------- shaders/dots.frag | 18 +- src/backend.cpp | 67 +++++- src/backend.h | 37 ++++ src/data_backend.cpp | 102 +++++++++- src/data_backend.h | 6 +- src/glwidget.cpp | 56 ++++- src/glwidget.h | 15 +- src/serial/serial_backend.cpp | 12 +- src/serial/serial_backend.h | 7 + src/serial/serial_types.h | 4 +- 21 files changed, 1200 insertions(+), 223 deletions(-) create mode 100644 qml/content/OpenFileDialog.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index b4b65d8..f2795f0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -9,6 +9,8 @@ set(CMAKE_AUTOUIC ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) +add_subdirectory(3rdpart/QXlsx/QXlsx) + include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src) find_package(Qt6 COMPONENTS @@ -22,6 +24,7 @@ find_package(Qt6 COMPONENTS Quick QuickControls2 QuickLayouts + QuickDialogs2 LinguistTools ) @@ -70,6 +73,8 @@ target_link_libraries(TactileIpc3D Qt6::Quick Qt6::QuickControls2 Qt6::QuickLayouts + Qt6::QuickDialogs2 + QXlsx::QXlsx ) set(TS_FILES diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d455ec3..a7f6dc8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -10,8 +10,18 @@ classDiagram +bool connected +SerialBackend* serial +DataBackend* data + +int rangeMin + +int rangeMax + +QColor colorLow + +QColor colorMid + +QColor colorHigh +setLightMode(bool) +setLanguage(string) + +setRangeMin(int) + +setRangeMax(int) + +setColorLow(QColor) + +setColorMid(QColor) + +setColorHigh(QColor) } class SerialConfig { @@ -60,6 +70,10 @@ classDiagram +setTransport(transport) } class GLWidget { + +setRange(int, int) + +setColorLow(QColor) + +setColorMid(QColor) + +setColorHigh(QColor) +dotClicked(index, row, col, value) } @@ -160,6 +174,7 @@ classDiagram AppBackend --> SerialBackend AppBackend --> DataBackend + AppBackend ..> GLWidget : render config SerialBackend --> SerialConfig SerialBackend --> SensorRequest SerialBackend --> SensorSpec @@ -248,6 +263,15 @@ flowchart TD SB -->|updateProtocolBindings_| SW3[SerialSendWorker.setBuildRequestFunc] ``` +## 渲染/颜色映射流程 (Mermaid) + +```mermaid +flowchart LR + UI[LeftPanel 颜色映射] -->|rangeMin/rangeMax
colorLow/Mid/High| AB[AppBackend] + AB -->|rangeChanged/colorChanged| GL[GLWidget] + GL -->|uMinV/uMaxV/uColorLow/Mid/High| SH[dots.frag] +``` + ## 配置接口与步骤说明 - 设置协议: @@ -272,6 +296,11 @@ flowchart TD - `SerialSendWorker::setBuildRequestFunc(codec->buildRequest)` - 打开串口: - `SerialBackend::open()` -> `SerialSendWorker::openTransport(config)`,成功后在从站模式启动轮询发送。 +- 渲染/颜色映射配置: + - QML 绑定 `AppBackend::rangeMin/rangeMax` 与 `colorLow/Mid/High`(`LeftPanel` 中的颜色映射面板)。 + - `setRangeMin/Max` 发出 `rangeChanged(min, max)`,由 `GLWidget::setRange` 同步到 `uMinV/uMaxV`。 + - `setColorLow/Mid/High` 发出 `color*Changed`,由 `GLWidget::setColor*` 同步到 `uColorLow/Mid/High`。 + - `dots.frag` 归一化公式:`value01 = clamp((v - min) / (max - min))`,并用 low->mid->high 线性插值。 ## 分层设计概述 @@ -279,6 +308,7 @@ flowchart TD - 串口采集层:`Transport + Format + Codec + Decoder + Manager` 分层,独立于业务逻辑。 - 串口线程化:读取/解码/发送三线程 + Packet/Frame 队列,降低 UI 卡顿风险。 - 数据驱动层:负责帧缓存、数据导入导出与回放,提供渲染回调。 +- 渲染配置层:`AppBackend` 提供范围/颜色参数,`GLWidget` 将其转为 shader uniform 进行颜色映射。 - UI 层:`NavBar + LeftPanel + OpenGL View + RightPanel` 的 1+3 布局。 ## 类接口与成员说明(C++) @@ -289,11 +319,16 @@ flowchart TD - `lightMode` / `language` / `connected`:UI 基础状态。 - `serial()` / `data()`:暴露子系统实例给 QML。 - `setLightMode(bool)` / `setLanguage(string)`。 + - `rangeMin` / `rangeMax`:颜色映射的数值范围。 + - `colorLow` / `colorMid` / `colorHigh`:颜色映射的三个基准颜色。 + - `setRangeMin(int)` / `setRangeMax(int)` / `setColorLow(QColor)` / `setColorMid(QColor)` / `setColorHigh(QColor)`。 - 成员变量: - `m_serial`:串口采集层对象。 - `m_data`:数据驱动层对象。 - `m_lightMode` / `m_language`:全局 UI 状态。 + - `m_rangeMin` / `m_rangeMax` / `m_colorLow` / `m_colorMid` / `m_colorHigh`:颜色映射参数。 - 备注:串口连接成功后会清空历史数据缓存,避免旧数据残留。 + - `rangeChanged(min, max)` 用于驱动 `GLWidget::setRange`,颜色变更通过 `color*Changed` 同步。 ### SerialBackend (`src/serial/serial_backend.h`) - 作用:串口采集层的统一控制器,负责协议选择、三线程调度与数据分发。 @@ -395,9 +430,14 @@ flowchart TD ### GLWidget (`src/glwidget.h`) - 作用:OpenGL 渲染窗口,显示传感器点阵与背景。 +- 接口: + - `setRange(int minV, int maxV)`:设置 `uMinV/uMaxV`。 + - `setColorLow(QColor)` / `setColorMid(QColor)` / `setColorHigh(QColor)`:设置 `uColorLow/uColorMid/uColorHigh`。 - 信号: - `dotClicked(index, row, col, value)`:鼠标点击某个点时发出索引与数据值。 -- 备注:拾取使用屏幕投影 + 半径阈值,便于 RightPanel 订阅点击事件做曲线展示。 +- 备注: + - 颜色映射在 `dots.frag` 内完成,低/中/高三段线性插值。 + - 拾取使用屏幕投影 + 半径阈值,便于 RightPanel 订阅点击事件做曲线展示。 ### DataFrame (`src/data_frame.h`) - 字段: @@ -440,6 +480,9 @@ flowchart TD - 采样周期(从站模式可用)。 - 采样参数:功能码、起始地址、读取长度。 - 传感器规格:协议名、型号、网格规格占位。 +- 颜色映射: + - 数值范围(min/max)。 + - 低/中/高三色选择(`ColorDialog`)。 - 显示控制:显示网络/坐标轴、回放与导出入口。 ### RightPanel(`qml/content/RightPanel.qml`) @@ -518,6 +561,7 @@ flowchart TD ## 更新记录 - 2026-01-11:新增 `QtSerialTransport`(`QSerialPort` 传输实现)并设为默认传输;补齐点选拾取逻辑;新增数据流/时序图并补充可视化 TODO 说明。 +- 2026-01-12:`LeftPanel` 新增颜色映射参数;`AppBackend`/`GLWidget` 增加颜色与范围接口,shader 使用三色渐变映射数据值。 - 2026-01-11:补充串口配置流程图与配置接口说明(协议/参数/解码器绑定/打开流程)。 - 2026-01-05:新增串口三线程流水线(读/解码/发送)与 Packet/Frame 队列,更新协议起始符说明,补充队列溢出 TODO 与线程组件文档。 - 2026-01-05:CollapsiblePanel 组件改为跟随 `backend.lightMode` 切换暗色主题配色。 diff --git a/i18n/app_en_US.ts b/i18n/app_en_US.ts index 9e35756..958f138 100644 --- a/i18n/app_en_US.ts +++ b/i18n/app_en_US.ts @@ -96,6 +96,14 @@ 采样周期 Sample Interval + + 宽 + Width + + + 高 + Height + 连接 Connect @@ -140,6 +148,34 @@ 重新识别 Rescan + + 颜色映射 + Color Mapping + + + 最小值 + Min Value + + + 最大值 + Max Value + + + 低色 + Low Color + + + 选择 + Select + + + 中色 + Mid Color + + + 高色 + High Color + 显示控制 Display @@ -160,6 +196,45 @@ 导出数据 Export Data + + 选择低色 + Select Low Color + + + 选择中色 + Select Mid Color + + + 选择高色 + Select High Color + + + + OpenFileDialog + + 导入数据 + Import Data + + + 位置 + Locations + + + 此电脑 + This PC + + + 桌面 + Desktop + + + 文档 + Documents + + + 下载 + Downloads + RightPanel @@ -171,6 +246,10 @@ Live Trend Live Trend + + Legend + Legend + Metrics Metrics diff --git a/i18n/app_zh_CN.ts b/i18n/app_zh_CN.ts index e92fb0a..d783a0a 100644 --- a/i18n/app_zh_CN.ts +++ b/i18n/app_zh_CN.ts @@ -171,6 +171,10 @@ Live Trend 实时趋势 + + Legend + 图例 + Metrics 指标 diff --git a/main.cpp b/main.cpp index 20538d5..7349f3b 100644 --- a/main.cpp +++ b/main.cpp @@ -44,7 +44,8 @@ int main(int argc, char *argv[]) { QOpenGLContext probeCtx; probeCtx.setFormat(QSurfaceFormat::defaultFormat()); if (!probeCtx.create()) { - qCritical().noquote() << "Failed to create an OpenGL context (required: OpenGL 3.3 Core)."; + qCritical().noquote() << "Failed to create an OpenGL context " + "(required: OpenGL 3.3 Core)."; return 1; } @@ -54,17 +55,22 @@ int main(int argc, char *argv[]) { if (!probeCtx.makeCurrent(&probeSurface)) { qCritical().noquote() - << "Failed to make the OpenGL context current. This usually means the requested format is unsupported by the current graphics driver."; + << "Failed to make the OpenGL context current. This usually " + "means the requested format is unsupported by the current " + "graphics driver."; return 1; } const QSurfaceFormat actual = probeCtx.format(); - const bool versionOk = (actual.majorVersion() > 3) || (actual.majorVersion() == 3 && actual.minorVersion() >= 3); + const bool versionOk = + (actual.majorVersion() > 3) || + (actual.majorVersion() == 3 && actual.minorVersion() >= 3); if (!versionOk || actual.profile() != QSurfaceFormat::CoreProfile) { probeCtx.doneCurrent(); qCritical().noquote() << "OpenGL context is not OpenGL 3.3 Core (got: " - << actual.majorVersion() << "." << actual.minorVersion() << ", profile=" << actual.profile() << ")."; + << actual.majorVersion() << "." << actual.minorVersion() + << ", profile=" << actual.profile() << ")."; return 1; } @@ -84,12 +90,12 @@ int main(int argc, char *argv[]) { qmlRegisterSingletonInstance("TactileIPC", 1, 0, "I18n", &i18n); qmlRegisterType("LiveTrend", 1, 0, "SparklinePlot"); i18n.setLanguage(backend.language()); - QObject::connect(&backend, &AppBackend::languageChanged, &i18n, [&backend, &i18n]() { - i18n.setLanguage(backend.language()); - }); + QObject::connect( + &backend, &AppBackend::languageChanged, &i18n, + [&backend, &i18n]() { i18n.setLanguage(backend.language()); }); auto *qmlEngine = new QQmlEngine(root); - auto createQuickWidget = [&](const QUrl& sourceUrl) -> QQuickWidget* { + auto createQuickWidget = [&](const QUrl &sourceUrl) -> QQuickWidget * { auto *view = new QQuickWidget(qmlEngine, root); view->setResizeMode(QQuickWidget::SizeRootObjectToView); view->setSource(sourceUrl); @@ -107,26 +113,30 @@ int main(int argc, char *argv[]) { leftView->setFixedWidth(350); auto *glw = new GLWidget; - glw->setSpec(8, 11, 0.1f, 0.03f); + glw->setSpec(12, 7, 0.1f, 0.03f); glw->setPanelThickness(0.08f); - glw->setRange(0, 1000); + glw->setRange(backend.rangeMin(), backend.rangeMax()); + glw->setColorLow(backend.colorLow()); + glw->setColorMid(backend.colorMid()); + glw->setColorHigh(backend.colorHigh()); /* backend.data()->setLiveRenderCallback([glw](const DataFrame& frame) { if (frame.data.size() != glw->dotCount()) return; glw->submitValues(frame.data); }); */ - backend.data()->setLiveRenderCallback([](const DataFrame& frame) { + backend.data()->setLiveRenderCallback([](const DataFrame &frame) { if (frame.data.size() != 0) { - // AA 55 1A 00 34 00 FB 00 1C 00 00 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 F1 - // aa 55 1a 00 34 00 fb 00 1c 00 00 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f1 + // AA 55 1A 00 34 00 FB 00 1C 00 00 10 00 00 00 00 00 00 00 00 00 00 + // 00 00 00 00 00 00 00 00 F1 aa 55 1a 00 34 00 fb 00 1c 00 00 10 00 + // 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f1 qDebug() << "data size: " << frame.data.size(); - } }); // TODO:待实现内容(将frame数据分发给右侧曲线/指标的QML接口) - auto *rightView = createQuickWidget(QUrl("qrc:/qml/content/RightPanel.qml")); + auto *rightView = + createQuickWidget(QUrl("qrc:/qml/content/RightPanel.qml")); splitter->addWidget(leftView); splitter->addWidget(glw); @@ -136,33 +146,52 @@ int main(int argc, char *argv[]) { splitter->setStretchFactor(2, 0); splitter->setSizes({320, 640, 320}); auto applySplitterStyle = [&backend, splitter]() { - const QString handleColor = backend.lightMode() ? QStringLiteral("#E0E0E0") : QStringLiteral("#2C2C2C"); - splitter->setStyleSheet(QStringLiteral("QSplitter::handle { background: %1; }").arg(handleColor)); + const QString handleColor = backend.lightMode() + ? QStringLiteral("#E0E0E0") + : QStringLiteral("#2C2C2C"); + splitter->setStyleSheet( + QStringLiteral("QSplitter::handle { background: %1; }") + .arg(handleColor)); }; applySplitterStyle(); - QObject::connect(&backend, &AppBackend::lightModeChanged, splitter, [applySplitterStyle]() { - applySplitterStyle(); - }); + QObject::connect(&backend, &AppBackend::lightModeChanged, splitter, + [applySplitterStyle]() { applySplitterStyle(); }); auto applyQuickTheme = [&backend, navView, leftView, rightView]() { - const QColor navColor = backend.lightMode() ? QColor(QStringLiteral("#F5F7F5")) : QColor(QStringLiteral("#2B2F2B")); - const QColor panelColor = backend.lightMode() ? QColor(QStringLiteral("#F5F5F5")) : QColor(QStringLiteral("#2C2C2C")); + const QColor navColor = backend.lightMode() + ? QColor(QStringLiteral("#F5F7F5")) + : QColor(QStringLiteral("#2B2F2B")); + const QColor panelColor = backend.lightMode() + ? QColor(QStringLiteral("#F5F5F5")) + : QColor(QStringLiteral("#2C2C2C")); navView->setClearColor(navColor); leftView->setClearColor(panelColor); rightView->setClearColor(panelColor); }; applyQuickTheme(); - QObject::connect(&backend, &AppBackend::lightModeChanged, navView, [applyQuickTheme]() { - applyQuickTheme(); - }); + QObject::connect(&backend, &AppBackend::lightModeChanged, navView, + [applyQuickTheme]() { applyQuickTheme(); }); + QObject::connect(&backend, &AppBackend::lightModeChanged, glw, + [&backend, glw]() { + bool m = backend.lightMode() ? true : false; + glw->setLightMode(m); + }); - QObject::connect(&backend, &AppBackend::lightModeChanged, glw, [&backend, glw]() { - bool m = backend.lightMode() ? true : false; - glw->setLightMode(m); - }); - - QObject::connect(&backend, &AppBackend::showGridChanged, glw, &GLWidget::setShowGrid); + QObject::connect(&backend, &AppBackend::showGridChanged, glw, + &GLWidget::setShowGrid); + 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::colorLowChanged, glw, + &GLWidget::setColorLow); + QObject::connect(&backend, &AppBackend::colorMidChanged, glw, + &GLWidget::setColorMid); + QObject::connect(&backend, &AppBackend::colorHighChanged, glw, + &GLWidget::setColorHigh); rootLayout->addWidget(navView); rootLayout->addWidget(splitter); diff --git a/qml/content/CollapsiblePanel.qml b/qml/content/CollapsiblePanel.qml index cc826bd..85dcc5a 100644 --- a/qml/content/CollapsiblePanel.qml +++ b/qml/content/CollapsiblePanel.qml @@ -6,7 +6,7 @@ import TactileIPC 1.0 Item { id: root - width: 350 + implicitWidth: 350 property alias title: titleText.text property bool expanded: true diff --git a/qml/content/LeftPanel.qml b/qml/content/LeftPanel.qml index ffef415..38e6f14 100644 --- a/qml/content/LeftPanel.qml +++ b/qml/content/LeftPanel.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls.Material import QtQuick.Controls import QtQuick.Layouts +import QtQuick.Dialogs import "." import TactileIPC 1.0 @@ -208,6 +209,42 @@ Rectangle { } } + RowLayout { + Layout.fillWidth: true + spacing: 8 + Label { + text: root.tr("宽") + Layout.preferredWidth: 90 + color: root.textColor + } + SpinBox { + Layout.fillWidth: true + from: 1 + to: 20 + value: Backend.sensorCol + enabled: Backend.serial.connected === false + onValueModified: Backend.sensorCol = value + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Label { + text: root.tr("高") + Layout.preferredWidth: 90 + color: root.textColor + } + SpinBox { + Layout.fillWidth: true + from: 1 + to: 20 + value: Backend.sensorRow + enabled: Backend.serial.connected === false + onValueModified: Backend.sensorRow = value + } + } + RowLayout { Layout.fillWidth: true spacing: 12 @@ -372,6 +409,150 @@ Rectangle { } } + CollapsiblePanel { + title: root.tr("颜色映射") + expanded: true + Layout.fillWidth: true + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Label { + text: root.tr("最小值") + Layout.preferredWidth: 90 + color: root.textColor + } + SpinBox { + Layout.fillWidth: true + from: -999999 + to: 999999 + editable: true + value: Backend.rangeMin + onValueModified: Backend.rangeMin = value + } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Label { + text: root.tr("最大值") + Layout.preferredWidth: 90 + color: root.textColor + } + SpinBox { + Layout.fillWidth: true + from: -999999 + to: 999999 + editable: true + value: Backend.rangeMax + onValueModified: Backend.rangeMax = value + } + } + + 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.colorLow + border.width: 1 + border.color: Qt.rgba(0, 0, 0, 0.2) + Layout.alignment: Qt.AlignVCenter + MouseArea { + anchors.fill: parent + onClicked: { + lowColorDialog.selectedColor = Backend.colorLow + lowColorDialog.open() + } + } + } + Button { + text: root.tr("选择") + Layout.fillWidth: true + onClicked: { + lowColorDialog.selectedColor = Backend.colorLow + lowColorDialog.open() + } + } + } + + 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.colorMid + border.width: 1 + border.color: Qt.rgba(0, 0, 0, 0.2) + Layout.alignment: Qt.AlignVCenter + MouseArea { + anchors.fill: parent + onClicked: { + midColorDialog.selectedColor = Backend.colorMid + midColorDialog.open() + } + } + } + Button { + text: root.tr("选择") + Layout.fillWidth: true + onClicked: { + midColorDialog.selectedColor = Backend.colorMid + midColorDialog.open() + } + } + } + + 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.colorHigh + border.width: 1 + border.color: Qt.rgba(0, 0, 0, 0.2) + Layout.alignment: Qt.AlignVCenter + MouseArea { + anchors.fill: parent + onClicked: { + highColorDialog.selectedColor = Backend.colorHigh + highColorDialog.open() + } + } + } + Button { + text: root.tr("选择") + Layout.fillWidth: true + onClicked: { + highColorDialog.selectedColor = Backend.colorHigh + highColorDialog.open() + } + } + } + } + CollapsiblePanel { title: root.tr("显示控制") expanded: true @@ -399,7 +580,14 @@ Rectangle { Button { Layout.fillWidth: true text: root.tr("导出数据") - onClicked: exportDlg.open() + onClicked: { + if (Backend.data.frameCount != 0) { + exportDlg.open() + } + else { + console.log("Backend.data.frameCount() === 0") + } + } } } } @@ -407,6 +595,25 @@ Rectangle { Item { Layout.fillHeight: true } } } + + ColorDialog { + id: lowColorDialog + title: root.tr("选择低色") + onAccepted: Backend.colorLow = selectedColor + } + + ColorDialog { + id: midColorDialog + title: root.tr("选择中色") + onAccepted: Backend.colorMid = selectedColor + } + + ColorDialog { + id: highColorDialog + title: root.tr("选择高色") + onAccepted: Backend.colorHigh = selectedColor + } + SaveAsExportDialog { id: exportDlg /* onSaveTo: (folder, filename, format, method) => { diff --git a/qml/content/Legend.qml b/qml/content/Legend.qml index 7110952..53acc3d 100644 --- a/qml/content/Legend.qml +++ b/qml/content/Legend.qml @@ -7,9 +7,14 @@ Item { id: root property int minValue: 0 property int maxValue: 100 + property color colorLow: Qt.rgba(0.10, 0.75, 1.00, 1.0) + property color colorMid: Qt.rgba(0.10, 0.95, 0.35, 1.0) + property color colorHigh: Qt.rgba(1.00, 0.22, 0.10, 1.0) + property int barWidth: 34 + property int barRadius: 8 - implicitWidth: 90 - implicitHeight: 220 + implicitWidth: barWidth + 48 + implicitHeight: 240 ColumnLayout { anchors.fill: parent @@ -25,17 +30,16 @@ Item { Rectangle { Layout.alignment: Qt.AlignHCenter Layout.fillHeight: true - width: 26 - radius: 6 + width: root.barWidth + radius: root.barRadius border.width: 1 border.color: Qt.rgba(1, 1, 1, 0.18) gradient: Gradient { // must match shaders/dots.frag:dataColorRamp (high at top) - GradientStop { position: 0.00; color: Qt.rgba(1.00, 0.22, 0.10, 1.0) } // c3 - GradientStop { position: 0.34; color: Qt.rgba(1.00, 0.92, 0.22, 1.0) } // c2 - GradientStop { position: 0.67; color: Qt.rgba(0.10, 0.95, 0.35, 1.0) } // c1 - GradientStop { position: 1.00; color: Qt.rgba(0.10, 0.75, 1.00, 1.0) } // c0 + GradientStop { position: 0.00; color: root.colorHigh } + GradientStop { position: 0.50; color: root.colorMid } + GradientStop { position: 1.00; color: root.colorLow } } } @@ -47,4 +51,3 @@ Item { } } } - diff --git a/qml/content/NavBar.qml b/qml/content/NavBar.qml index 99d2f30..e3444cb 100644 --- a/qml/content/NavBar.qml +++ b/qml/content/NavBar.qml @@ -92,8 +92,8 @@ Rectangle { anchors.left: parent.left anchors.leftMargin: 8 Image { - width: 18 - height: 12 + width: 16 + height: 16 fillMode: Image.PreserveAspectFit source: modelData.icon } @@ -107,10 +107,13 @@ Rectangle { anchors.fill: parent Row { spacing: 8 + anchors.left: parent.left + anchors.leftMargin: 10 + anchors.rightMargin: 24 anchors.verticalCenter: parent.verticalCenter Image { - width: 18 - height: 12 + width: 16 + height: 16 fillMode: Image.PreserveAspectFit source: langBox.model[langBox.currentIndex] ? langBox.model[langBox.currentIndex].icon diff --git a/qml/content/OpenFileDialog.qml b/qml/content/OpenFileDialog.qml new file mode 100644 index 0000000..3707d53 --- /dev/null +++ b/qml/content/OpenFileDialog.qml @@ -0,0 +1,291 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Controls.Material 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Window 2.15 +import Qt.labs.folderlistmodel 2.15 +import QtCore 6.2 +import QtQuick.Dialogs +import TactileIPC 1.0 + +Window { + id: root + width: 980 + height: 640 + minimumWidth: 880 + minimumHeight: 560 + visible: false + modality: Qt.ApplicationModal + flags: Qt.Dialog | Qt.WindowTitleHint | Qt.WindowCloseButtonHint + title: root.tr("导入数据") + color: windowBg + + readonly property bool isDark: !Backend.lightMode + readonly property color windowBg: isDark ? "#1B1F1B" : "#F7F8F9" + Material.accent: root.accent + Material.primary: root.accent + Material.theme: root.isDark ? Material.Dark : Material.Light + + readonly property color accent: "#21A453" + readonly property color accentSoft: root.isDark ? "#1F3A2A" : "#E6F6EC" + readonly property color panel: root.isDark ? "#242924" : "#FFFFFF" + readonly property color border: root.isDark ? "#343A35" : "#E1E5EA" + readonly property color text: root.isDark ? "#E6ECE7" : "#1E2A32" + readonly property color subText: root.isDark ? "#9AA5A0" : "#6E7A86" + readonly property color fieldBg: root.isDark ? "#1E221E" : "#FFFFFF" + readonly property color surfaceAlt: root.isDark ? "#202520" : "#F9FAFB" + readonly property color hoverBg: root.isDark ? "#2C322D" : "#F3F6F8" + readonly property color iconBg: root.isDark ? "#25362B" : "#E8F3EA" + readonly property color iconBgAlt: root.isDark ? "#2A302A" : "#EFF2F5" + readonly property color disabledBg: root.isDark ? "#4B544E" : "#C9D2D8" + readonly property string uiFont: "Microsoft YaHei UI" + + property url currentFolder: StandardPaths.writableLocation(StandardPaths.DocumentsLocation) + "/" + property string chosenFilename: "" + property string importFormat: "" + property string importMethod: "" + + signal importIn(url filename, string format, string method) + + function open() { + + } + + function accept() { + visible = false + } + + function reject() { + visible = false + } + + function centerOnScreen_() { + x = Math.round((Screen.width - width) / 2) + y = Math.round((Screen.height - height) / 2) + } + + function normalizeFolder_(path) { + if (!path) + return path + if (path.endsWith("/")) + return path + return path + "/" + } + + + function tr(text) { + I18n.retranslateToken + return qsTr(text) + } + + onVisibleChanged: if (visible) centerOnScreen_() + + ColumnLayout { + anchors.fill: parent + anchors.margins: 16 + spacing: 12 + + Rectangle { + Layout.fillWidth: true + height: 54 + radius: 6 + color: root.panel + border.color: root.border + + RowLayout { + anchors.fill: parent + anchors.margins: 8 + spacing: 8 + + ToolButton { + id: backBtn + text: "<" + font.family: root.uiFont + onClicked: { + + } + background: Rectangle { + radius: 4 + color: backBtn.hovered ? root.accentSoft : "transparent" + border.color: backBtn.hovered ? root.accent : root.border + } + } + ToolButton { + id: forwardBtn + text: ">" + font.family: root.uiFont + onClicked: { + + } + background: Rectangle { + radius: 4 + color: backBtn.hovered ? root.accentSoft : "transparent" + border.color: backBtn.hovered ? root.accent : root.border + } + } + ToolButton { + id: upBtn + text: "^" + font.family: root.uiFont + onClicked: { + + } + background: Rectangle { + radius: 4 + color: backBtn.hovered ? root.accentSoft : "transparent" + border.color: backBtn.hovered ? root.accent : root.border + } + } + TextField { + id: breadcrumb + Layout.fillWidth: true + readOnly: true + font.family: root.uiFont + color: root.text + text: root.currentFolder.toString() + background: Rectangle { + radius: 4 + color: root.surfaceAlt + border.color: root.border + } + } + + } + } + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: 12 + + Rectangle { + Layout.preferredWidth: 220 + Layout.fillHeight: true + radius: 6 + color: root.panel + border.color: root.border + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 8 + + Label { + text: root.tr("位置") + font.bold: true + font.family: root.uiFont + color: root.text + } + ListView { + id: places + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: [ + { name: root.tr("此电脑"), url: "file:///", icon: root.isDark ? "qrc:/images/computer_dark.png" : "qrc:/images/computer_light.png" }, + { name: root.tr("桌面"), url: StandardPaths.writableLocation(StandardPaths.DesktopLocation) + "/", icon: root.isDark ? "qrc:/images/desktop_dark.png" : "qrc:/images/desktop_light.png" }, + { name: root.tr("文档"), url: StandardPaths.writableLocation(StandardPaths.DocumentsLocation) + "/", icon: root.isDark ? "qrc:/images/docs_dark.png" : "qrc:/images/docs_light.png" }, + { name: root.tr("下载"), url: StandardPaths.writableLocation(StandardPaths.DownloadLocation) + "/", icon: root.isDark ? "qrc:/images/download_dark.png" : "qrc:/images/download_light.png" } + ] + + delegate: ItemDelegate { + width: ListView.view.width + onClicked: { + places.currentIndex = index + root.currentFolder = normalizeFolder_(modeData.url) + } + background: Rectangle { + radius: 4 + color: places.currentIndex === index ? root.accentSoft : "transparent" + border.color: places.currentIndex === index ? root.accent : "transparent" + } + + contentItem: RowLayout { + spacing: 8 + Image { + width: 16 + height: 16 + source: modelData.icon + fillMode: Image.PreserveAspectFit + smooth: true + } + Label { + text: modelData.name + font.family: root.uiFont + color: root.text + } + } + } + } + } + } + Rectangle { + Layout.fillWidth: true + Layout.fillHeight: true + radius: 6 + color: root.panel + border.color: root.border + + ColumnLayout { + anchors.fill: parent + anchors.margins: 10 + spacing: 6 + + RowLayout { + Layout.fillWidth: true + Lable { + // TODO table title + } + } + Rectangle { + Layout.fillWidth: true + height: 1 + color: root.border + } + FolderListModel { + id: fileModel + folder: root.currentFolder + showDotAndDotDot: false + showDirs: true + showFiles: true + sortField: FolderListModel.Name + } + ListView { + id: fileList + Layout.fillWidth: true + Layout.fillHeight: true + clip: true + model: fileModel + + delegate: ItemDelegate { + id: fileRow + width: ListView.view.width + onDoubleClicked: { + const isDir = fileModel.get(index, "fileIsDir") + if (isDir) { + root.currentFolder = normalizeFolder_(fileModel.get(index, "filePath")) + } + else { + // TODO import file + } + } + onClicked: { + fileList.currentIndex = index + } + + background: Rectangle { + radius: 4 + color: fileRow.hovered ? root.hoverBg : "transparent" + } + contentItem: RowLayout { + spacing: 8 + Rectangle { + width+: + } + } + } + } + } + } + } + } +} diff --git a/qml/content/RightPanel.qml b/qml/content/RightPanel.qml index 6c32a6c..e6b1060 100644 --- a/qml/content/RightPanel.qml +++ b/qml/content/RightPanel.qml @@ -22,177 +22,205 @@ Rectangle { Material.accent: Material.Green Material.primary: Material.Green - ColumnLayout { + ScrollView { + id: scrollView anchors.fill: parent - anchors.margins: 12 - spacing: 12 + ScrollBar.vertical.policy: ScrollBar.AlwaysOff + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + contentWidth: scrollView.availableWidth + contentHeight: contentLayout.implicitHeight + 24 - LiveTrendCard { - id: card - Layout.fillWidth: true - Layout.preferredHeight: 180 - title: root.tr("Payload Sum") + ColumnLayout { + id: contentLayout + x: 12 + y: 12 + width: scrollView.availableWidth - 24 + spacing: 12 - Connections { - target: Backend.data - function onMetricsChanged() { - if (Backend.data.frameCount > 0) - card.plot.append(Backend.data.metricSum) - } - } - } - - CollapsiblePanel { - title: root.tr("Live Trend") - expanded: true - Layout.fillWidth: true - - - - /* - Canvas { - id: trendCanvas + LiveTrendCard { + id: card Layout.fillWidth: true Layout.preferredHeight: 180 - property var samples: [0.08, 0.24, 0.52, 0.41, 0.63, 0.47, 0.72, 0.58, 0.82, 0.69] + title: root.tr("Payload Sum") - onPaint: { - const ctx = getContext("2d") - const w = width - const h = height - ctx.clearRect(0, 0, w, h) + Connections { + target: Backend.data + function onMetricsChanged() { + if (Backend.data.frameCount > 0) + card.plot.append(Backend.data.metricSum) + } + } + } - ctx.strokeStyle = "#D8EAD9" - ctx.lineWidth = 1 - for (let i = 1; i < 5; i++) { - const y = (h / 5) * i + CollapsiblePanel { + title: root.tr("Live Trend") + expanded: true + Layout.fillWidth: true + + + + /* + Canvas { + id: trendCanvas + Layout.fillWidth: true + Layout.preferredHeight: 180 + property var samples: [0.08, 0.24, 0.52, 0.41, 0.63, 0.47, 0.72, 0.58, 0.82, 0.69] + + onPaint: { + const ctx = getContext("2d") + const w = width + const h = height + ctx.clearRect(0, 0, w, h) + + ctx.strokeStyle = "#D8EAD9" + ctx.lineWidth = 1 + for (let i = 1; i < 5; i++) { + const y = (h / 5) * i + ctx.beginPath() + ctx.moveTo(0, y) + ctx.lineTo(w, y) + ctx.stroke() + } + + ctx.strokeStyle = root.accentColor + ctx.lineWidth = 2 ctx.beginPath() - ctx.moveTo(0, y) - ctx.lineTo(w, y) + for (let i = 0; i < samples.length; i++) { + const x = (w - 12) * (i / (samples.length - 1)) + 6 + const y = h - (h - 12) * samples[i] - 6 + if (i === 0) + ctx.moveTo(x, y) + else + ctx.lineTo(x, y) + } ctx.stroke() } - ctx.strokeStyle = root.accentColor - ctx.lineWidth = 2 - ctx.beginPath() - for (let i = 0; i < samples.length; i++) { - const x = (w - 12) * (i / (samples.length - 1)) + 6 - const y = h - (h - 12) * samples[i] - 6 - if (i === 0) - ctx.moveTo(x, y) - else - ctx.lineTo(x, y) + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + Component.onCompleted: requestPaint() + } + */ + } + + CollapsiblePanel { + title: root.tr("Metrics") + expanded: true + Layout.fillWidth: true + + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Rectangle { + Layout.fillWidth: true + radius: 6 + color: Qt.rgba(0, 0, 0, 0.03) + border.color: Qt.rgba(0, 0, 0, 0.08) + height: 72 + + Column { + anchors.centerIn: parent + spacing: 4 + Label { text: root.tr("峰值"); font.pixelSize: 12 } + Label { text: Backend.data.metricPeak.toFixed(0); font.pixelSize: 18; font.bold: true } + } + } + + Rectangle { + Layout.fillWidth: true + radius: 6 + color: Qt.rgba(0, 0, 0, 0.03) + border.color: Qt.rgba(0, 0, 0, 0.08) + height: 72 + + Column { + anchors.centerIn: parent + spacing: 4 + Label { text: root.tr("均方根"); font.pixelSize: 12 } + Label { text: Backend.data.metricRms.toFixed(0); font.pixelSize: 18; font.bold: true } + } } - ctx.stroke() } - onWidthChanged: requestPaint() - onHeightChanged: requestPaint() - Component.onCompleted: requestPaint() + RowLayout { + Layout.fillWidth: true + spacing: 8 + + Rectangle { + Layout.fillWidth: true + radius: 6 + color: Qt.rgba(0, 0, 0, 0.03) + border.color: Qt.rgba(0, 0, 0, 0.08) + height: 72 + + Column { + anchors.centerIn: parent + spacing: 4 + Label { text: root.tr("平均值"); font.pixelSize: 12 } + Label { text: Backend.data.metricAvg.toFixed(0); font.pixelSize: 18; font.bold: true } + } + } + + Rectangle { + Layout.fillWidth: true + radius: 6 + color: Qt.rgba(0, 0, 0, 0.03) + border.color: Qt.rgba(0, 0, 0, 0.08) + height: 72 + + Column { + anchors.centerIn: parent + spacing: 4 + Label { text: root.tr("变化量"); font.pixelSize: 12 } + Label { text: Backend.data.metricDelta.toFixed(0); font.pixelSize: 18; font.bold: true } + } + } + } } - */ + + CollapsiblePanel { + title: root.tr("Session") + expanded: true + Layout.fillWidth: true + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Label { text: root.tr("Frames"); Layout.preferredWidth: 80 } + Label { text: Backend.data.frameCount; Layout.fillWidth: true; horizontalAlignment: Text.AlignRight } + } + + RowLayout { + Layout.fillWidth: true + spacing: 8 + Label { text: root.tr("Playback"); Layout.preferredWidth: 80 } + Label { + text: Backend.data.playbackRunning ? root.tr("Running") : root.tr("Idle") + Layout.fillWidth: true + horizontalAlignment: Text.AlignRight + } + } + } + + CollapsiblePanel { + title: root.tr("Legend") + expanded: true + Layout.fillWidth: true + + Legend { + Layout.alignment: Qt.AlignHCenter + Layout.preferredHeight: 200 + barWidth: 36 + minValue: Backend.rangeMin + maxValue: Backend.rangeMax + colorLow: Backend.colorLow + colorMid: Backend.colorMid + colorHigh: Backend.colorHigh + } + } + + Item { Layout.fillHeight: true } } - - CollapsiblePanel { - title: root.tr("Metrics") - expanded: true - Layout.fillWidth: true - - RowLayout { - Layout.fillWidth: true - spacing: 8 - - Rectangle { - Layout.fillWidth: true - radius: 6 - color: Qt.rgba(0, 0, 0, 0.03) - border.color: Qt.rgba(0, 0, 0, 0.08) - height: 72 - - Column { - anchors.centerIn: parent - spacing: 4 - Label { text: root.tr("峰值"); font.pixelSize: 12 } - Label { text: Backend.data.metricPeak.toFixed(0); font.pixelSize: 18; font.bold: true } - } - } - - Rectangle { - Layout.fillWidth: true - radius: 6 - color: Qt.rgba(0, 0, 0, 0.03) - border.color: Qt.rgba(0, 0, 0, 0.08) - height: 72 - - Column { - anchors.centerIn: parent - spacing: 4 - Label { text: root.tr("均方根"); font.pixelSize: 12 } - Label { text: Backend.data.metricRms.toFixed(0); font.pixelSize: 18; font.bold: true } - } - } - } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - - Rectangle { - Layout.fillWidth: true - radius: 6 - color: Qt.rgba(0, 0, 0, 0.03) - border.color: Qt.rgba(0, 0, 0, 0.08) - height: 72 - - Column { - anchors.centerIn: parent - spacing: 4 - Label { text: root.tr("平均值"); font.pixelSize: 12 } - Label { text: Backend.data.metricAvg.toFixed(0); font.pixelSize: 18; font.bold: true } - } - } - - Rectangle { - Layout.fillWidth: true - radius: 6 - color: Qt.rgba(0, 0, 0, 0.03) - border.color: Qt.rgba(0, 0, 0, 0.08) - height: 72 - - Column { - anchors.centerIn: parent - spacing: 4 - Label { text: root.tr("变化量"); font.pixelSize: 12 } - Label { text: Backend.data.metricDelta.toFixed(0); font.pixelSize: 18; font.bold: true } - } - } - } - } - - CollapsiblePanel { - title: root.tr("Session") - expanded: true - Layout.fillWidth: true - - RowLayout { - Layout.fillWidth: true - spacing: 8 - Label { text: root.tr("Frames"); Layout.preferredWidth: 80 } - Label { text: Backend.data.frameCount; Layout.fillWidth: true; horizontalAlignment: Text.AlignRight } - } - - RowLayout { - Layout.fillWidth: true - spacing: 8 - Label { text: root.tr("Playback"); Layout.preferredWidth: 80 } - Label { - text: Backend.data.playbackRunning ? root.tr("Running") : root.tr("Idle") - Layout.fillWidth: true - horizontalAlignment: Text.AlignRight - } - } - } - - Item { Layout.fillHeight: true } } } diff --git a/shaders/dots.frag b/shaders/dots.frag index 5373972..6cdaaed 100644 --- a/shaders/dots.frag +++ b/shaders/dots.frag @@ -6,6 +6,9 @@ out vec4 FragColor; uniform float uMinV; uniform float uMaxV; +uniform vec3 uColorLow; +uniform vec3 uColorMid; +uniform vec3 uColorHigh; uniform sampler2D uDotTex; uniform int uHasData; // 0 = no data, 1 = has data uniform vec3 uCameraPos; @@ -18,14 +21,8 @@ float saturate(float x) { return clamp(x, 0.0, 1.0); } vec3 dataColorRamp(float t) { t = saturate(t); - vec3 c0 = vec3(0.10, 0.75, 1.00); // cyan-blue (low) - vec3 c1 = vec3(0.10, 0.95, 0.35); // green - vec3 c2 = vec3(1.00, 0.92, 0.22); // yellow - vec3 c3 = vec3(1.00, 0.22, 0.10); // red (high) - - if (t < 0.33) return mix(c0, c1, t / 0.33); - if (t < 0.66) return mix(c1, c2, (t - 0.33) / 0.33); - return mix(c2, c3, (t - 0.66) / 0.34); + if (t < 0.5) return mix(uColorLow, uColorMid, t / 0.5); + return mix(uColorMid, uColorHigh, (t - 0.5) / 0.5); } vec3 fresnelSchlick(float cosTheta, vec3 F0) { @@ -117,9 +114,8 @@ void main() { float value01 = clamp((vValue - uMinV) / max(1e-6, (uMaxV - uMinV)), 0.0, 1.0); vec3 dataCol = dataColorRamp(value01); - // bool hasData = (uHasData != 0); - // vec3 baseColor = hasData ? dataCol : metalBase; - vec3 baseColor = metalBase; + bool hasData = (uHasData != 0); + vec3 baseColor = hasData ? dataCol : metalBase; // dataViz: flat/unlit, no lighting modulation (keep pure baseColor) if (uRenderMode == 1) { diff --git a/src/backend.cpp b/src/backend.cpp index 267f69d..b8e4073 100644 --- a/src/backend.cpp +++ b/src/backend.cpp @@ -20,6 +20,8 @@ AppBackend::AppBackend(QObject* parent) m_data->clear(); emit connectedChanged(); }); + connect(this, &AppBackend::sensorRowChanged, m_serial, &SerialBackend::setSensorHeight); + connect(this, &AppBackend::sensorColChanged, m_serial, &SerialBackend::setSensorWidth); } bool AppBackend::connected() const { @@ -47,4 +49,67 @@ void AppBackend::setShowGrid(bool on) { m_showGrid = on; emit showGridChanged(on); -} \ No newline at end of file +} + +void AppBackend::setSensorCol(int c) { + if (m_serial->connected()) { + return; + } + m_sensorCol = c; + qInfo() << "sensorColChanged: " << c; + emit sensorColChanged(c); +} + +void AppBackend::setSensorRow(int r) { + if (m_serial->connected()) { + return; + } + m_sensorRow = r; + qInfo() << "sensorRowChanged: " << r; + emit sensorRowChanged(r); +} + +void AppBackend::setRangeMin(int v) { + if (m_rangeMin == v) + return; + m_rangeMin = v; + if (m_rangeMin > m_rangeMax) { + m_rangeMax = m_rangeMin; + emit rangeMaxChanged(m_rangeMax); + } + emit rangeMinChanged(m_rangeMin); + emit rangeChanged(m_rangeMin, m_rangeMax); +} + +void AppBackend::setRangeMax(int v) { + if (m_rangeMax == v) + return; + m_rangeMax = v; + if (m_rangeMax < m_rangeMin) { + m_rangeMin = m_rangeMax; + emit rangeMinChanged(m_rangeMin); + } + emit rangeMaxChanged(m_rangeMax); + emit rangeChanged(m_rangeMin, m_rangeMax); +} + +void AppBackend::setColorLow(const QColor& color) { + if (m_colorLow == color) + return; + m_colorLow = color; + emit colorLowChanged(m_colorLow); +} + +void AppBackend::setColorMid(const QColor& color) { + if (m_colorMid == color) + return; + m_colorMid = color; + emit colorMidChanged(m_colorMid); +} + +void AppBackend::setColorHigh(const QColor& color) { + if (m_colorHigh == color) + return; + m_colorHigh = color; + emit colorHighChanged(m_colorHigh); +} diff --git a/src/backend.h b/src/backend.h index 4c0227b..828b505 100644 --- a/src/backend.h +++ b/src/backend.h @@ -6,6 +6,7 @@ #define TACTILEIPC3D_BACKEND_H #include #include +#include #include #include "data_backend.h" @@ -19,6 +20,13 @@ class AppBackend : public QObject { Q_PROPERTY(bool showGrid READ showGrid WRITE setShowGrid NOTIFY showGridChanged); 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 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); public: explicit AppBackend(QObject* parent=nullptr); @@ -35,12 +43,34 @@ public: bool showGrid() const { return m_showGrid; } void setShowGrid(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); + void setSensorCol(int c); + int rangeMin() const { return m_rangeMin; } + int rangeMax() const { return m_rangeMax; } + void setRangeMin(int v); + void setRangeMax(int v); + QColor colorLow() const { return m_colorLow; } + QColor colorMid() const { return m_colorMid; } + QColor colorHigh() const { return m_colorHigh; } + void setColorLow(const QColor& color); + void setColorMid(const QColor& color); + void setColorHigh(const QColor& color); signals: void lightModeChanged(); void languageChanged(); void connectedChanged(); void showGridChanged(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 colorLowChanged(const QColor& color); + void colorMidChanged(const QColor& color); + void colorHighChanged(const QColor& color); private: SerialBackend* m_serial = nullptr; DataBackend* m_data = nullptr; @@ -48,6 +78,13 @@ private: QString m_language = QStringLiteral("zh_CN"); bool m_showGrid = true; + int m_sensorRow = 12; + int m_sensorCol = 7; + int m_rangeMin = 0; + int m_rangeMax = 1000; + 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); }; #endif //TACTILEIPC3D_BACKEND_H diff --git a/src/data_backend.cpp b/src/data_backend.cpp index 3ddce82..a7b5766 100644 --- a/src/data_backend.cpp +++ b/src/data_backend.cpp @@ -23,7 +23,7 @@ DataBackend::DataBackend(QObject* parent) emitFrame_(m_frames[m_playbackIndex], cb); m_playbackIndex++; }); - seedDebugFrames_(); + // seedDebugFrames_(); } void DataBackend::ingestFrame(const DataFrame& frame) { @@ -56,6 +56,14 @@ bool DataBackend::exportCsv(const QString& path) const { return true; } +bool DataBackend::exportXlsx(const QString& path) const { + /* QFile file(path); + if (!file.open(QIODevice::WriteOnly)) + return false; + */ + return buildXlsx_(path); +} + bool DataBackend::importJson(const QString& path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) @@ -70,6 +78,57 @@ bool DataBackend::importCsv(const QString& path) { return loadCsv_(file.readAll()); } +bool DataBackend::importXlsx(const QString& path) { + QXlsx::Document doc(path); + if (!doc.isLoadPackage()) { + qCritical() << "failed to open file: " << path; + return false; + } + + stopPlayback(); + clear(); + doc.selectSheet(1); + QXlsx::CellRange range = doc.dimension(); + int firstRow = range.firstRow(); + int lastRow = range.lastRow(); + int firstCol = range.firstColumn(); + int lastCol = range.lastColumn(); + + if (firstRow == 0 && lastRow == 0 && firstCol == 0 && lastCol == 0) { + return false; + } + int row_count = lastRow - firstRow + 1; + int col_count = lastCol - firstCol + 1; + // TODO 完善xlsx数据导入 + struct SpanInfo { + int row = 0; + int column = 0; + int rowSpan = 1; + int colSpan = 1; + }; + QVector> cells; + QVector spans; + for (int r = 0; r < row_count; ++r) { + cells.resize(col_count); + for (int c = 0; c < col_count; ++c) { + int excel_row = firstRow + r; + int excel_col = firstCol + c; + + std::shared_ptr cell_obj = doc.cellAt(excel_row, excel_col); + QVariant v; + + if (cell_obj) { + v = cell_obj->readValue(); + } + else { + v = doc.read(excel_row, excel_col); + } + + cells[r][c] = v; + } + } +} + void DataBackend::startPlayback(int intervalMs) { if (m_frames.isEmpty()) return; @@ -273,6 +332,47 @@ QByteArray DataBackend::buildCsv_() const { return out; } +bool DataBackend::buildXlsx_(const QString& path) const { + QXlsx::Document xlsx; + int current_sheet_index = 1; + int current_sheet_row_start = 1; + + int col_count = m_frames.at(0).data.size(); + auto ensure_sheet_for_row = [&](int row){ + if (m_frames.size() <= 0) { + if (xlsx.currentWorksheet() == nullptr) + xlsx.addSheet("Sheet1"); + return; + } + if (xlsx.currentWorksheet() == nullptr) { + xlsx.addSheet(QStringLiteral("Sheet%1").arg(current_sheet_index)); + current_sheet_row_start = 1; + return; + } + if (row - current_sheet_row_start >= m_frames.size()) { + ++current_sheet_index; + xlsx.addSheet(QStringLiteral("Sheet%1").arg(current_sheet_index)); + current_sheet_row_start = row; + } + }; + + for (size_t row = 1; row <= m_frames.size(); row++) { + ensure_sheet_for_row(row); + xlsx.write(1, 1, m_frames.at(row - 1).pts); + int col_index = 2; + for (auto data : m_frames.at(row - 1).data) { + xlsx.write(row, col_index, data); + col_index++; + } + } + if (!xlsx.saveAs(path)) { + qCritical() << "failed to save file: " << path; + return false; + } + + return true; +} + void DataBackend::seedDebugFrames_() { if (!m_frames.isEmpty()) return; diff --git a/src/data_backend.h b/src/data_backend.h index 9d9351c..0e73877 100644 --- a/src/data_backend.h +++ b/src/data_backend.h @@ -9,6 +9,8 @@ #include #include #include +#include "xlsxdocument.h" +#include "xlsxformat.h" #include "data_frame.h" class DataBackend : public QObject { @@ -42,13 +44,14 @@ public: Q_INVOKABLE void clear(); Q_INVOKABLE bool exportJson(const QString& path) const; Q_INVOKABLE bool exportCsv(const QString& path) const; + Q_INVOKABLE bool exportXlsx(const QString& path) const; Q_INVOKABLE bool importJson(const QString& path); Q_INVOKABLE bool importCsv(const QString& path); + Q_INVOKABLE bool importXlsx(const QString& path); Q_INVOKABLE void startPlayback(int intervalMs); Q_INVOKABLE void stopPlayback(); Q_INVOKABLE void exportHandler(const QUrl& folder, const QString& filename, const QString& format, const QString& method); - signals: void frameCountChanged(); void playbackRunningChanged(); @@ -59,6 +62,7 @@ private: bool loadCsv_(const QByteArray& data); QByteArray buildJson_() const; QByteArray buildCsv_() const; + bool buildXlsx_(const QString& path) const; void seedDebugFrames_(); diff --git a/src/glwidget.cpp b/src/glwidget.cpp index b57d94b..9d72ba3 100644 --- a/src/glwidget.cpp +++ b/src/glwidget.cpp @@ -41,6 +41,10 @@ static void matIdentity(float m[16]) { m[0] = m[5] = m[10] = m[15] = 1; } +static QVector3D toColorVec(const QColor& color) { + return QVector3D(color.redF(), color.greenF(), color.blueF()); +} + GLWidget::GLWidget(QWidget *parent) : QOpenGLWidget(parent) { setMinimumSize(640, 480); @@ -191,6 +195,30 @@ void GLWidget::setShowBg(bool on = true) { update(); } +void GLWidget::setColorLow(const QColor& color) { + const QVector3D next = toColorVec(color); + if (m_colorLow == next) + return; + m_colorLow = next; + update(); +} + +void GLWidget::setColorMid(const QColor& color) { + const QVector3D next = toColorVec(color); + if (m_colorMid == next) + return; + m_colorMid = next; + update(); +} + +void GLWidget::setColorHigh(const QColor& color) { + const QVector3D next = toColorVec(color); + if (m_colorHigh == next) + return; + m_colorHigh = next; + update(); +} + void GLWidget::initializeGL() { initializeOpenGLFunctions(); @@ -211,6 +239,14 @@ void GLWidget::initializeGL() { matIdentity(m_proj); } +void GLWidget::initGeometry_() { + initDotTexture_(); + initBackgroundGeometry_(); + initPanelGeometry_(); + initDotGeometry_(); + initRoomGeometry_(); +} + void GLWidget::resizeGL(int w, int h) { glViewport(0, 0, w, h); } @@ -293,8 +329,11 @@ void GLWidget::paintGL() { // 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", 0); + m_dotsProg->setUniformValue("uHasData", hasData); m_dotsProg->setUniformValue("uCameraPos", m_cameraPos); m_dotsProg->setUniformValue("uDotTex", 0); if (m_dotTex) { @@ -427,6 +466,7 @@ void GLWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { QVector3D world; const int index = pickDotIndex_(event->pos(), &world); + qInfo() << "clicked index: " << index; if (index >= 0) { float value = 0.0f; int row = 0; @@ -1098,3 +1138,17 @@ void GLWidget::setShowGrid(bool on) { m_showGrid = on; update(); } + +void GLWidget::setRow(int row) { + row = qMax(0, row); + if (m_rows == row) + return; + setSpec(row, m_cols, m_pitch, m_dotRadius); +} + +void GLWidget::setCol(int col) { + col = qMax(0, col); + if (m_cols == col) + return; + setSpec(m_rows, col, m_pitch, m_dotRadius); +} diff --git a/src/glwidget.h b/src/glwidget.h index 5813522..639ff49 100644 --- a/src/glwidget.h +++ b/src/glwidget.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,8 @@ struct Ray { class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core { Q_OBJECT Q_PROPERTY(float yaw READ yaw WRITE setYaw NOTIFY yawChanged) + Q_PROPERTY(int row READ row WRITE setRow) + Q_PROPERTY(int col READ col WRITE setCol) // Q_PROPERTY(bool showGrid READ showGrid WRITE setShowGrid NOTIFY showGridChanged) public: enum RenderMode { @@ -59,7 +62,8 @@ public: bool showGrid() const { return m_showGrid; } - + int row() const { return m_rows; } + int col() const { return m_cols; } public slots: // 值域范围,用于 shader 里把 value 映射到颜色(绿->红) @@ -70,6 +74,11 @@ public slots: void setLightMode(bool on); void setShowBg(bool on); void setShowGrid(bool on); + void setCol(int col); + void setRow(int row); + void setColorLow(const QColor& color); + void setColorMid(const QColor& color); + void setColorHigh(const QColor& color); signals: void yawChanged(); @@ -85,6 +94,7 @@ protected: void wheelEvent(QWheelEvent *event) override; private: + void initGeometry_(); void initPanelGeometry_(); void initDotGeometry_(); void initBackgroundGeometry_(); @@ -154,6 +164,9 @@ private: unsigned int m_bgVbo = 0; bool m_lightMode = true; bool m_showBg = true; + QVector3D m_colorLow{0.10f, 0.75f, 1.00f}; + QVector3D m_colorMid{0.10f, 0.95f, 0.35f}; + QVector3D m_colorHigh{1.00f, 0.22f, 0.10f}; // MVP = Projection * View * Model。 // 这里 panel/dots 顶点基本已经是“世界坐标”,所以我们用 proj*view 即可(model 先省略)。 diff --git a/src/serial/serial_backend.cpp b/src/serial/serial_backend.cpp index 3d129bf..a1334f4 100644 --- a/src/serial/serial_backend.cpp +++ b/src/serial/serial_backend.cpp @@ -18,8 +18,6 @@ SerialBackend::SerialBackend(QObject* parent) , m_decodeThread(&m_packetQueue, &m_frameQueue) { m_request.dataLength = 24; m_spec.model = QStringLiteral("PZR-A"); - m_spec.rows = 3; - m_spec.cols = 4; auto codec = std::make_shared(); auto decoder = std::make_shared(); @@ -270,6 +268,16 @@ void SerialBackend::feedBytes(const QByteArray& data) { m_readThread.enqueueBytes(data); } +void SerialBackend::setSensorWidth(int w) { + m_spec.cols = w; + syncSendConfig_(); +} + +void SerialBackend::setSensorHeight(int h) { + m_spec.rows = h; + syncSendConfig_(); +} + void SerialBackend::drainFrames_() { if (!m_frameCallback) return; diff --git a/src/serial/serial_backend.h b/src/serial/serial_backend.h index 9b60f24..8530675 100644 --- a/src/serial/serial_backend.h +++ b/src/serial/serial_backend.h @@ -69,6 +69,13 @@ public: Q_INVOKABLE void requestOnce(); Q_INVOKABLE void feedBytes(const QByteArray& data); + int sensorWidth() const { return m_spec.cols; } + int sensorHeight() const { return m_spec.rows; } + +public slots: + void setSensorWidth(int w); + void setSensorHeight(int h); + signals: void portNameChanged(); void baudRateChanged(); diff --git a/src/serial/serial_types.h b/src/serial/serial_types.h index 2697f9c..e4194cd 100644 --- a/src/serial/serial_types.h +++ b/src/serial/serial_types.h @@ -32,8 +32,8 @@ struct SensorRequest { struct SensorSpec { QString model; QString version; - int rows = 0; - int cols = 0; + int rows = 12; + int cols = 7; float pitch = 0.0f; float dotRadius = 0.0f; float rangeMin = 0.0f;