颜色映射图例,规格尺寸修改

This commit is contained in:
2026-01-15 16:13:36 +08:00
parent f700dd360e
commit 354552dc88
21 changed files with 1200 additions and 223 deletions

View File

@@ -9,6 +9,8 @@ set(CMAKE_AUTOUIC ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_subdirectory(3rdpart/QXlsx/QXlsx)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src)
find_package(Qt6 COMPONENTS find_package(Qt6 COMPONENTS
@@ -22,6 +24,7 @@ find_package(Qt6 COMPONENTS
Quick Quick
QuickControls2 QuickControls2
QuickLayouts QuickLayouts
QuickDialogs2
LinguistTools LinguistTools
) )
@@ -70,6 +73,8 @@ target_link_libraries(TactileIpc3D
Qt6::Quick Qt6::Quick
Qt6::QuickControls2 Qt6::QuickControls2
Qt6::QuickLayouts Qt6::QuickLayouts
Qt6::QuickDialogs2
QXlsx::QXlsx
) )
set(TS_FILES set(TS_FILES

View File

@@ -10,8 +10,18 @@ classDiagram
+bool connected +bool connected
+SerialBackend* serial +SerialBackend* serial
+DataBackend* data +DataBackend* data
+int rangeMin
+int rangeMax
+QColor colorLow
+QColor colorMid
+QColor colorHigh
+setLightMode(bool) +setLightMode(bool)
+setLanguage(string) +setLanguage(string)
+setRangeMin(int)
+setRangeMax(int)
+setColorLow(QColor)
+setColorMid(QColor)
+setColorHigh(QColor)
} }
class SerialConfig { class SerialConfig {
@@ -60,6 +70,10 @@ classDiagram
+setTransport(transport) +setTransport(transport)
} }
class GLWidget { class GLWidget {
+setRange(int, int)
+setColorLow(QColor)
+setColorMid(QColor)
+setColorHigh(QColor)
+dotClicked(index, row, col, value) +dotClicked(index, row, col, value)
} }
@@ -160,6 +174,7 @@ classDiagram
AppBackend --> SerialBackend AppBackend --> SerialBackend
AppBackend --> DataBackend AppBackend --> DataBackend
AppBackend ..> GLWidget : render config
SerialBackend --> SerialConfig SerialBackend --> SerialConfig
SerialBackend --> SensorRequest SerialBackend --> SensorRequest
SerialBackend --> SensorSpec SerialBackend --> SensorSpec
@@ -248,6 +263,15 @@ flowchart TD
SB -->|updateProtocolBindings_| SW3[SerialSendWorker.setBuildRequestFunc] SB -->|updateProtocolBindings_| SW3[SerialSendWorker.setBuildRequestFunc]
``` ```
## 渲染/颜色映射流程 (Mermaid)
```mermaid
flowchart LR
UI[LeftPanel 颜色映射] -->|rangeMin/rangeMax<br/>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)` - `SerialSendWorker::setBuildRequestFunc(codec->buildRequest)`
- 打开串口: - 打开串口:
- `SerialBackend::open()` -> `SerialSendWorker::openTransport(config)`,成功后在从站模式启动轮询发送。 - `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` 分层,独立于业务逻辑。 - 串口采集层:`Transport + Format + Codec + Decoder + Manager` 分层,独立于业务逻辑。
- 串口线程化:读取/解码/发送三线程 + Packet/Frame 队列,降低 UI 卡顿风险。 - 串口线程化:读取/解码/发送三线程 + Packet/Frame 队列,降低 UI 卡顿风险。
- 数据驱动层:负责帧缓存、数据导入导出与回放,提供渲染回调。 - 数据驱动层:负责帧缓存、数据导入导出与回放,提供渲染回调。
- 渲染配置层:`AppBackend` 提供范围/颜色参数,`GLWidget` 将其转为 shader uniform 进行颜色映射。
- UI 层:`NavBar + LeftPanel + OpenGL View + RightPanel` 的 1+3 布局。 - UI 层:`NavBar + LeftPanel + OpenGL View + RightPanel` 的 1+3 布局。
## 类接口与成员说明C++ ## 类接口与成员说明C++
@@ -289,11 +319,16 @@ flowchart TD
- `lightMode` / `language` / `connected`UI 基础状态。 - `lightMode` / `language` / `connected`UI 基础状态。
- `serial()` / `data()`:暴露子系统实例给 QML。 - `serial()` / `data()`:暴露子系统实例给 QML。
- `setLightMode(bool)` / `setLanguage(string)` - `setLightMode(bool)` / `setLanguage(string)`
- `rangeMin` / `rangeMax`:颜色映射的数值范围。
- `colorLow` / `colorMid` / `colorHigh`:颜色映射的三个基准颜色。
- `setRangeMin(int)` / `setRangeMax(int)` / `setColorLow(QColor)` / `setColorMid(QColor)` / `setColorHigh(QColor)`
- 成员变量: - 成员变量:
- `m_serial`:串口采集层对象。 - `m_serial`:串口采集层对象。
- `m_data`:数据驱动层对象。 - `m_data`:数据驱动层对象。
- `m_lightMode` / `m_language`:全局 UI 状态。 - `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`) ### SerialBackend (`src/serial/serial_backend.h`)
- 作用:串口采集层的统一控制器,负责协议选择、三线程调度与数据分发。 - 作用:串口采集层的统一控制器,负责协议选择、三线程调度与数据分发。
@@ -395,9 +430,14 @@ flowchart TD
### GLWidget (`src/glwidget.h`) ### GLWidget (`src/glwidget.h`)
- 作用OpenGL 渲染窗口,显示传感器点阵与背景。 - 作用OpenGL 渲染窗口,显示传感器点阵与背景。
- 接口:
- `setRange(int minV, int maxV)`:设置 `uMinV/uMaxV`
- `setColorLow(QColor)` / `setColorMid(QColor)` / `setColorHigh(QColor)`:设置 `uColorLow/uColorMid/uColorHigh`
- 信号: - 信号:
- `dotClicked(index, row, col, value)`:鼠标点击某个点时发出索引与数据值。 - `dotClicked(index, row, col, value)`:鼠标点击某个点时发出索引与数据值。
- 备注:拾取使用屏幕投影 + 半径阈值,便于 RightPanel 订阅点击事件做曲线展示。 - 备注:
- 颜色映射在 `dots.frag` 内完成,低/中/高三段线性插值。
- 拾取使用屏幕投影 + 半径阈值,便于 RightPanel 订阅点击事件做曲线展示。
### DataFrame (`src/data_frame.h`) ### DataFrame (`src/data_frame.h`)
- 字段: - 字段:
@@ -440,6 +480,9 @@ flowchart TD
- 采样周期(从站模式可用)。 - 采样周期(从站模式可用)。
- 采样参数:功能码、起始地址、读取长度。 - 采样参数:功能码、起始地址、读取长度。
- 传感器规格:协议名、型号、网格规格占位。 - 传感器规格:协议名、型号、网格规格占位。
- 颜色映射:
- 数值范围min/max
- 低/中/高三色选择(`ColorDialog`)。
- 显示控制:显示网络/坐标轴、回放与导出入口。 - 显示控制:显示网络/坐标轴、回放与导出入口。
### RightPanel`qml/content/RightPanel.qml` ### RightPanel`qml/content/RightPanel.qml`
@@ -518,6 +561,7 @@ flowchart TD
## 更新记录 ## 更新记录
- 2026-01-11新增 `QtSerialTransport``QSerialPort` 传输实现)并设为默认传输;补齐点选拾取逻辑;新增数据流/时序图并补充可视化 TODO 说明。 - 2026-01-11新增 `QtSerialTransport``QSerialPort` 传输实现)并设为默认传输;补齐点选拾取逻辑;新增数据流/时序图并补充可视化 TODO 说明。
- 2026-01-12`LeftPanel` 新增颜色映射参数;`AppBackend`/`GLWidget` 增加颜色与范围接口shader 使用三色渐变映射数据值。
- 2026-01-11补充串口配置流程图与配置接口说明协议/参数/解码器绑定/打开流程)。 - 2026-01-11补充串口配置流程图与配置接口说明协议/参数/解码器绑定/打开流程)。
- 2026-01-05新增串口三线程流水线读/解码/发送)与 Packet/Frame 队列,更新协议起始符说明,补充队列溢出 TODO 与线程组件文档。 - 2026-01-05新增串口三线程流水线读/解码/发送)与 Packet/Frame 队列,更新协议起始符说明,补充队列溢出 TODO 与线程组件文档。
- 2026-01-05CollapsiblePanel 组件改为跟随 `backend.lightMode` 切换暗色主题配色。 - 2026-01-05CollapsiblePanel 组件改为跟随 `backend.lightMode` 切换暗色主题配色。

