修复色域拖拽回弹、网格 GX 拖拽重排、均值滤波多阶与逐信号条目

1. 自定义颜色面板:拖动时鼠标移出控件不再"弹回去"
   - 根因是选点位置每帧都从 RGB 反推:色相 1 与 0 是同一个颜色(红),拖到色相条底部
     下一帧反推回 0,指示条弹到顶部;拖到色域方块右下角之外时明度为 0(黑色),
     黑色的饱和度无定义,反推得 0,选点弹到最左边。
   - 现在把"选点位置(饱和度/明度)与色相"作为独立状态保存,只由对应控件与外部改色维护;
     越界一律夹取到最近边界,因此鼠标离开控件后选点停在边界上。
   - 统一色域方块、色相条、预览色块与色号框的圆角;RGB 滑块适当加粗;
     色域选点去掉外圈深色描边(只保留白色描边),色相指示器改为白色描边的圆角长方条、
     底色取该位置的颜色;色相条改用分段线性渐变绘制。

2. 网格 GX 编号支持拖拽重排
   - 按住网格图例左侧的 GX 徽标即可上下拖动,整块网格(连同波形与标尺)跟随鼠标移动;
     途经的网格保持自身大小、整体让出一格,目标槽位有吸附高亮提示。
   - 松开后真正重排并从下到上/从上到下重新编号,信号列表徽标、图例徽标、菜单与浮动面板
     中所有 GX 编号同步更新;手动排布后切换为自定义划分,后续重建网格不会打乱次序。
   - 拖动期间复用静态场景缓存,按网格条带平移贴图,因此大文件下依然流畅。

3. 均值滤波升级为"均值点数 X + 阶数 N"
   - 原"均值点数 N"更名"均值点数 X",新增"阶数 N":预览曲线以 X 点均值滤波的结果为蓝本,
     再重复同样的滤波共 N 阶(1 阶即与原来的单次滤波逐位一致)。
   - 浮动面板改为逐信号条目:第一排是信号名与复选框,第二排是均值点数与阶数输入;
     只有勾选的信号才绘制预览曲线,未勾选时不显示第二排;启用功能时默认只勾选第一个信号。
   - 参数按信号保存(同一网格内不同信号可用不同参数),「构建为新信号」按各自参数构建。
This commit is contained in:
DeepSeek V4.1 Flash committed 2026-10-10 15:33:45 +08:00
1 parent 2d029fb09b
commit bc8db72f18
8 files changed
+1133 -176

No files matched your search

