Compare commits

..

3 Commits

Author SHA1 Message Date
c9495680d6 Merge branch 'main' of http://gitea.huangyanjie.com/lenn/tactileipc3d 2026-01-20 20:00:49 +08:00
4882dc1a67 update gitignore 2026-01-20 20:00:16 +08:00
bc9f2824ed 点阵完成,加入opencv 2026-01-20 19:55:56 +08:00
11 changed files with 764 additions and 53 deletions

2
.gitignore vendored
View File

@@ -11,7 +11,7 @@ TactileIpc3D_autogen/
*.ninja *.ninja
*.ninja_deps *.ninja_deps
*.ninja_log *.ninja_log
OpenCV/
# Qt generated files # Qt generated files
*.moc *.moc
moc_*.cpp moc_*.cpp

View File

@@ -9,9 +9,6 @@ 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)
find_package(Qt6 COMPONENTS find_package(Qt6 COMPONENTS
Core Core
@@ -28,6 +25,10 @@ find_package(Qt6 COMPONENTS
LinguistTools LinguistTools
) )
set(QT_VERSION_MAJOR 6)
add_subdirectory(3rdpart/QXlsx/QXlsx)
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src)
qt_standard_project_setup() qt_standard_project_setup()
add_executable(TactileIpc3D add_executable(TactileIpc3D
@@ -62,8 +63,10 @@ add_executable(TactileIpc3D
src/ringbuffer.cpp src/ringbuffer.cpp
src/sparkline_plotitem.h src/sparkline_plotitem.h
src/sparkling_plotitem.cpp src/sparkling_plotitem.cpp
src/globalhelper.h
src/globalhelper.h
) )
target_link_libraries(TactileIpc3D target_link_libraries(TactileIpc3D PRIVATE
Qt6::Core Qt6::Core
Qt6::Gui Qt6::Gui
Qt6::Widgets Qt6::Widgets
@@ -76,7 +79,9 @@ target_link_libraries(TactileIpc3D
Qt6::QuickDialogs2 Qt6::QuickDialogs2
QXlsx::QXlsx QXlsx::QXlsx
) )
target_include_directories(TactileIpc3D PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/3rdpart/OpenCV/include
)
set(TS_FILES set(TS_FILES
${CMAKE_CURRENT_SOURCE_DIR}/i18n/app_zh_CN.ts ${CMAKE_CURRENT_SOURCE_DIR}/i18n/app_zh_CN.ts
${CMAKE_CURRENT_SOURCE_DIR}/i18n/app_en_US.ts ${CMAKE_CURRENT_SOURCE_DIR}/i18n/app_en_US.ts
@@ -109,31 +114,17 @@ qt_add_resources(TactileIpc3D i18n_resources
FILES ${QM_FILES} FILES ${QM_FILES}
) )
#if (WIN32 AND NOT DEFINED CMAKE_TOOLCHAIN_FILE) set(runtime_out_dir "${CMAKE_BINARY_DIR}/out")
# set(DEBUG_SUFFIX) set_target_properties(TactileIpc3D PROPERTIES
# if (MSVC AND CMAKE_BUILD_TYPE MATCHES "Debug") RUNTIME_OUTPUT_DIRECTORY "${runtime_out_dir}"
# set(DEBUG_SUFFIX "d") RUNTIME_OUTPUT_DIRECTORY_DEBUG "${runtime_out_dir}/Debug"
# endif () RUNTIME_OUTPUT_DIRECTORY_RELEASE "${runtime_out_dir}/Release"
# set(QT_INSTALL_PATH "${CMAKE_PREFIX_PATH}") RUNTIME_OUTPUT_DIRECTORY_RELWITHDEBINFO "${runtime_out_dir}/RelWithDebInfo"
# if (NOT EXISTS "${QT_INSTALL_PATH}/bin") RUNTIME_OUTPUT_DIRECTORY_MINSIZEREL "${runtime_out_dir}/MinSizeRel"
# set(QT_INSTALL_PATH "${QT_INSTALL_PATH}/..") )
# if (NOT EXISTS "${QT_INSTALL_PATH}/bin")
# set(QT_INSTALL_PATH "${QT_INSTALL_PATH}/..") include(GNUInstallDirs)
# endif () install(TARGETS TactileIpc3D
# endif () RUNTIME DESTINATION bin
# if (EXISTS "${QT_INSTALL_PATH}/plugins/platforms/qwindows${DEBUG_SUFFIX}.dll") )
# add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
# COMMAND ${CMAKE_COMMAND} -E make_directory
# "$<TARGET_FILE_DIR:${PROJECT_NAME}>/plugins/platforms/")
# add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
# COMMAND ${CMAKE_COMMAND} -E copy
# "${QT_INSTALL_PATH}/plugins/platforms/qwindows${DEBUG_SUFFIX}.dll"
# "$<TARGET_FILE_DIR:${PROJECT_NAME}>/plugins/platforms/")
# endif ()
# foreach (QT_LIB Core Gui Widgets)
# add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
# COMMAND ${CMAKE_COMMAND} -E copy
# "${QT_INSTALL_PATH}/bin/Qt6${QT_LIB}${DEBUG_SUFFIX}.dll"
# "$<TARGET_FILE_DIR:${PROJECT_NAME}>")
# endforeach (QT_LIB)
#endif ()

1
Configure Normal file
View File

@@ -0,0 +1 @@
==

View File

@@ -345,7 +345,7 @@ QSGNode* SparklinePlotItem::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeDat
当前限制: 当前限制:
- `xlsx` 仅提示未实现(不会写出文件) - `xlsx` 通过QXlsx是实现但是缺少传感器没有测试
- `zip` 未实现压缩逻辑(请视为暂不支持) - `zip` 未实现压缩逻辑(请视为暂不支持)
相关代码:`src/data_backend.cpp``exportHandler` / `buildCsv_` / `buildJson_``qml/content/SaveAsExportDialog.qml` 相关代码:`src/data_backend.cpp``exportHandler` / `buildCsv_` / `buildJson_``qml/content/SaveAsExportDialog.qml`

1
VERSION.txt Normal file
View File

@@ -0,0 +1 @@
1.0.0

View File

@@ -0,0 +1,52 @@
#define MyAppName "TactileIpc3D"
#define MyAppVersion "0.2.0"
#define MyAppPublisher "TactileIpc3D"
#define MyAppExeName "TactileIpc3D.exe"
#define SourceDir "..\\build\Desktop_Qt_6_8_3_MinGW_64_bit-Release\\out\Release"
#define OutputDir "..\\dist"
#define AssetsDir "assets"
#define AppIconFile AssetsDir + "\\App.ico"
#define WizardSmallFile AssetsDir + "\\WizardSmall.bmp"
#define WizardLargeFile AssetsDir + "\\WizardLarge.bmp"
[Setup]
AppId={{1E4C86D4-4E53-4B8A-9F7F-1D56E8E04353}}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
OutputDir={#OutputDir}
OutputBaseFilename={#MyAppName}-Setup-{#MyAppVersion}
Compression=lzma
SolidCompression=yes
WizardStyle=modern
SetupLogging=yes
ArchitecturesAllowed=x64
ArchitecturesInstallIn64BitMode=x64
#ifexist AppIconFile
SetupIconFile={#AppIconFile}
#endif
#ifexist WizardSmallFile
WizardSmallImageFile={#WizardSmallFile}
#endif
#ifexist WizardLargeFile
WizardImageFile={#WizardLargeFile}
#endif
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
; Name: "chinesesimp"; MessagesFile: "compiler:Languages\\ChineseSimplified.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: "{#SourceDir}\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\\{#MyAppName}"; Filename: "{app}\\{#MyAppExeName}"
Name: "{autodesktop}\\{#MyAppName}"; Filename: "{app}\\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#MyAppName}}"; Flags: postinstall nowait skipifsilent

View File

@@ -0,0 +1,6 @@
Place installer assets here:
- App.ico
- WizardSmall.bmp
- WizardLarge.bmp
If these files are missing, the Inno Setup script will skip the custom icon/images.

View File

@@ -0,0 +1,616 @@
// ColorPickerWindow.qml
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.Controls.Material
import TactileIPC 1.0
Window {
id: root
width: 360
height: 650
visible: false
modality: Qt.ApplicationModal
flags: Qt.Dialog
title: qsTr("颜色选择")
Material.theme: Backend.lightMode ? Material.Light : Material.Dark
Material.accent: Material.Green
Material.primary: Material.Green
readonly property bool isDark: !Backend.lightMode
readonly property color windowBg: isDark ? "#1F1F1F" : "#F7F7F7"
readonly property color panelBorder: isDark ? "#2E2E2E" : "#D9D9D9"
readonly property color surfaceBorder: isDark ? "#343434" : "#CFCFCF"
readonly property color textPrimary: isDark ? "#EDEDED" : "#1F1F1F"
readonly property color textSecondary: isDark ? "#D8D8D8" : "#616161"
readonly property color textMuted: isDark ? "#BEBEBE" : "#6E6E6E"
readonly property color controlBg: isDark ? "#2A2A2A" : "#FFFFFF"
readonly property color controlBorder: isDark ? "#3A3A3A" : "#CFCFCF"
readonly property color highlightBg: isDark ? "#55FFFFFF" : "#33000000"
readonly property color highlightText: isDark ? "#111111" : "#FFFFFF"
readonly property color indicatorBorder: isDark ? "#6A6A6A" : "#9E9E9E"
readonly property color accentColor: Material.color(Material.Green)
// ===== API =====
property color color: "#FF7032D2" // AARRGGBB
signal accepted(color c)
signal rejected()
function openWith(c) {
syncFromColor(c ?? root.color)
visible = true
requestActivate()
}
Keys.onEscapePressed: {
visible = false
rejected()
}
// ===== internal HSV(A) =====
property real h: 0.75 // 0..1
property real s: 0.45
property real v: 0.82
property real a: 1.0
property bool _lock: false
function clamp01(x) { return Math.max(0, Math.min(1, x)) }
function clampInt(x, lo, hi) { return Math.max(lo, Math.min(hi, Math.round(x))) }
function hsvToRgb(hh, ss, vv) {
const h6 = (hh % 1) * 6
const c = vv * ss
const x = c * (1 - Math.abs((h6 % 2) - 1))
const m = vv - c
let r1=0, g1=0, b1=0
if (0 <= h6 && h6 < 1) { r1=c; g1=x; b1=0 }
else if (1 <= h6 && h6 < 2) { r1=x; g1=c; b1=0 }
else if (2 <= h6 && h6 < 3) { r1=0; g1=c; b1=x }
else if (3 <= h6 && h6 < 4) { r1=0; g1=x; b1=c }
else if (4 <= h6 && h6 < 5) { r1=x; g1=0; b1=c }
else { r1=c; g1=0; b1=x }
return { r: r1 + m, g: g1 + m, b: b1 + m }
}
function rgbToHsv(r, g, b) {
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
const d = max - min
let hh = 0
if (d !== 0) {
if (max === r) hh = ((g - b) / d) % 6
else if (max === g) hh = (b - r) / d + 2
else hh = (r - g) / d + 4
hh /= 6
if (hh < 0) hh += 1
}
const ss = (max === 0) ? 0 : d / max
const vv = max
return { h: hh, s: ss, v: vv }
}
function toHex2(v01) {
const n = clampInt(clamp01(v01) * 255, 0, 255)
const s = n.toString(16).toUpperCase()
return (s.length === 1) ? ("0" + s) : s
}
function colorToHexAARRGGBB(c) {
return "#" + toHex2(c.a) + toHex2(c.r) + toHex2(c.g) + toHex2(c.b)
}
function parseHex(str) {
let t = ("" + str).trim()
if (t.startsWith("0x") || t.startsWith("0X")) t = t.slice(2)
if (t.startsWith("#")) t = t.slice(1)
if (t.length === 6) {
const rr = parseInt(t.slice(0,2), 16)
const gg = parseInt(t.slice(2,4), 16)
const bb = parseInt(t.slice(4,6), 16)
if ([rr,gg,bb].some(x => isNaN(x))) return null
return Qt.rgba(rr/255, gg/255, bb/255, 1)
}
if (t.length === 8) {
const aa = parseInt(t.slice(0,2), 16)
const rr = parseInt(t.slice(2,4), 16)
const gg = parseInt(t.slice(4,6), 16)
const bb = parseInt(t.slice(6,8), 16)
if ([aa,rr,gg,bb].some(x => isNaN(x))) return null
return Qt.rgba(rr/255, gg/255, bb/255, aa/255)
}
return null
}
function syncFromHSV() {
if (_lock) return
_lock = true
const rgb = hsvToRgb(h, s, v)
color = Qt.rgba(rgb.r, rgb.g, rgb.b, a)
// 更新 UI
hexField.text = colorToHexAARRGGBB(color)
alphaPercent.text = clampInt(a*100, 0, 100) + "%"
rField.value = clampInt(color.r * 255, 0, 255)
gField.value = clampInt(color.g * 255, 0, 255)
bField.value = clampInt(color.b * 255, 0, 255)
// HSV 显示用度/百分比
hField.value = clampInt(h * 360, 0, 360)
sField.value = clampInt(s * 100, 0, 100)
vField.value = clampInt(v * 100, 0, 100)
svCanvas.requestPaint()
hueCanvas.requestPaint()
alphaCanvas.requestPaint()
_lock = false
}
function syncFromColor(c) {
if (_lock) return
_lock = true
const hsv = rgbToHsv(c.r, c.g, c.b)
h = hsv.h; s = hsv.s; v = hsv.v; a = c.a
color = Qt.rgba(c.r, c.g, c.b, a)
hexField.text = colorToHexAARRGGBB(color)
alphaPercent.text = clampInt(a*100, 0, 100) + "%"
rField.value = clampInt(c.r * 255, 0, 255)
gField.value = clampInt(c.g * 255, 0, 255)
bField.value = clampInt(c.b * 255, 0, 255)
hField.value = clampInt(h * 360, 0, 360)
sField.value = clampInt(s * 100, 0, 100)
vField.value = clampInt(v * 100, 0, 100)
svCanvas.requestPaint()
hueCanvas.requestPaint()
alphaCanvas.requestPaint()
_lock = false
}
function applyRGB(r255, g255, b255) {
if (_lock) return
const rr = clampInt(r255, 0, 255) / 255
const gg = clampInt(g255, 0, 255) / 255
const bb = clampInt(b255, 0, 255) / 255
const hsv = rgbToHsv(rr, gg, bb)
h = hsv.h; s = hsv.s; v = hsv.v
syncFromHSV()
}
function applyHSV(hDeg, sPct, vPct) {
if (_lock) return
h = clamp01(hDeg / 360)
s = clamp01(sPct / 100)
v = clamp01(vPct / 100)
syncFromHSV()
}
// 初始同步
Component.onCompleted: syncFromColor(root.color)
// ====== UI: 深色窗口面板(不是卡片)======
Rectangle {
anchors.fill: parent
radius: 0
color: root.windowBg
border.width: 1
border.color: root.panelBorder
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 10
// 顶部:标题 + 勾选(参考图右上角)
// RowLayout {
// Layout.fillWidth: true
// spacing: 8
// Label {
// text: root.title
// color: root.textPrimary
// font.pixelSize: 14
// font.weight: Font.DemiBold
// Layout.fillWidth: true
// }
// CheckBox {
// id: alphaToggle
// checked: true
// contentItem: Label {
// text: qsTr("实时预览")
// color: root.textSecondary
// verticalAlignment: Text.AlignVCenter
// elide: Text.ElideRight
// }
// indicator: Rectangle {
// implicitWidth: 16
// implicitHeight: 16
// radius: 3
// border.width: 1
// border.color: root.indicatorBorder
// color: alphaToggle.checked ? root.accentColor : "transparent"
// // 选中勾
// Canvas {
// anchors.fill: parent
// onPaint: {
// const ctx = getContext("2d")
// ctx.clearRect(0,0,width,height)
// if (!alphaToggle.checked) return
// ctx.strokeStyle = "white"
// ctx.lineWidth = 2
// ctx.lineCap = "round"
// ctx.beginPath()
// ctx.moveTo(width*0.25, height*0.55)
// ctx.lineTo(width*0.45, height*0.72)
// ctx.lineTo(width*0.78, height*0.30)
// ctx.stroke()
// }
// }
// }
// }
// ToolButton {
// text: "✕"
// onClicked: { root.visible = false; root.rejected() }
// }
// }
// HSV 面板
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 185
radius: 8
border.width: 1
border.color: root.surfaceBorder
clip: true
Canvas {
id: svCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d")
const w = width, hh = height
ctx.clearRect(0,0,w,hh)
const rgbHue = root.hsvToRgb(root.h, 1, 1)
// X: white -> hue
const gx = ctx.createLinearGradient(0,0,w,0)
gx.addColorStop(0, "rgb(255,255,255)")
gx.addColorStop(1, "rgb(" + Math.round(rgbHue.r*255) + "," + Math.round(rgbHue.g*255) + "," + Math.round(rgbHue.b*255) + ")")
ctx.fillStyle = gx
ctx.fillRect(0,0,w,hh)
// Y: transparent -> black
const gy = ctx.createLinearGradient(0,0,0,hh)
gy.addColorStop(0, "rgba(0,0,0,0)")
gy.addColorStop(1, "rgba(0,0,0,1)")
ctx.fillStyle = gy
ctx.fillRect(0,0,w,hh)
// handle
const x = root.s * w
const y = (1 - root.v) * hh
ctx.beginPath()
ctx.arc(x, y, 7, 0, Math.PI*2)
ctx.lineWidth = 2
ctx.strokeStyle = "rgba(255,255,255,0.95)"
ctx.stroke()
ctx.beginPath()
ctx.arc(x, y, 6, 0, Math.PI*2)
ctx.lineWidth = 1
ctx.strokeStyle = "rgba(0,0,0,0.55)"
ctx.stroke()
}
}
MouseArea {
anchors.fill: parent
function apply(mx, my) {
root.s = root.clamp01(mx / width)
root.v = root.clamp01(1 - (my / height))
root.syncFromHSV()
}
onPressed: (e) => apply(e.x, e.y)
onPositionChanged: (e) => { if (pressed) apply(e.x, e.y) }
}
}
// Hue 彩虹条(参考图那种)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 16
radius: 8
border.width: 1
border.color: root.surfaceBorder
clip: true
Canvas {
id: hueCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d")
const w = width, hh = height
ctx.clearRect(0,0,w,hh)
const grad = ctx.createLinearGradient(0,0,w,0)
for (let i=0; i<=6; i++) {
const t = i/6
const rgb = root.hsvToRgb(t, 1, 1)
grad.addColorStop(t, "rgb(" + Math.round(rgb.r*255) + "," + Math.round(rgb.g*255) + "," + Math.round(rgb.b*255) + ")")
}
ctx.fillStyle = grad
ctx.fillRect(0,0,w,hh)
const x = root.h * w
ctx.beginPath()
ctx.arc(x, hh/2, 7, 0, Math.PI*2)
ctx.lineWidth = 2
ctx.strokeStyle = "rgba(255,255,255,0.95)"
ctx.stroke()
ctx.beginPath()
ctx.arc(x, hh/2, 6, 0, Math.PI*2)
ctx.lineWidth = 1
ctx.strokeStyle = "rgba(0,0,0,0.55)"
ctx.stroke()
}
}
MouseArea {
anchors.fill: parent
function apply(mx) {
root.h = root.clamp01(mx / width)
root.syncFromHSV()
}
onPressed: (e) => apply(e.x)
onPositionChanged: (e) => { if (pressed) apply(e.x) }
}
}
// Alpha 条(棋盘 + 渐变 + 右侧圆点)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 16
radius: 8
border.width: 1
border.color: root.surfaceBorder
clip: true
// checker
Canvas {
id: checkerCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d")
const w = width, hh = height
ctx.clearRect(0,0,w,hh)
const s = 8
for (let y=0; y<hh; y+=s) {
for (let x=0; x<w; x+=s) {
const on = ((x/s + y/s) % 2) === 0
ctx.fillStyle = on ? "rgba(255,255,255,0.16)" : "rgba(0,0,0,0.0)"
ctx.fillRect(x,y,s,s)
}
}
}
}
Canvas {
id: alphaCanvas
anchors.fill: parent
onPaint: {
const ctx = getContext("2d")
const w = width, hh = height
ctx.clearRect(0,0,w,hh)
const rgb = root.hsvToRgb(root.h, root.s, root.v)
const grad = ctx.createLinearGradient(0,0,w,0)
grad.addColorStop(0, "rgba(" + Math.round(rgb.r*255) + "," + Math.round(rgb.g*255) + "," + Math.round(rgb.b*255) + ",0)")
grad.addColorStop(1, "rgba(" + Math.round(rgb.r*255) + "," + Math.round(rgb.g*255) + "," + Math.round(rgb.b*255) + ",1)")
ctx.fillStyle = grad
ctx.fillRect(0,0,w,hh)
const x = root.a * w
ctx.beginPath()
ctx.arc(x, hh/2, 7, 0, Math.PI*2)
ctx.lineWidth = 2
ctx.strokeStyle = "rgba(255,255,255,0.95)"
ctx.stroke()
ctx.beginPath()
ctx.arc(x, hh/2, 6, 0, Math.PI*2)
ctx.lineWidth = 1
ctx.strokeStyle = "rgba(0,0,0,0.55)"
ctx.stroke()
}
}
MouseArea {
anchors.fill: parent
function apply(mx) {
root.a = root.clamp01(mx / width)
root.syncFromHSV()
}
onPressed: (e) => apply(e.x)
onPositionChanged: (e) => { if (pressed) apply(e.x) }
}
}
// 下方:色块 + Hex + 透明度百分比
RowLayout {
Layout.fillWidth: true
spacing: 10
// 预览色块(带棋盘底)
Rectangle {
width: 44
height: 44
radius: 8
border.width: 1
border.color: root.surfaceBorder
clip: true
Canvas {
anchors.fill: parent
onPaint: {
const ctx = getContext("2d")
const w = width, hh = height
ctx.clearRect(0,0,w,hh)
const s = 10
for (let y=0; y<hh; y+=s) {
for (let x=0; x<w; x+=s) {
const on = ((x/s + y/s) % 2) === 0
ctx.fillStyle = on ? "rgba(255,255,255,0.14)" : "rgba(0,0,0,0.0)"
ctx.fillRect(x,y,s,s)
}
}
}
}
Rectangle {
anchors.fill: parent
color: root.color
}
}
TextField {
id: hexField
Layout.fillWidth: true
text: "#FF7032D2"
placeholderText: "#AARRGGBB"
inputMethodHints: Qt.ImhPreferUppercase | Qt.ImhNoPredictiveText
// 用 palette而不是 color/selectionColor/selectedTextColor
palette.text: root.textPrimary
palette.placeholderText: root.textMuted
palette.highlight: root.highlightBg
palette.highlightedText: root.highlightText
palette.base: root.controlBg // 输入框底色(有的 style 会用它)
palette.buttonText: root.textPrimary
background: Rectangle {
radius: 8
color: root.controlBg
border.width: 1
border.color: root.controlBorder
}
onEditingFinished: {
const c = root.parseHex(text)
if (c) root.syncFromColor(c)
else text = root.colorToHexAARRGGBB(root.color)
}
}
Rectangle {
width: 54
height: 44
radius: 8
color: root.controlBg
border.width: 1
border.color: root.controlBorder
Label {
id: alphaPercent
anchors.centerIn: parent
text: "100%"
color: root.textPrimary
font.pixelSize: 12
}
}
}
// RGB / HSV 数值区(像参考图那样两行块)
GridLayout {
Layout.fillWidth: true
columns: 3
columnSpacing: 8
rowSpacing: 8
// ---- Row1: RGB ----
Label { text: "RGB"; color: root.textMuted; Layout.alignment: Qt.AlignVCenter }
SpinBox {
id: rField
from: 0; to: 255; editable: true
value: 112
Layout.fillWidth: true
onValueModified: root.applyRGB(value, gField.value, bField.value)
}
SpinBox {
id: gField
from: 0; to: 255; editable: true
value: 50
Layout.fillWidth: true
onValueModified: root.applyRGB(rField.value, value, bField.value)
}
Item { width: 1; height: 1 } // 占位,让下一行对齐
SpinBox {
id: bField
from: 0; to: 255; editable: true
value: 210
Layout.fillWidth: true
Layout.columnSpan: 2
onValueModified: root.applyRGB(rField.value, gField.value, value)
}
// ---- Row2: HSV ----
Label { text: "HSV"; color: root.textMuted; Layout.alignment: Qt.AlignVCenter }
SpinBox {
id: hField
from: 0; to: 360; editable: true
value: 263
Layout.fillWidth: true
onValueModified: root.applyHSV(value, sField.value, vField.value)
}
SpinBox {
id: sField
from: 0; to: 100; editable: true
value: 64
Layout.fillWidth: true
onValueModified: root.applyHSV(hField.value, value, vField.value)
}
Item { width: 1; height: 1 }
SpinBox {
id: vField
from: 0; to: 100; editable: true
value: 51
Layout.fillWidth: true
Layout.columnSpan: 2
onValueModified: root.applyHSV(hField.value, sField.value, value)
}
}
// 底部按钮(像窗口)
RowLayout {
Layout.fillWidth: true
spacing: 10
Item { Layout.fillWidth: true }
Button {
text: qsTr("Cancel")
onClicked: { root.visible = false; root.rejected() }
}
Button {
text: qsTr("OK")
highlighted: true
onClicked: { root.visible = false; root.accepted(root.color) }
}
}
}
}
// 每次 HSV 改变UI 同步
onHChanged: if (!_lock) syncFromHSV()
onSChanged: if (!_lock) syncFromHSV()
onVChanged: if (!_lock) syncFromHSV()
onAChanged: if (!_lock) syncFromHSV()
}

View File

@@ -469,8 +469,7 @@ Rectangle {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: { onClicked: {
lowColorDialog.selectedColor = Backend.colorLow lowColorDialog.openWith(Backend.colorLow)
lowColorDialog.open()
} }
} }
} }
@@ -478,8 +477,7 @@ Rectangle {
text: root.tr("选择") text: root.tr("选择")
Layout.fillWidth: true Layout.fillWidth: true
onClicked: { onClicked: {
lowColorDialog.selectedColor = Backend.colorLow lowColorDialog.openWith(Backend.colorLow)
lowColorDialog.open()
} }
} }
} }
@@ -503,8 +501,7 @@ Rectangle {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: { onClicked: {
midColorDialog.selectedColor = Backend.colorMid midColorDialog.openWith(Backend.colorMid)
midColorDialog.open()
} }
} }
} }
@@ -512,8 +509,7 @@ Rectangle {
text: root.tr("选择") text: root.tr("选择")
Layout.fillWidth: true Layout.fillWidth: true
onClicked: { onClicked: {
midColorDialog.selectedColor = Backend.colorMid midColorDialog.openWith(Backend.colorMid)
midColorDialog.open()
} }
} }
} }
@@ -537,8 +533,7 @@ Rectangle {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: { onClicked: {
highColorDialog.selectedColor = Backend.colorHigh highColorDialog.openWith(Backend.colorHigh)
highColorDialog.open()
} }
} }
} }
@@ -546,8 +541,7 @@ Rectangle {
text: root.tr("选择") text: root.tr("选择")
Layout.fillWidth: true Layout.fillWidth: true
onClicked: { onClicked: {
highColorDialog.selectedColor = Backend.colorHigh highColorDialog.openWith(Backend.colorHigh)
highColorDialog.open()
} }
} }
} }
@@ -596,22 +590,22 @@ Rectangle {
} }
} }
ColorDialog { ColorPickerDialog {
id: lowColorDialog id: lowColorDialog
title: root.tr("选择低色") title: root.tr("选择低色")
onAccepted: Backend.colorLow = selectedColor onAccepted: Backend.colorLow = c
} }
ColorDialog { ColorPickerDialog {
id: midColorDialog id: midColorDialog
title: root.tr("选择中色") title: root.tr("选择中色")
onAccepted: Backend.colorMid = selectedColor onAccepted: Backend.colorMid = c
} }
ColorDialog { ColorPickerDialog {
id: highColorDialog id: highColorDialog
title: root.tr("选择高色") title: root.tr("选择高色")
onAccepted: Backend.colorHigh = selectedColor onAccepted: Backend.colorHigh = c
} }
SaveAsExportDialog { SaveAsExportDialog {

View File

@@ -11,6 +11,7 @@
<file>qml/content/LeftPanel.qml</file> <file>qml/content/LeftPanel.qml</file>
<file>qml/content/RightPanel.qml</file> <file>qml/content/RightPanel.qml</file>
<file>qml/content/CollapsiblePanel.qml</file> <file>qml/content/CollapsiblePanel.qml</file>
<file>qml/content/ColorPickerDialog.qml</file>
<file>shaders/dots.frag</file> <file>shaders/dots.frag</file>
<file>shaders/dots.vert</file> <file>shaders/dots.vert</file>
<file>shaders/bg.frag</file> <file>shaders/bg.frag</file>

49
src/globalhelper.h Normal file
View File

@@ -0,0 +1,49 @@
//
// Created by Lenn on 2026/1/20.
//
#ifndef TACTILEIPC3D_GLOBALHELPER_H
#define TACTILEIPC3D_GLOBALHELPER_H
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
class GlobalHelper {
public:
const std::string fuck = "fuck you lsp!!!";
static GlobalHelper* Instance() {
static GlobalHelper ins;
return &ins;
}
static void transToMultiMatrix(const cv::Mat& raw_data, float min_threshold,
float max_range, cv::Size display_res, cv::Mat& out_image) {
CV_Assert(raw_data.type() == CV_32F);
cv::Mat raw = raw_data.clone();
raw.setTo(0.0f, raw < min_threshold);
double maxVal = 0.0;
cv::minMaxLoc(raw, nullptr, &maxVal);
if (maxVal > 0.0) {
cv::GaussianBlur(raw, raw, cv::Size(3, 3), 0.8, 0.8, cv::BORDER_DEFAULT);
}
cv::Mat norm_data = raw / max_range;
cv::min(norm_data, 1.0f, norm_data);
cv::max(norm_data, 0.0f, norm_data);
cv::pow(norm_data, 0.7, norm_data);
cv::Mat smoothed;
cv::resize(norm_data, smoothed, display_res, 0.0, 0.0, cv::INTER_CUBIC);
cv::GaussianBlur(smoothed, smoothed, cv::Size(31, 31), 0.0, 0.0, cv::BORDER_DEFAULT);
cv::transpose(smoothed, out_image);
}
private:
GlobalHelper() {}
};
#endif //TACTILEIPC3D_GLOBALHELPER_H