View File

@@ -96,6 +96,14 @@
<source></source> <source></source>
<translation>Sample Interval</translation> <translation>Sample Interval</translation>
</message> </message>
<message>
<source></source>
<translation>Width</translation>
</message>
<message>
<source></source>
<translation>Height</translation>
</message>
<message> <message>
<source></source> <source></source>
<translation>Connect</translation> <translation>Connect</translation>
@@ -140,6 +148,34 @@
<source></source> <source></source>
<translation>Rescan</translation> <translation>Rescan</translation>
</message> </message>
<message>
<source></source>
<translation>Color Mapping</translation>
</message>
<message>
<source></source>
<translation>Min Value</translation>
</message>
<message>
<source></source>
<translation>Max Value</translation>
</message>
<message>
<source></source>
<translation>Low Color</translation>
</message>
<message>
<source></source>
<translation>Select</translation>
</message>
<message>
<source></source>
<translation>Mid Color</translation>
</message>
<message>
<source></source>
<translation>High Color</translation>
</message>
<message> <message>
<source></source> <source></source>
<translation>Display</translation> <translation>Display</translation>
@@ -160,6 +196,45 @@
<source></source> <source></source>
<translation>Export Data</translation> <translation>Export Data</translation>
</message> </message>
<message>
<source></source>
<translation>Select Low Color</translation>
</message>
<message>
<source></source>
<translation>Select Mid Color</translation>
</message>
<message>
<source></source>
<translation>Select High Color</translation>
</message>
</context>
<context>
<name>OpenFileDialog</name>
<message>
<source></source>
<translation>Import Data</translation>
</message>
<message>
<source></source>
<translation>Locations</translation>
</message>
<message>
<source></source>
<translation>This PC</translation>
</message>
<message>
<source></source>
<translation>Desktop</translation>
</message>
<message>
<source></source>
<translation>Documents</translation>
</message>
<message>
<source></source>
<translation>Downloads</translation>
</message>
</context> </context>
<context> <context>
<name>RightPanel</name> <name>RightPanel</name>
@@ -171,6 +246,10 @@
<source>Live Trend</source> <source>Live Trend</source>
<translation>Live Trend</translation> <translation>Live Trend</translation>
</message> </message>
<message>
<source>Legend</source>
<translation>Legend</translation>
</message>
<message> <message>
<source>Metrics</source> <source>Metrics</source>
<translation>Metrics</translation> <translation>Metrics</translation>

