修复色域拖拽回弹、网格 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
+1138 -181

No files matched your search

+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)
{