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;