View File

@@ -171,6 +171,10 @@
<source>Live Trend</source> <source>Live Trend</source>
<translation></translation> <translation></translation>
</message> </message>
<message>
<source>Legend</source>
<translation></translation>
</message>
<message> <message>
<source>Metrics</source> <source>Metrics</source>
<translation></translation> <translation></translation>

View File

@@ -44,7 +44,8 @@ int main(int argc, char *argv[]) {
QOpenGLContext probeCtx; QOpenGLContext probeCtx;
probeCtx.setFormat(QSurfaceFormat::defaultFormat()); probeCtx.setFormat(QSurfaceFormat::defaultFormat());
if (!probeCtx.create()) { 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; return 1;
} }
@@ -54,17 +55,22 @@ int main(int argc, char *argv[]) {
if (!probeCtx.makeCurrent(&probeSurface)) { if (!probeCtx.makeCurrent(&probeSurface)) {
qCritical().noquote() 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; return 1;
} }
const QSurfaceFormat actual = probeCtx.format(); 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) { if (!versionOk || actual.profile() != QSurfaceFormat::CoreProfile) {
probeCtx.doneCurrent(); probeCtx.doneCurrent();
qCritical().noquote() qCritical().noquote()
<< "OpenGL context is not OpenGL 3.3 Core (got: " << "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; return 1;
} }
@@ -84,12 +90,12 @@ int main(int argc, char *argv[]) {
qmlRegisterSingletonInstance("TactileIPC", 1, 0, "I18n", &i18n); qmlRegisterSingletonInstance("TactileIPC", 1, 0, "I18n", &i18n);
qmlRegisterType<SparklinePlotItem>("LiveTrend", 1, 0, "SparklinePlot"); qmlRegisterType<SparklinePlotItem>("LiveTrend", 1, 0, "SparklinePlot");
i18n.setLanguage(backend.language()); i18n.setLanguage(backend.language());
QObject::connect(&backend, &AppBackend::languageChanged, &i18n, [&backend, &i18n]() { QObject::connect(
i18n.setLanguage(backend.language()); &backend, &AppBackend::languageChanged, &i18n,
}); [&backend, &i18n]() { i18n.setLanguage(backend.language()); });
auto *qmlEngine = new QQmlEngine(root); auto *qmlEngine = new QQmlEngine(root);
auto createQuickWidget = [&](const QUrl& sourceUrl) -> QQuickWidget* { auto createQuickWidget = [&](const QUrl &sourceUrl) -> QQuickWidget * {
auto *view = new QQuickWidget(qmlEngine, root); auto *view = new QQuickWidget(qmlEngine, root);
view->setResizeMode(QQuickWidget::SizeRootObjectToView); view->setResizeMode(QQuickWidget::SizeRootObjectToView);
view->setSource(sourceUrl); view->setSource(sourceUrl);
@@ -107,26 +113,30 @@ int main(int argc, char *argv[]) {
leftView->setFixedWidth(350); leftView->setFixedWidth(350);
auto *glw = new GLWidget; 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->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) { /* backend.data()->setLiveRenderCallback([glw](const DataFrame& frame) {
if (frame.data.size() != glw->dotCount()) if (frame.data.size() != glw->dotCount())
return; return;
glw->submitValues(frame.data); glw->submitValues(frame.data);
}); */ }); */
backend.data()->setLiveRenderCallback([](const DataFrame& frame) { backend.data()->setLiveRenderCallback([](const DataFrame &frame) {
if (frame.data.size() != 0) { 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
// 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 // 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(); qDebug() << "data size: " << frame.data.size();
} }
}); });
// TODO:待实现内容(将frame数据分发给右侧曲线/指标的QML接口) // 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(leftView);
splitter->addWidget(glw); splitter->addWidget(glw);
@@ -136,33 +146,52 @@ int main(int argc, char *argv[]) {
splitter->setStretchFactor(2, 0); splitter->setStretchFactor(2, 0);
splitter->setSizes({320, 640, 320}); splitter->setSizes({320, 640, 320});
auto applySplitterStyle = [&backend, splitter]() { auto applySplitterStyle = [&backend, splitter]() {
const QString handleColor = backend.lightMode() ? QStringLiteral("#E0E0E0") : QStringLiteral("#2C2C2C"); const QString handleColor = backend.lightMode()
splitter->setStyleSheet(QStringLiteral("QSplitter::handle { background: %1; }").arg(handleColor)); ? QStringLiteral("#E0E0E0")
: QStringLiteral("#2C2C2C");
splitter->setStyleSheet(
QStringLiteral("QSplitter::handle { background: %1; }")
.arg(handleColor));
}; };
applySplitterStyle(); applySplitterStyle();
QObject::connect(&backend, &AppBackend::lightModeChanged, splitter, [applySplitterStyle]() { QObject::connect(&backend, &AppBackend::lightModeChanged, splitter,
applySplitterStyle(); [applySplitterStyle]() { applySplitterStyle(); });
});
auto applyQuickTheme = [&backend, navView, leftView, rightView]() { auto applyQuickTheme = [&backend, navView, leftView, rightView]() {
const QColor navColor = backend.lightMode() ? QColor(QStringLiteral("#F5F7F5")) : QColor(QStringLiteral("#2B2F2B")); const QColor navColor = backend.lightMode()
const QColor panelColor = backend.lightMode() ? QColor(QStringLiteral("#F5F5F5")) : QColor(QStringLiteral("#2C2C2C")); ? QColor(QStringLiteral("#F5F7F5"))
: QColor(QStringLiteral("#2B2F2B"));
const QColor panelColor = backend.lightMode()
? QColor(QStringLiteral("#F5F5F5"))
: QColor(QStringLiteral("#2C2C2C"));
navView->setClearColor(navColor); navView->setClearColor(navColor);
leftView->setClearColor(panelColor); leftView->setClearColor(panelColor);
rightView->setClearColor(panelColor); rightView->setClearColor(panelColor);
}; };
applyQuickTheme(); applyQuickTheme();
QObject::connect(&backend, &AppBackend::lightModeChanged, navView, [applyQuickTheme]() { QObject::connect(&backend, &AppBackend::lightModeChanged, navView,
applyQuickTheme(); [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]() { QObject::connect(&backend, &AppBackend::showGridChanged, glw,
bool m = backend.lightMode() ? true : false; &GLWidget::setShowGrid);
glw->setLightMode(m); QObject::connect(&backend, &AppBackend::sensorRowChanged, glw,
}); &GLWidget::setRow);
QObject::connect(&backend, &AppBackend::sensorColChanged, glw,
QObject::connect(&backend, &AppBackend::showGridChanged, glw, &GLWidget::setShowGrid); &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(navView);
rootLayout->addWidget(splitter); rootLayout->addWidget(splitter);

View File

@@ -6,7 +6,7 @@ import TactileIPC 1.0
Item { Item {
id: root id: root
width: 350 implicitWidth: 350
property alias title: titleText.text property alias title: titleText.text
property bool expanded: true property bool expanded: true

View File

@@ -2,6 +2,7 @@ import QtQuick
import QtQuick.Controls.Material import QtQuick.Controls.Material
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Dialogs
import "." import "."
import TactileIPC 1.0 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 { RowLayout {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 12 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 { CollapsiblePanel {
title: root.tr("显示控制") title: root.tr("显示控制")
expanded: true expanded: true
@@ -399,7 +580,14 @@ Rectangle {
Button { Button {
Layout.fillWidth: true Layout.fillWidth: true
text: root.tr("导出数据") 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 } 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 { SaveAsExportDialog {
id: exportDlg id: exportDlg
/* onSaveTo: (folder, filename, format, method) => { /* onSaveTo: (folder, filename, format, method) => {

View File

@@ -7,9 +7,14 @@ Item {
id: root id: root
property int minValue: 0 property int minValue: 0
property int maxValue: 100 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 implicitWidth: barWidth + 48
implicitHeight: 220 implicitHeight: 240
ColumnLayout { ColumnLayout {
anchors.fill: parent anchors.fill: parent
@@ -25,17 +30,16 @@ Item {
Rectangle { Rectangle {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
Layout.fillHeight: true Layout.fillHeight: true
width: 26 width: root.barWidth
radius: 6 radius: root.barRadius
border.width: 1 border.width: 1
border.color: Qt.rgba(1, 1, 1, 0.18) border.color: Qt.rgba(1, 1, 1, 0.18)
gradient: Gradient { gradient: Gradient {
// must match shaders/dots.frag:dataColorRamp (high at top) // 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.00; color: root.colorHigh }
GradientStop { position: 0.34; color: Qt.rgba(1.00, 0.92, 0.22, 1.0) } // c2 GradientStop { position: 0.50; color: root.colorMid }
GradientStop { position: 0.67; color: Qt.rgba(0.10, 0.95, 0.35, 1.0) } // c1 GradientStop { position: 1.00; color: root.colorLow }
GradientStop { position: 1.00; color: Qt.rgba(0.10, 0.75, 1.00, 1.0) } // c0
} }
} }
@@ -47,4 +51,3 @@ Item {
} }
} }
} }

View File

@@ -92,8 +92,8 @@ Rectangle {
anchors.left: parent.left anchors.left: parent.left
anchors.leftMargin: 8 anchors.leftMargin: 8
Image { Image {
width: 18 width: 16
height: 12 height: 16
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
source: modelData.icon source: modelData.icon
} }
@@ -107,10 +107,13 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
Row { Row {
spacing: 8 spacing: 8
anchors.left: parent.left
anchors.leftMargin: 10
anchors.rightMargin: 24
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Image { Image {
width: 18 width: 16
height: 12 height: 16
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
source: langBox.model[langBox.currentIndex] source: langBox.model[langBox.currentIndex]
? langBox.model[langBox.currentIndex].icon ? langBox.model[langBox.currentIndex].icon

View File

@@ -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+:
}
}
}
}
}
}
}
}
}

View File

@@ -22,177 +22,205 @@ Rectangle {
Material.accent: Material.Green Material.accent: Material.Green
Material.primary: Material.Green Material.primary: Material.Green
ColumnLayout { ScrollView {
id: scrollView
anchors.fill: parent anchors.fill: parent
anchors.margins: 12 ScrollBar.vertical.policy: ScrollBar.AlwaysOff
spacing: 12 ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
contentWidth: scrollView.availableWidth
contentHeight: contentLayout.implicitHeight + 24
LiveTrendCard { ColumnLayout {
id: card id: contentLayout
Layout.fillWidth: true x: 12
Layout.preferredHeight: 180 y: 12
title: root.tr("Payload Sum") width: scrollView.availableWidth - 24
spacing: 12
Connections { LiveTrendCard {
target: Backend.data id: card
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
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 180 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: { Connections {
const ctx = getContext("2d") target: Backend.data
const w = width function onMetricsChanged() {
const h = height if (Backend.data.frameCount > 0)
ctx.clearRect(0, 0, w, h) card.plot.append(Backend.data.metricSum)
}
}
}
ctx.strokeStyle = "#D8EAD9" CollapsiblePanel {
ctx.lineWidth = 1 title: root.tr("Live Trend")
for (let i = 1; i < 5; i++) { expanded: true
const y = (h / 5) * i 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.beginPath()
ctx.moveTo(0, y) for (let i = 0; i < samples.length; i++) {
ctx.lineTo(w, y) 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.stroke()
} }
ctx.strokeStyle = root.accentColor onWidthChanged: requestPaint()
ctx.lineWidth = 2 onHeightChanged: requestPaint()
ctx.beginPath() Component.onCompleted: requestPaint()
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) CollapsiblePanel {
else title: root.tr("Metrics")
ctx.lineTo(x, y) 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() RowLayout {
onHeightChanged: requestPaint() Layout.fillWidth: true
Component.onCompleted: requestPaint() 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 }
} }
} }

View File

@@ -6,6 +6,9 @@ out vec4 FragColor;
uniform float uMinV; uniform float uMinV;
uniform float uMaxV; uniform float uMaxV;
uniform vec3 uColorLow;
uniform vec3 uColorMid;
uniform vec3 uColorHigh;
uniform sampler2D uDotTex; uniform sampler2D uDotTex;
uniform int uHasData; // 0 = no data, 1 = has data uniform int uHasData; // 0 = no data, 1 = has data
uniform vec3 uCameraPos; uniform vec3 uCameraPos;
@@ -18,14 +21,8 @@ float saturate(float x) { return clamp(x, 0.0, 1.0); }
vec3 dataColorRamp(float t) { vec3 dataColorRamp(float t) {
t = saturate(t); t = saturate(t);
vec3 c0 = vec3(0.10, 0.75, 1.00); // cyan-blue (low) if (t < 0.5) return mix(uColorLow, uColorMid, t / 0.5);
vec3 c1 = vec3(0.10, 0.95, 0.35); // green return mix(uColorMid, uColorHigh, (t - 0.5) / 0.5);
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);
} }
vec3 fresnelSchlick(float cosTheta, vec3 F0) { 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); float value01 = clamp((vValue - uMinV) / max(1e-6, (uMaxV - uMinV)), 0.0, 1.0);
vec3 dataCol = dataColorRamp(value01); vec3 dataCol = dataColorRamp(value01);
// bool hasData = (uHasData != 0); bool hasData = (uHasData != 0);
// vec3 baseColor = hasData ? dataCol : metalBase; vec3 baseColor = hasData ? dataCol : metalBase;
vec3 baseColor = metalBase;
// dataViz: flat/unlit, no lighting modulation (keep pure baseColor) // dataViz: flat/unlit, no lighting modulation (keep pure baseColor)
if (uRenderMode == 1) { if (uRenderMode == 1) {

View File

@@ -20,6 +20,8 @@ AppBackend::AppBackend(QObject* parent)
m_data->clear(); m_data->clear();
emit connectedChanged(); emit connectedChanged();
}); });
connect(this, &AppBackend::sensorRowChanged, m_serial, &SerialBackend::setSensorHeight);
connect(this, &AppBackend::sensorColChanged, m_serial, &SerialBackend::setSensorWidth);
} }
bool AppBackend::connected() const { bool AppBackend::connected() const {
@@ -47,4 +49,67 @@ void AppBackend::setShowGrid(bool on) {
m_showGrid = on; m_showGrid = on;
emit showGridChanged(on); emit showGridChanged(on);
} }
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);
}

View File

@@ -6,6 +6,7 @@
#define TACTILEIPC3D_BACKEND_H #define TACTILEIPC3D_BACKEND_H
#include <QObject> #include <QObject>
#include <QString> #include <QString>
#include <QColor>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include "data_backend.h" #include "data_backend.h"
@@ -19,6 +20,13 @@ class AppBackend : public QObject {
Q_PROPERTY(bool showGrid READ showGrid WRITE setShowGrid NOTIFY showGridChanged); Q_PROPERTY(bool showGrid READ showGrid WRITE setShowGrid NOTIFY showGridChanged);
Q_PROPERTY(SerialBackend* serial READ serial CONSTANT) Q_PROPERTY(SerialBackend* serial READ serial CONSTANT)
Q_PROPERTY(DataBackend* data READ data 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: public:
explicit AppBackend(QObject* parent=nullptr); explicit AppBackend(QObject* parent=nullptr);
@@ -35,12 +43,34 @@ public:
bool showGrid() const { return m_showGrid; } bool showGrid() const { return m_showGrid; }
void setShowGrid(bool on); 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: signals:
void lightModeChanged(); void lightModeChanged();
void languageChanged(); void languageChanged();
void connectedChanged(); void connectedChanged();
void showGridChanged(bool on); 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: private:
SerialBackend* m_serial = nullptr; SerialBackend* m_serial = nullptr;
DataBackend* m_data = nullptr; DataBackend* m_data = nullptr;
@@ -48,6 +78,13 @@ private:
QString m_language = QStringLiteral("zh_CN"); QString m_language = QStringLiteral("zh_CN");
bool m_showGrid = true; 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 #endif //TACTILEIPC3D_BACKEND_H

View File

@@ -23,7 +23,7 @@ DataBackend::DataBackend(QObject* parent)
emitFrame_(m_frames[m_playbackIndex], cb); emitFrame_(m_frames[m_playbackIndex], cb);
m_playbackIndex++; m_playbackIndex++;
}); });
seedDebugFrames_(); // seedDebugFrames_();
} }
void DataBackend::ingestFrame(const DataFrame& frame) { void DataBackend::ingestFrame(const DataFrame& frame) {
@@ -56,6 +56,14 @@ bool DataBackend::exportCsv(const QString& path) const {
return true; 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) { bool DataBackend::importJson(const QString& path) {
QFile file(path); QFile file(path);
if (!file.open(QIODevice::ReadOnly)) if (!file.open(QIODevice::ReadOnly))
@@ -70,6 +78,57 @@ bool DataBackend::importCsv(const QString& path) {
return loadCsv_(file.readAll()); 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<QVector<QVariant>> cells;
QVector<SpanInfo> 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<QXlsx::Cell> 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) { void DataBackend::startPlayback(int intervalMs) {
if (m_frames.isEmpty()) if (m_frames.isEmpty())
return; return;
@@ -273,6 +332,47 @@ QByteArray DataBackend::buildCsv_() const {
return out; 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_() { void DataBackend::seedDebugFrames_() {
if (!m_frames.isEmpty()) if (!m_frames.isEmpty())
return; return;

View File

@@ -9,6 +9,8 @@
#include <QSerialPort> #include <QSerialPort>
#include <QSerialPortInfo> #include <QSerialPortInfo>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include "xlsxdocument.h"
#include "xlsxformat.h"
#include "data_frame.h" #include "data_frame.h"
class DataBackend : public QObject { class DataBackend : public QObject {
@@ -42,13 +44,14 @@ public:
Q_INVOKABLE void clear(); Q_INVOKABLE void clear();
Q_INVOKABLE bool exportJson(const QString& path) const; Q_INVOKABLE bool exportJson(const QString& path) const;
Q_INVOKABLE bool exportCsv(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 importJson(const QString& path);
Q_INVOKABLE bool importCsv(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 startPlayback(int intervalMs);
Q_INVOKABLE void stopPlayback(); Q_INVOKABLE void stopPlayback();
Q_INVOKABLE void exportHandler(const QUrl& folder, const QString& filename, Q_INVOKABLE void exportHandler(const QUrl& folder, const QString& filename,
const QString& format, const QString& method); const QString& format, const QString& method);
signals: signals:
void frameCountChanged(); void frameCountChanged();
void playbackRunningChanged(); void playbackRunningChanged();
@@ -59,6 +62,7 @@ private:
bool loadCsv_(const QByteArray& data); bool loadCsv_(const QByteArray& data);
QByteArray buildJson_() const; QByteArray buildJson_() const;
QByteArray buildCsv_() const; QByteArray buildCsv_() const;
bool buildXlsx_(const QString& path) const;
void seedDebugFrames_(); void seedDebugFrames_();

View File

@@ -41,6 +41,10 @@ static void matIdentity(float m[16]) {
m[0] = m[5] = m[10] = m[15] = 1; m[0] = m[5] = m[10] = m[15] = 1;
} }
static QVector3D toColorVec(const QColor& color) {
return QVector3D(color.redF(), color.greenF(), color.blueF());
}
GLWidget::GLWidget(QWidget *parent) GLWidget::GLWidget(QWidget *parent)
: QOpenGLWidget(parent) { : QOpenGLWidget(parent) {
setMinimumSize(640, 480); setMinimumSize(640, 480);
@@ -191,6 +195,30 @@ void GLWidget::setShowBg(bool on = true) {
update(); 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() { void GLWidget::initializeGL() {
initializeOpenGLFunctions(); initializeOpenGLFunctions();
@@ -211,6 +239,14 @@ void GLWidget::initializeGL() {
matIdentity(m_proj); matIdentity(m_proj);
} }
void GLWidget::initGeometry_() {
initDotTexture_();
initBackgroundGeometry_();
initPanelGeometry_();
initDotGeometry_();
initRoomGeometry_();
}
void GLWidget::resizeGL(int w, int h) { void GLWidget::resizeGL(int w, int h) {
glViewport(0, 0, w, h); glViewport(0, 0, w, h);
} }
@@ -293,8 +329,11 @@ void GLWidget::paintGL() {
// uMinV/uMaxV: 传感值范围,用于 fragment shader 把 value 映射成颜色 // uMinV/uMaxV: 传感值范围,用于 fragment shader 把 value 映射成颜色
m_dotsProg->setUniformValue("uMinV", float(m_min)); m_dotsProg->setUniformValue("uMinV", float(m_min));
m_dotsProg->setUniformValue("uMaxV", float(m_max)); 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; 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("uCameraPos", m_cameraPos);
m_dotsProg->setUniformValue("uDotTex", 0); m_dotsProg->setUniformValue("uDotTex", 0);
if (m_dotTex) { if (m_dotTex) {
@@ -427,6 +466,7 @@ void GLWidget::mousePressEvent(QMouseEvent *event) {
if (event->button() == Qt::LeftButton) { if (event->button() == Qt::LeftButton) {
QVector3D world; QVector3D world;
const int index = pickDotIndex_(event->pos(), &world); const int index = pickDotIndex_(event->pos(), &world);
qInfo() << "clicked index: " << index;
if (index >= 0) { if (index >= 0) {
float value = 0.0f; float value = 0.0f;
int row = 0; int row = 0;
@@ -1098,3 +1138,17 @@ void GLWidget::setShowGrid(bool on) {
m_showGrid = on; m_showGrid = on;
update(); 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);
}

View File

@@ -14,6 +14,7 @@
#include <QMatrix4x4> #include <QMatrix4x4>
#include <QVector3D> #include <QVector3D>
#include <QString> #include <QString>
#include <QColor>
#include <atomic> #include <atomic>
#include <qtmetamacros.h> #include <qtmetamacros.h>
#include <qvectornd.h> #include <qvectornd.h>
@@ -26,6 +27,8 @@ struct Ray {
class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core { class GLWidget : public QOpenGLWidget, protected QOpenGLFunctions_3_3_Core {
Q_OBJECT Q_OBJECT
Q_PROPERTY(float yaw READ yaw WRITE setYaw NOTIFY yawChanged) 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) // Q_PROPERTY(bool showGrid READ showGrid WRITE setShowGrid NOTIFY showGridChanged)
public: public:
enum RenderMode { enum RenderMode {
@@ -59,7 +62,8 @@ public:
bool showGrid() const { return m_showGrid; } bool showGrid() const { return m_showGrid; }
int row() const { return m_rows; }
int col() const { return m_cols; }
public slots: public slots:
// 值域范围,用于 shader 里把 value 映射到颜色(绿->红) // 值域范围,用于 shader 里把 value 映射到颜色(绿->红)
@@ -70,6 +74,11 @@ public slots:
void setLightMode(bool on); void setLightMode(bool on);
void setShowBg(bool on); void setShowBg(bool on);
void setShowGrid(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: signals:
void yawChanged(); void yawChanged();
@@ -85,6 +94,7 @@ protected:
void wheelEvent(QWheelEvent *event) override; void wheelEvent(QWheelEvent *event) override;
private: private:
void initGeometry_();
void initPanelGeometry_(); void initPanelGeometry_();
void initDotGeometry_(); void initDotGeometry_();
void initBackgroundGeometry_(); void initBackgroundGeometry_();
@@ -154,6 +164,9 @@ private:
unsigned int m_bgVbo = 0; unsigned int m_bgVbo = 0;
bool m_lightMode = true; bool m_lightMode = true;
bool m_showBg = 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。 // MVP = Projection * View * Model。
// 这里 panel/dots 顶点基本已经是“世界坐标”,所以我们用 proj*view 即可model 先省略)。 // 这里 panel/dots 顶点基本已经是“世界坐标”,所以我们用 proj*view 即可model 先省略)。

View File

@@ -18,8 +18,6 @@ SerialBackend::SerialBackend(QObject* parent)
, m_decodeThread(&m_packetQueue, &m_frameQueue) { , m_decodeThread(&m_packetQueue, &m_frameQueue) {
m_request.dataLength = 24; m_request.dataLength = 24;
m_spec.model = QStringLiteral("PZR-A"); m_spec.model = QStringLiteral("PZR-A");
m_spec.rows = 3;
m_spec.cols = 4;
auto codec = std::make_shared<PiezoresistiveACodec>(); auto codec = std::make_shared<PiezoresistiveACodec>();
auto decoder = std::make_shared<PiezoresistiveADecoder>(); auto decoder = std::make_shared<PiezoresistiveADecoder>();
@@ -270,6 +268,16 @@ void SerialBackend::feedBytes(const QByteArray& data) {
m_readThread.enqueueBytes(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_() { void SerialBackend::drainFrames_() {
if (!m_frameCallback) if (!m_frameCallback)
return; return;

View File

@@ -69,6 +69,13 @@ public:
Q_INVOKABLE void requestOnce(); Q_INVOKABLE void requestOnce();
Q_INVOKABLE void feedBytes(const QByteArray& data); 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: signals:
void portNameChanged(); void portNameChanged();
void baudRateChanged(); void baudRateChanged();

View File

@@ -32,8 +32,8 @@ struct SensorRequest {
struct SensorSpec { struct SensorSpec {
QString model; QString model;
QString version; QString version;
int rows = 0; int rows = 12;
int cols = 0; int cols = 7;
float pitch = 0.0f; float pitch = 0.0f;
float dotRadius = 0.0f; float dotRadius = 0.0f;
float rangeMin = 0.0f; float rangeMin = 0.0f;