+5 -3
View File
@@ -250,14 +250,16 @@ namespace jdi
bool ApplyPendingMeanFilterBuild();
/**
* @brief 把一条均值滤波派生信号插入数据层并同步各视图。
* @brief 把一条多阶均值滤波派生信号插入数据层并同步各视图。
* @param [IN] fileIndex 原信号所属文件下标。
* @param [IN] signalIndex 原信号下标。
* @param [IN] radius 均值滤波半径 N。
* @param [IN] points 均值点数 X(滑平半径)。
* @param [IN] order 阶数 N(迭代次数)。
* @param [IN] makeVisible 是否在当前标签页中勾选新信号。
* @return 构建并插入成功返回 true。
*/
bool BuildMeanFilterSignal(int fileIndex, int signalIndex, int radius, bool makeVisible);
bool BuildMeanFilterSignal(int fileIndex, int signalIndex, int points, int order,
bool makeVisible);
/**
* @brief 弹出文件选择对话框并加载所选文件。
+29 -25
View File
@@ -1436,7 +1436,8 @@ namespace jdi
return true;
}
bool Application::BuildMeanFilterSignal(int fileIndex, int signalIndex, int radius, bool makeVisible)
bool Application::BuildMeanFilterSignal(int fileIndex, int signalIndex, int points, int order,
bool makeVisible)
{
const DataFile* file = m_dataSet.FileAt(fileIndex);
if (file == nullptr || signalIndex < 0 || signalIndex >= static_cast<int>(file->signals.size()))
@@ -1446,13 +1447,18 @@ namespace jdi
const SignalData& source = file->signals[static_cast<std::size_t>(signalIndex)];
const std::wstring baseName = source.Title();
// 1. 计算均值滤波采样值(与预览曲线共用同一实现)。
// 1. 计算多阶均值滤波采样值(与预览曲线共用同一实现)。
std::vector<float> filtered;
ComputeMovingAverage(source.values, radius, filtered);
ComputeMovingAverageIterated(source.values, points, order, filtered);
// 2. 组装新信号:名称形如「<原信号名称> (N点均值)」。
// 2. 组装新信号:名称形如「<原信号名称> (N点均值x阶)」。
SignalData derived;
derived.name = baseName + L" (" + std::to_wstring(radius) + L"点均值)";
derived.name = baseName + L" (" + std::to_wstring(points) + L"点均值";
if (order > 1)
{
derived.name += L"×" + std::to_wstring(order) + L"阶";
}
derived.name += L")";
derived.rawHeader = derived.name;
derived.unit = source.unit;
derived.values.assign(filtered.begin(), filtered.end());
@@ -1490,8 +1496,8 @@ namespace jdi
bool Application::ApplyPendingMeanFilterBuild()
{
int laneIndex = -1;
int radius = 0;
if (!m_plot.TakePendingMeanFilterBuild(laneIndex, radius))
std::vector<PlotView::MeanFilterBuild> items;
if (!m_plot.TakePendingMeanFilterBuild(laneIndex, items))
{
return false;
}
@@ -1501,21 +1507,10 @@ namespace jdi
return false;
}
// 先收集该网格内的全部可见信号(插入过程中信号下标会变化,因此先取快照)。
std::vector<SignalRef> targets;
for (const SignalRef& ref : ActiveTab().Lanes()[static_cast<std::size_t>(laneIndex)].signals)
{
if (m_dataSet.Resolve(ref) == nullptr || !ActiveTab().IsVisible(ref))
{
continue;
}
targets.push_back(ref);
}
// **同一文件内必须按下标降序处理**:每次插入只让插入点之后的信号下标 +1,
// 降序处理可保证尚未处理的信号下标始终有效(升序会把新插入的信号当成原信号)。
std::sort(targets.begin(), targets.end(),
[](const SignalRef& a, const SignalRef& b)
std::sort(items.begin(), items.end(),
[](const PlotView::MeanFilterBuild& a, const PlotView::MeanFilterBuild& b)
{
if (a.fileIndex != b.fileIndex)
{
@@ -1525,19 +1520,28 @@ namespace jdi
});
int built = 0;
for (const SignalRef& ref : targets)
int lastPoints = 0;
int lastOrder = 1;
for (const PlotView::MeanFilterBuild& item : items)
{
if (BuildMeanFilterSignal(ref.fileIndex, ref.signalIndex, radius, true))
if (BuildMeanFilterSignal(item.fileIndex, item.signalIndex, item.points, item.order, true))
{
++built;
lastPoints = item.points;
lastOrder = item.order;
}
}
if (built > 0)
{
SetStatusMessage(L"已构建 " + std::to_wstring(built) + L" 条 " + std::to_wstring(radius) +
L"点均值滤波信号。",
false);
std::wstring text = L"已构建 " + std::to_wstring(built) + L" 条 " +
std::to_wstring(lastPoints) + L"点均值";
if (lastOrder > 1)
{
text += L"×" + std::to_wstring(lastOrder) + L"阶";
}
text += L"滤波信号。";
SetStatusMessage(text, false);
}
return built > 0;
}
+145 -21
View File
@@ -29,7 +29,8 @@ namespace jdi
CURSOR_SPAN_MOVE, /**< 拖拽双光标差值标签:两条光标整体平移,间隔不变。 */
MOVE_SIGNAL, /**< 拖拽信号曲线到其它网格。 */
EDGE_MOVE, /**< 拖动「边沿计数」的横向光标(上下移动幅值)。 */
EDGE_PANEL_MOVE /**< 拖动边沿计数浮动面板。 */
EDGE_PANEL_MOVE, /**< 拖动边沿计数浮动面板。 */
LANE_REORDER /**< 拖动网格图例中的 GX 编号徽标:上下调整网格次序。 */
};
/**
@@ -54,7 +55,8 @@ namespace jdi
LANE_Y_MIN, /**< 编辑某网格的 Y 轴最小值。 */
SIGNAL_NAME, /**< 编辑图例中某信号的显示名称。 */
EDGE_CENTER_SMOOTH, /**< 编辑「中心轴边沿计数」的平滑点数 N。 */
MEAN_FILTER_RADIUS /**< 编辑「均值滤波」的均值点数 N。 */
MEAN_FILTER_POINTS, /**< 编辑「均值滤波」的均值点数 X。 */
MEAN_FILTER_ORDER /**< 编辑「均值滤波」的阶数 N。 */
};
/**
@@ -492,17 +494,28 @@ namespace jdi
*/
bool ToggleLaneMeanFilter(int laneIndex);
/**
* @brief 「均值滤波 → 构建为新信号」的单个请求(一个被勾选的信号 + 自己的参数)。
*/
struct MeanFilterBuild
{
int fileIndex = -1; /**< 信号所属文件下标。 */
int signalIndex = -1; /**< 文件内信号下标。 */
int points = 5; /**< 均值点数 X(滑平半径)。 */
int order = 1; /**< 阶数 N(迭代次数)。 */
};
/**
* @brief 取出「构建为新信号」请求(由面板底部按钮触发)。
*
* 请求只描述「哪一网格、用多大的 N」,真正的信号构建与插入由宿主(Application)
* 完成——数据层属于 DataSet,绘图区不能直接修改它。
* 请求只描述「哪一网格、哪些被勾选的信号及其参数」,真正的信号构建与插入由宿主
* (Application)完成——数据层属于 DataSet,绘图区不能直接修改它。
*
* @param [OUT] laneIndex 触发该请求的网格下标。
* @param [OUT] radius 均值滤波半径 N。
* @param [OUT] items 需要构建的信号及其参数(按信号在网格内的顺序)。
* @return 存在待处理请求返回 true(取出后清空)。
*/
bool TakePendingMeanFilterBuild(int& laneIndex, int& radius);
bool TakePendingMeanFilterBuild(int& laneIndex, std::vector<MeanFilterBuild>& items);
/**
* @brief 直接把文本解析为网格 Y 轴上限/下限并应用(供菜单内联输入框提交使用)。
@@ -938,6 +951,17 @@ namespace jdi
gd::RectF edit; /**< 面板底部编辑行(无编辑内容时宽度为 0)。 */
gd::RectF button; /**< 面板底部按钮(无按钮时宽度为 0)。 */
int rows = 0; /**< 统计行数(每条可见信号一行)。 */
/**
* @brief 均值滤波面板的逐信号条目矩形(仅 MEAN 面板使用)。
*
* 索引与 `rowRefs` 一一对应(即网格内可见信号的顺序):
* `rowChecks` 为第一排右侧的复选框;`rowPoints` / `rowOrder` 为第二排的两个输入框
* (未勾选的条目两个矩形均为空)。
*/
std::vector<gd::RectF> rowChecks;
std::vector<gd::RectF> rowPoints;
std::vector<gd::RectF> rowOrder;
std::vector<SignalRef> rowRefs;
};
/**
@@ -949,7 +973,50 @@ namespace jdi
EdgePanelLayout EdgePanelLayoutFor(int laneIndex, EdgePanelKind kind) const;
/**
* @brief 获取中心平均值曲线(每个数据点 = 自身与左右各 N 点的平均值),结果带缓存。
* @brief 获取多阶(迭代)均值滤波曲线,结果带缓存。
*
* 语义:以「均值点数 X」作滑平半径,对输入重复做 `order` 次滑动平均;
* `order == 1` 时与 `CenterCurveFor` 完全一致。
*
* @param [IN] signal 目标信号。
* @param [IN] points 均值点数 X(滑平半径)。
* @param [IN] order 阶数 N(迭代次数,至少 1)。
* @return 曲线数值数组(长度与 `signal.values` 相同;缺失处为 NaN)。
*/
const std::vector<float>& MeanFilterCurveFor(const SignalData& signal, int points,
int order) const;
/**
* @brief 取某个时刻的多阶均值滤波曲线取值(用于纵向光标的取值标注)。
* @param [IN] file 信号所属文件。
* @param [IN] signal 目标信号。
* @param [IN] points 均值点数 X。
* @param [IN] order 阶数 N。
* @param [IN] time 采样时刻。
* @param [OUT] value 输出取值。
* @return 取到有效值返回 true。
*/
bool MeanFilterValueAt(const DataFile& file, const SignalData& signal, int points, int order,
double time, double& value) const;
/**
* @brief 切换某个信号在「均值滤波」面板中的预览勾选状态。
* @param [IN] laneIndex 网格下标。
* @param [IN] ref 目标信号。
* @return 切换后的勾选状态。
*/
bool ToggleLaneMeanFilterSignal(int laneIndex, const SignalRef& ref);
/**
* @brief 查询某个信号在「均值滤波」面板中的预览勾选状态。
* @param [IN] laneIndex 网格下标。
* @param [IN] ref 目标信号。
* @return 已勾选返回 true。
*/
bool LaneMeanFilterSignalEnabled(int laneIndex, const SignalRef& ref) const;
/**
* @brief 获取多阶(迭代)均值滤波曲线,结果带缓存。
* @param [IN] signal 目标信号。
* @param [IN] smooth 平滑半径 N(0 表示不平滑)。
* @return 曲线数值数组(长度与 `signal.values` 相同;缺失处为 NaN)。
@@ -994,26 +1061,25 @@ namespace jdi
int smooth) const;
/**
* @brief 绘制某条信号的均值滤波曲线(与中心平均值曲线同一算法与同一配色派生规则)。
* @brief 绘制一段派生曲线(中心平均值 / 多阶均值滤波共用)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] resolved 信号视图。
* @param [IN] lane 网格绘制信息。
* @param [IN] radius 均值滤波半径 N。
* @param [IN] curve 曲线数值数组(与信号采样等长,缺失处为 NaN)。
*/
void PaintMeanFilterCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
int radius) const;
void PaintDerivedCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
const std::vector<float>& curve) const;
/**
* @brief 取某个时刻的均值滤波曲线取值(用于纵向光标的取值标注)。
* @param [IN] file 信号所属文件。
* @param [IN] signal 目标信号。
* @param [IN] radius 均值滤波半径 N。
* @param [IN] time 采样时刻。
* @param [OUT] value 输出取值。
* @return 取到有效值返回 true。
* @brief 绘制某条信号的多阶均值滤波曲线(与中心平均值曲线同一配色派生规则)。
* @param [IN,OUT] g 绘图上下文。
* @param [IN] resolved 信号视图。
* @param [IN] lane 网格绘制信息。
* @param [IN] points 均值点数 X(滑平半径)。
* @param [IN] order 阶数 N(迭代次数)。
*/
bool MeanFilterValueAt(const DataFile& file, const SignalData& signal, int radius, double time,
double& value) const;
void PaintMeanFilterCurve(gd::Graphics& g, const ResolvedSignal& resolved, const LaneVisual& lane,
int points, int order) const;
/**
* @brief 数据点标记的位置与"越界方向"(用于决定画圆点还是贴边三角)。
@@ -1216,6 +1282,12 @@ namespace jdi
PlotEditTarget m_editTarget = PlotEditTarget::NONE; /**< 当前内联输入目标。 */
TextEdit m_edit; /**< 输入框编辑模型(光标/选区/剪贴板)。 */
int m_editLane = -1; /**< 内联输入所作用的网格下标。 */
SignalRef m_editMeanRef; /**< 均值滤波参数编辑所作用的信号。 */
/** 网格拖拽重排状态(拖动图例中的 GX 徽标)。 */
int m_laneDragFrom = -1; /**< 被拖拽的网格下标,-1 表示未拖拽。 */
int m_laneDragTarget = -1; /**< 当前插入位置(0..网格数)。 */
int m_laneDragMouseY = 0; /**< 最近一次鼠标纵坐标(被拖网格自由跟随)。 */
float m_laneDragGrabOffset = 0.0f; /**< 抓取时鼠标相对网格顶边的偏移。 */
SignalRef m_editSignal; /**< 内联输入所作用的信号(图例改名)。 */
gd::RectF m_editFieldRect; /**< 本次编辑输入框的矩形(用于点外提交判定)。 */
SignalRef m_pendingRenameRef; /**< 待宿主窗口应用的信号重命名引用。 */
@@ -1223,7 +1295,7 @@ namespace jdi
bool m_pendingRename = false; /**< 是否存在待应用的重命名请求。 */
int m_pendingMeanFilterLane = -1; /**< 待构建的均值滤波信号所属网格下标(-1 表示无请求)。 */
int m_pendingMeanFilterRadius = 0; /**< 待构建的均值滤波半径 N。 */
std::vector<MeanFilterBuild> m_pendingMeanFilterItems; /**< 待构建的信号及其参数。 */
/**
* @brief 实验性功能浮动面板的 ✕ 处于"已按下、等待抬起确认"状态。
@@ -1279,6 +1351,16 @@ namespace jdi
/** 中心平均值曲线缓存:**deque** 保证 push_back 后已有元素引用仍然有效。 */
mutable std::deque<CenterCurveEntry> m_centerCurveCache;
/** 多阶均值滤波曲线缓存项(按「信号指针 + 均值点数 + 阶数」标识)。 */
struct MeanCurveEntry
{
const SignalData* signal = nullptr;
int points = 5;
int order = 1;
std::vector<float> values; /**< 与 signal->values 等长;缺失处为 NaN。 */
};
mutable std::deque<MeanCurveEntry> m_meanCurveCache;
/** 中心轴边沿统计缓存项(增量统计机制与 `EdgeCountEntry` 完全一致)。 */
struct EdgeCenterCountEntry
{
@@ -1497,6 +1579,48 @@ namespace jdi
void PaintSignalHalo(gd::Graphics& g, const ResolvedSignal& resolved,
const std::vector<gd::PointF>& points, const std::vector<std::size_t>& breaks);
/**
* @brief 计算某网格图例中 GX 编号徽标的矩形(绘制与命中测试共用,保证一致)。
* @param [IN] laneIndex 网格下标。
* @return 徽标矩形;网格不可见时返回空矩形。
*/
gd::RectF LaneBadgeRect(int laneIndex) const;
/**
* @brief 查询坐标命中的网格编号徽标(用于发起网格次序拖拽)。
* @param [IN] x 客户区横坐标。
* @param [IN] y 客户区纵坐标。
* @return 命中的网格下标,未命中返回 -1。
*/
int LaneBadgeAtPoint(int x, int y) const;
/**
* @brief 网格拖拽重排时某条网格的显示位移(像素)。
*
* 被拖拽的网格自由跟随鼠标;它路径上的其它网格整体上/下让位一格。
* @param [IN] laneIndex 网格下标。
* @return 纵向位移。
*/
float LaneDragOffset(int laneIndex) const;
/**
* @brief 网格拖拽重排:按鼠标位置计算插入位置(0..网格数)。
* @param [IN] mouseY 鼠标客户区纵坐标。
* @return 插入位置。
*/
int LaneDragTargetAt(int mouseY) const;
/**
* @brief 绘制网格拖拽重排过程中的内容(逐网格按位移贴图 + 目标位置高亮)。
* @param [IN,OUT] g 绘图上下文。
*/
void PaintLaneReorder(gd::Graphics& g);
/**
* @brief 结束网格拖拽重排:若目标位置与原位置不同则真正重排并同步编号。
*/
void FinishLaneReorder();
/**
* @brief 确保离屏缓存位图存在且尺寸匹配。
* @param [IN] width 位图宽度(像素)。
File diff suppressed because it is too large. Load diff
+10 -2
View File
@@ -254,9 +254,9 @@ namespace jdi
double CurrentHue() const;
/**
* @brief RGB 分量变化后刷新记忆色相(仅在饱和度足够大时更新)。
* @brief RGB 分量变化后刷新派生的色相与饱和度/明度选择状态。
*/
void RefreshHueFromChannels();
void RefreshSelectionFromChannels();
/**
* @brief 销毁色域方块缓存位图(尺寸或色相变化时重建)。
@@ -281,6 +281,14 @@ namespace jdi
DragTarget m_drag = DragTarget::NONE; /**< 正在拖拽的控件。 */
std::array<double, 3> m_channels = {0.0, 0.0, 0.0}; /**< RGB 分量(0-1,唯一数据源)。 */
double m_hue = 0.0; /**< 记忆色相(0-1);灰度/黑色时保留上次取值。 */
/**
* @brief 色域方块上的选点位置(饱和度/明度,均为 0-1)。
*
* 必须**与 RGB 分开保存**:黑色/灰色的饱和度无定义,若每帧都从 RGB 反推,
* 把选点拖到方块右边界之外(明度归零)时选点会瞬间弹回最左侧。
*/
double m_sat = 0.0;
double m_val = 0.0;
gd::RectF m_rect; /**< 面板整体矩形。 */
gd::RectF m_titleRect; /**< 标题区域。 */
+63 -4
View File
@@ -355,6 +355,19 @@ namespace jdi
*/
struct Lane
{
/**
* @brief 「均值滤波」中某个信号自己的参数。
*
* 仅参与列表中的信号才会被勾选预览;参数按信号保存,
* 因此同一条网格内不同信号可以使用不同的均值点数与阶数。
*/
struct MeanFilterItem
{
SignalRef ref; /**< 目标信号。 */
int points = 5; /**< 均值点数 X(滑平半径,窗口 = 2X+1)。 */
int order = 1; /**< 阶数 N(重复滤波次数,至少 1)。 */
};
std::wstring name; /**< 网格名称。 */
std::vector<SignalRef> signals; /**< 该网格包含的信号(按加入顺序)。 */
bool autoRange = true; /**< 是否依据信号取值范围自动计算 Y 轴。 */
@@ -416,14 +429,33 @@ namespace jdi
/**
* @brief 「实验性功能 → 均值滤波」开关。
*
* 开启后为本网格内每条信号额外绘制一条 N 点滑动平均曲线(算法与「边沿计数(中心轴)」
* 的中心平均值曲线完全一致),并在浮动面板中提供参数 N 与「构建为新信号」按钮。
* 开启后为本网格内**被勾选**的信号额外绘制一条多阶滑动平均曲线(按各自的均值点数 X
* 重复 N 阶),并在浮动面板中提供每个信号自己的参数与「构建为新信号」按钮。
* 与其它实验性功能一样属于网格自身状态,重建网格时按信号继承。
*/
bool meanFilter = false;
/** 均值滤波半径 N(左右各取 N 个数据点,窗口大小 2N+1)。 */
int meanFilterRadius = 5;
/**
* @brief 参与均值滤波预览的信号及其参数(只包含被勾选的信号)。
*
* 顺序与信号在网格内的顺序不一定一致,绘制时按网格内信号顺序派发。
*/
std::vector<MeanFilterItem> meanFilterItems;
/** 取某个信号的均值滤波参数(未勾选时返回空指针)。 */
const MeanFilterItem* FindMeanFilterItem(const SignalRef& ref) const;
/** 取得或创建某信号的均值滤波参数(用于勾选)。 */
MeanFilterItem& EnsureMeanFilterItem(const SignalRef& ref);
/** 取消某信号的均值滤波勾选。 */
void RemoveMeanFilterItem(const SignalRef& ref);
/** 取某信号的均值滤波勾选状态。 */
bool MeanFilterEnabled(const SignalRef& ref) const
{
return FindMeanFilterItem(ref) != nullptr;
}
/** 均值滤波浮动面板是否展开。 */
bool meanFilterPanelOpen = true;
@@ -462,6 +494,20 @@ namespace jdi
*/
void ComputeMovingAverage(const std::vector<double>& values, int radius, std::vector<float>& out);
/**
* @brief 计算多阶(迭代)滑动平均:把 N 点滑动平均重复应用 `order` 次。
*
* 每次迭代以上一次的结果为输入(NaN 不参与平均),因此阶数越高曲线越平滑。
* `order <= 1` 时与 `ComputeMovingAverage` 完全一致。
*
* @param [IN] values 原始采样值。
* @param [IN] radius 滑平半径 N(0 表示原样返回)。
* @param [IN] order 阶数(迭代次数,至少为 1)。
* @param [OUT] out 输出数组(长度与 values 相同)。
*/
void ComputeMovingAverageIterated(const std::vector<double>& values, int radius, int order,
std::vector<float>& out);
/**
* @brief 在指定文件的时间轴上查找与给定时刻最接近的采样点。
* @param [IN] file 目标文件。
@@ -663,6 +709,19 @@ namespace jdi
*/
bool MoveLaneOfSignalBelow(const SignalRef& moved, const SignalRef& anchor);
/**
* @brief 把某个网格整体移动到新的位置(用于拖拽 GX 编号调整网格次序)。
*
* `target` 是**插入位置**(0..laneCount):0 表示移到最上方,laneCount 表示移到最下方。
* 移动后会切换到自定义划分策略并按新位置重新编号全部网格,
* 因此所有以 GX 编号关联的界面(信号列表徽标、图例徽标、菜单、浮动面板)自动同步。
*
* @param [IN] from 被移动的网格下标。
* @param [IN] target 插入位置(0..网格数)。
* @return 实际发生了移动返回 true。
*/
bool MoveLane(int from, int target);
/**
* @brief 按当前位置统一重命名全部网格(GX 编号由位置推导,须保持一致)。
*/
+40 -29
View File
@@ -23,8 +23,10 @@ namespace jdi
const float kLabelWidth = 16.0f; /**< 通道标签宽度。 */
const float kLabelGap = 6.0f; /**< 标签与滑块间距。 */
const float kValueWidth = 40.0f; /**< 数值文本宽度。 */
const float kTrackHeight = 6.0f; /**< 滑块轨道高度。 */
const float kTrackHeight = 8.0f; /**< 滑块轨道高度。 */
const float kCorner = 6.0f; /**< 面板圆角半径。 */
/** 色域方块 / 色相条 / 预览色块 / 色号框共用同一圆角(视觉上统一)。 */
const float kSwatchCorner = 3.0f;
const float kGamutSize = 112.0f; /**< 色域方块边长。 */
const float kHueWidth = 14.0f; /**< 色相条宽度。 */
const float kGamutGap = 10.0f; /**< 色域方块与色相条间距。 */
@@ -303,8 +305,8 @@ namespace jdi
m_draggingSlider = -1;
m_hoverSlider = -1;
m_drag = DragTarget::NONE;
// 记住初始色相:之后即使把饱和度拖到 0(灰色)色相也不会丢失。
m_hue = ToHsv(m_channels).h;
// 记住初始色相与选点位置:之后即使把明度拖到 0(黑色)色相/饱和度也不会丢失。
RefreshSelectionFromChannels();
InvalidateGamutCache();
BuildLayout(bounds, anchor);
@@ -330,19 +332,26 @@ namespace jdi
double ColorPickerPopup::CurrentHue() const
{
const Hsv hsv = ToHsv(m_channels);
// 灰度/黑色时 HSV 的色相没有定义:沿用记忆色相,避免拖动 RGB 滑块时色相跳变、
// 也保证"饱和度归零后仍能靠色相条换回颜色"。
return (hsv.s > 1.0e-4) ? hsv.h : m_hue;
// 直接返回保存的色相选择值(不再从 RGB 反推):
// 色相 1 与 0 是同一个颜色(红),反推会把它们归一到 0,
// 表现为"把滑块拖到最下面却弹回最上面"。
// `RefreshSelectionFromChannels` 会在改色(RGB 滑块 / 色号)后同步这个值。
return m_hue;
}
void ColorPickerPopup::RefreshHueFromChannels()
void ColorPickerPopup::RefreshSelectionFromChannels()
{
const Hsv hsv = ToHsv(m_channels);
// 饱和度足够大时才认色相(灰度时色相无定义,沿用上次取值)。
if (hsv.s > 1.0e-4)
{
m_hue = hsv.h;
}
// 饱和度/明度总是可以从 RGB 反推(明度=最大值,饱和度=max>0 ? delta/max : 0)。
// 注意这是"由 RGB 派生"的路径,只用于外部改色(滑块/色号)后的同步;
// 直接用色域方块时不会走这里,以免黑色把饱和度重置为 0。
m_sat = hsv.s;
m_val = hsv.v;
}
void ColorPickerPopup::InvalidateGamutCache()
@@ -402,16 +411,18 @@ namespace jdi
{
return;
}
// 越出方块时夹取到最近边界(而不是丢失位置):鼠标拖到方块之外时选点
// 会停在对应角落,与鼠标相对方块的方位一致。
const double saturation = Clamp01((x - m_gamutRect.X) / m_gamutRect.Width);
const double value = 1.0 - Clamp01((y - m_gamutRect.Y) / m_gamutRect.Height);
const std::array<double, 3> rgb = FromHsv(CurrentHue(), saturation, value);
if (std::fabs(rgb[0] - m_channels[0]) < 1.0e-6 && std::fabs(rgb[1] - m_channels[1]) < 1.0e-6 &&
std::fabs(rgb[2] - m_channels[2]) < 1.0e-6)
if (std::fabs(saturation - m_sat) < 1.0e-9 && std::fabs(value - m_val) < 1.0e-9)
{
return;
}
m_channels = rgb;
// 色相不变(明度归零时 FromHsv 会给出黑色,但记忆色相仍要保留)。
// 先记录选点位置,再推颜色:两者分开保存,明度为 0 时饱和度不会因此丢失。
m_sat = saturation;
m_val = value;
m_channels = FromHsv(CurrentHue(), m_sat, m_val);
m_changed = true;
}
@@ -421,16 +432,14 @@ namespace jdi
{
return;
}
// 越出色相条时同样夹取:拖到下方停在最下面(色相 1),不会因 0/1 等价而弹回顶部。
const double hue = Clamp01((y - m_hueRect.Y) / m_hueRect.Height);
const Hsv hsv = ToHsv(m_channels);
m_hue = hue;
const std::array<double, 3> rgb = FromHsv(m_hue, hsv.s, hsv.v);
if (std::fabs(rgb[0] - m_channels[0]) < 1.0e-6 && std::fabs(rgb[1] - m_channels[1]) < 1.0e-6 &&
std::fabs(rgb[2] - m_channels[2]) < 1.0e-6)
if (std::fabs(hue - m_hue) < 1.0e-9)
{
return;
}
m_channels = rgb;
m_hue = hue;
m_channels = FromHsv(m_hue, m_sat, m_val);
m_changed = true;
}
@@ -549,7 +558,7 @@ namespace jdi
m_channels[0] = static_cast<double>((value >> 16) & 0xFFu) / 255.0;
m_channels[1] = static_cast<double>((value >> 8) & 0xFFu) / 255.0;
m_channels[2] = static_cast<double>(value & 0xFFu) / 255.0;
RefreshHueFromChannels();
RefreshSelectionFromChannels();
m_changed = true;
}
}
@@ -581,7 +590,7 @@ namespace jdi
if (index >= 0 && index < 3)
{
m_channels[static_cast<std::size_t>(index)] = value / 255.0;
RefreshHueFromChannels();
RefreshSelectionFromChannels();
m_changed = true;
}
return true;
@@ -730,7 +739,7 @@ namespace jdi
return;
}
m_channels[slot] = ratio;
RefreshHueFromChannels();
RefreshSelectionFromChannels();
m_changed = true;
}
@@ -920,7 +929,7 @@ namespace jdi
{
g.DrawImage(m_gamutCache.get(), m_gamutRect);
}
UiDrawRoundRect(g, m_gamutRect, 2.0f * m_scale, m_theme->borderStrong, kGamutBorder);
UiDrawRoundRect(g, m_gamutRect, kSwatchCorner * m_scale, m_theme->border, kGamutBorder);
// ---- 色相条(纵向 0-1):6 段线性渐变拼出完整色相环 ----
{
@@ -945,12 +954,12 @@ namespace jdi
// 渐变端点会各自向两侧延伸半段,逐段微重叠地填充即可无缝拼接。
g.FillRectangle(&brush, m_hueRect.X, top, m_hueRect.Width, bottom - top);
}
UiDrawRoundRect(g, m_hueRect, 2.0f * m_scale, m_theme->border, kGamutBorder);
UiDrawRoundRect(g, m_hueRect, kSwatchCorner * m_scale, m_theme->border, kGamutBorder);
// ---- 色域方块上的选点:中心就是当前选中的颜色,外面只加一圈白色描边 ----
const Hsv hsv = ToHsv(m_channels);
const float markerX = m_gamutRect.X + static_cast<float>(hsv.s) * m_gamutRect.Width;
const float markerY = m_gamutRect.Y + static_cast<float>(1.0 - hsv.v) * m_gamutRect.Height;
// 位置直接用保存的选点状态(不再从 RGB 反推,否则黑色会让选点弹回左侧)。
const float markerX = m_gamutRect.X + static_cast<float>(m_sat) * m_gamutRect.Width;
const float markerY = m_gamutRect.Y + static_cast<float>(1.0 - m_val) * m_gamutRect.Height;
const float markerRadius = 5.5f * m_scale;
gd::SolidBrush markerBrush(ToGdiColor(CurrentColor()));
g.FillEllipse(&markerBrush, markerX - markerRadius, markerY - markerRadius,
@@ -960,6 +969,8 @@ namespace jdi
markerRadius * 2.0f, markerRadius * 2.0f);
// ---- 色相条上的指示器:白色描边的圆角长方条,底色就是该位置的颜色 ----
// 色相直接用保存值:色相 1 与 0 是同一个颜色(红),从 RGB 反推会归一到 0,
// 表现为"把滑块拖到最下面却弹回最上面"。
const double hue = CurrentHue();
const std::array<double, 3> hueRgb = FromHsv(hue, 1.0, 1.0);
const std::uint32_t hueColor =
@@ -999,8 +1010,8 @@ namespace jdi
// 预览与十六进制值。
const std::uint32_t color = CurrentColor();
UiFillRoundRect(g, m_previewRect, 3.0f * m_scale, color);
UiDrawRoundRect(g, m_previewRect, 3.0f * m_scale, m_theme->borderStrong, 1.0f);
UiFillRoundRect(g, m_previewRect, kSwatchCorner * m_scale, color);
UiDrawRoundRect(g, m_previewRect, kSwatchCorner * m_scale, m_theme->borderStrong, 1.0f);
if (m_editField == EditField::HEX)
{
+129 -4
View File
@@ -127,7 +127,7 @@ namespace jdi
double edgeCenterPanelX = -1.0;
double edgeCenterPanelY = -1.0;
bool meanFilter = false;
int meanFilterRadius = 5;
std::vector<Lane::MeanFilterItem> meanFilterItems;
bool meanFilterPanelOpen = true;
double meanFilterPanelX = -1.0;
double meanFilterPanelY = -1.0;
@@ -152,7 +152,7 @@ namespace jdi
carry.edgeCenterPanelX = lane.edgeCenterPanelX;
carry.edgeCenterPanelY = lane.edgeCenterPanelY;
carry.meanFilter = lane.meanFilter;
carry.meanFilterRadius = lane.meanFilterRadius;
carry.meanFilterItems = lane.meanFilterItems;
carry.meanFilterPanelOpen = lane.meanFilterPanelOpen;
carry.meanFilterPanelX = lane.meanFilterPanelX;
carry.meanFilterPanelY = lane.meanFilterPanelY;
@@ -188,7 +188,7 @@ namespace jdi
lane.edgeCenterPanelX = carry.edgeCenterPanelX;
lane.edgeCenterPanelY = carry.edgeCenterPanelY;
lane.meanFilter = carry.meanFilter;
lane.meanFilterRadius = carry.meanFilterRadius;
lane.meanFilterItems = carry.meanFilterItems;
lane.meanFilterPanelOpen = carry.meanFilterPanelOpen;
lane.meanFilterPanelX = carry.meanFilterPanelX;
lane.meanFilterPanelY = carry.meanFilterPanelY;
@@ -969,6 +969,67 @@ namespace jdi
}
}
void ComputeMovingAverageIterated(const std::vector<double>& values, int radius, int order,
std::vector<float>& out)
{
const int stages = std::max(1, order);
// 第 1 阶:从原始采样值出发。
ComputeMovingAverage(values, radius, out);
if (stages == 1)
{
return;
}
// 后续各阶以上一阶的结果为输入(double 中转,避免 float→double 的精度损失累积)。
std::vector<double> buffer(values.size(), std::numeric_limits<double>::quiet_NaN());
std::vector<float> stage;
for (int iteration = 1; iteration < stages; ++iteration)
{
for (std::size_t index = 0; index < out.size(); ++index)
{
buffer[index] = std::isfinite(out[index]) ? static_cast<double>(out[index])
: std::numeric_limits<double>::quiet_NaN();
}
ComputeMovingAverage(buffer, radius, stage);
out.swap(stage);
}
}
const Lane::MeanFilterItem* Lane::FindMeanFilterItem(const SignalRef& ref) const
{
for (const MeanFilterItem& item : meanFilterItems)
{
if (item.ref == ref)
{
return &item;
}
}
return nullptr;
}
Lane::MeanFilterItem& Lane::EnsureMeanFilterItem(const SignalRef& ref)
{
for (MeanFilterItem& item : meanFilterItems)
{
if (item.ref == ref)
{
return item;
}
}
MeanFilterItem item;
item.ref = ref;
meanFilterItems.push_back(item);
return meanFilterItems.back();
}
void Lane::RemoveMeanFilterItem(const SignalRef& ref)
{
meanFilterItems.erase(
std::remove_if(meanFilterItems.begin(), meanFilterItems.end(),
[&ref](const MeanFilterItem& item) { return item.ref == ref; }),
meanFilterItems.end());
}
int FindNearestSample(const DataFile& file, double time)
{
const std::size_t count = file.time.size();
@@ -1192,6 +1253,23 @@ namespace jdi
--ref.signalIndex;
}
}
// 均值滤波条目同样持有信号引用,必须一起删除/前移。
std::vector<Lane::MeanFilterItem>& items = lane.meanFilterItems;
items.erase(std::remove_if(items.begin(), items.end(),
[fileIndex, signalIndex](const Lane::MeanFilterItem& item)
{
return item.ref.fileIndex == fileIndex &&
item.ref.signalIndex == signalIndex;
}),
items.end());
for (Lane::MeanFilterItem& item : items)
{
if (item.ref.fileIndex == fileIndex && item.ref.signalIndex > signalIndex)
{
--item.ref.signalIndex;
}
}
}
// 被移除信号若独占了某个网格,该网格随之消失(与取消勾选的行为一致)。
m_lanes.erase(std::remove_if(m_lanes.begin(), m_lanes.end(),
@@ -1485,7 +1563,15 @@ namespace jdi
lane.edgeCenterPanelX = source.edgeCenterPanelX;
lane.edgeCenterPanelY = source.edgeCenterPanelY;
lane.meanFilter = source.meanFilter;
lane.meanFilterRadius = source.meanFilterRadius;
// 拆分后每个子网格只保留自己那个信号的均值滤波条目。
lane.meanFilterItems.clear();
for (const Lane::MeanFilterItem& item : source.meanFilterItems)
{
if (item.ref == ref)
{
lane.meanFilterItems.push_back(item);
}
}
lane.meanFilterPanelOpen = source.meanFilterPanelOpen;
lane.meanFilterPanelX = source.meanFilterPanelX;
lane.meanFilterPanelY = source.meanFilterPanelY;
@@ -1529,6 +1615,14 @@ namespace jdi
++ref.signalIndex;
}
}
// 均值滤波条目同样持有信号引用,必须一起后移。
for (Lane::MeanFilterItem& item : lane.meanFilterItems)
{
if (item.ref.fileIndex == fileIndex && item.ref.signalIndex >= insertPos)
{
++item.ref.signalIndex;
}
}
}
}
@@ -1560,6 +1654,37 @@ namespace jdi
return true;
}
bool TabConfig::MoveLane(int from, int target)
{
const int count = static_cast<int>(m_lanes.size());
if (from < 0 || from >= count || target < 0 || target > count)
{
return false;
}
// `target` 语义 = 「插入到该下标之前」;向后拖时扣除自身占位后再比较,
// 保证"拖到自己原来的位置"不会产生无意义的移动。
int destination = target;
if (destination > from)
{
--destination;
}
destination = std::max(0, std::min(destination, count - 1));
if (destination == from)
{
return false;
}
Lane moved = std::move(m_lanes[static_cast<std::size_t>(from)]);
m_lanes.erase(m_lanes.begin() + from);
m_lanes.insert(m_lanes.begin() + destination, std::move(moved));
// 手动排布 → 切换到自定义划分策略,避免后续重建网格时顺序被还原。
m_laneMode = LaneMode::CUSTOM;
RenameLanes();
return true;
}
void TabConfig::RenameLanes()
{
for (std::size_t index = 0; index < m_lanes.size(); ++index)