颜色映射图例,规格尺寸修改
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<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)`
|
||||
- 打开串口:
|
||||
- `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` 切换暗色主题配色。
|
||||
|
||||
@@ -96,6 +96,14 @@
|
||||
<source>采样周期</source>
|
||||
<translation>Sample Interval</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>宽</source>
|
||||
<translation>Width</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>高</source>
|
||||
<translation>Height</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>连接</source>
|
||||
<translation>Connect</translation>
|
||||
@@ -140,6 +148,34 @@
|
||||
<source>重新识别</source>
|
||||
<translation>Rescan</translation>
|
||||
</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>
|
||||
<source>显示控制</source>
|
||||
<translation>Display</translation>
|
||||
@@ -160,6 +196,45 @@
|
||||
<source>导出数据</source>
|
||||
<translation>Export Data</translation>
|
||||
</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>
|
||||
<name>RightPanel</name>
|
||||
@@ -171,6 +246,10 @@
|
||||
<source>Live Trend</source>
|
||||
<translation>Live Trend</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Legend</source>
|
||||
<translation>Legend</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Metrics</source>
|
||||
<translation>Metrics</translation>
|
||||
|
||||
@@ -171,6 +171,10 @@
|
||||
<source>Live Trend</source>
|
||||
<translation>实时趋势</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Legend</source>
|
||||
<translation>图例</translation>
|
||||
</message>
|
||||
<message>
|
||||
<source>Metrics</source>
|
||||
<translation>指标</translation>
|
||||
|
||||
91
main.cpp
91
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<SparklinePlotItem>("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);
|
||||
|
||||
@@ -6,7 +6,7 @@ import TactileIPC 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
width: 350
|
||||
implicitWidth: 350
|
||||
|
||||
property alias title: titleText.text
|
||||
property bool expanded: true
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
291
qml/content/OpenFileDialog.qml
Normal file
291
qml/content/OpenFileDialog.qml
Normal 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+:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
@@ -48,3 +50,66 @@ void AppBackend::setShowGrid(bool on) {
|
||||
m_showGrid = 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);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#define TACTILEIPC3D_BACKEND_H
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QColor>
|
||||
#include <qtmetamacros.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(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
|
||||
|
||||
@@ -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<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) {
|
||||
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;
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#include <QSerialPort>
|
||||
#include <QSerialPortInfo>
|
||||
#include <qtmetamacros.h>
|
||||
#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_();
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <QMatrix4x4>
|
||||
#include <QVector3D>
|
||||
#include <QString>
|
||||
#include <QColor>
|
||||
#include <atomic>
|
||||
#include <qtmetamacros.h>
|
||||
#include <qvectornd.h>
|
||||
@@ -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 先省略)。
|
||||
|
||||
@@ -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<PiezoresistiveACodec>();
|
||||
auto decoder = std::make_shared<PiezoresistiveADecoder>();
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user