样式区「调整」行改为 系数/镜像/绝对值:系数二级面板(还原图标 + 整行输入框)、镜像二级面板(水平/垂直复选框);Y 轴自适应按变换后曲线
- 「调整」行三个按钮改为「系数」「镜像」「绝对值」(顺序即结算顺序:系数 → 绝对值 → 镜像)。 「系数」「镜像」点击后在按钮下沿展开二级面板(按下只登记、抬起命中同一按钮才展开, 与自定义颜色入口一致),「绝对值」仍是直接切换的按钮。 打开面板时主动清除样式区悬停态 —— 面板会接管鼠标事件,否则该按钮会一直保持"鼠标按住"般的高亮。 - 数据层:`SignalStyle::inverted` 拆成 `mirrorVertical`(Y=0 上下翻转)与 `mirrorHorizontal`(水平镜像 = 数据反序);新增 `Mirrored()`、 `SourceIndexFor`(水平镜像的遍历下标 → 源下标映射)、`TransformValueRange`(区间映射)。 `ApplyDrawTransform` 顺序固定为 系数 → 绝对值 → 垂直镜像;水平镜像在采样遍历层处理。 - 绘制链路:`BuildPolyline`(逐点描边与像素列包络两条路径、跨列边界插值、粗粒度分组层)、 派生预览曲线、光标取值读数(含 Δt)全部按同一变换取值;水平镜像时按源下标区间反序读取, 包络层仍复用粗粒度分组;折线缓存键、`SceneContentHash`、样式快照都带上两个镜像位。 - 系数二级面板:标题「系数调整」,标题栏最右端是一个可点击的「还原」自绘图标按钮 (悬停有高亮反馈,点击后系数重置为 1,面板保持打开);下面是一整行输入框 (不再显示「绘制系数」说明文字),数值至少显示两位小数、精度更高时按实际精度显示 (1 → 1.00、2.5 → 2.50、1.2345678 原样)。 - 镜像二级面板:两行带复选框的切换项「水平镜像」「垂直镜像」,点击后**面板保持打开** (`MenuItem::keepOpen`),勾选状态就地回写;任一项勾选时「镜像」按钮显示为激活态。 - Y 轴自适应修复:整段自适应(`TabConfig::ComputeLaneRange`)按区间映射(`TransformValueRange`) 计算,不再直接用原始 min/max;可见区间自适应(`AutoRangeLaneToView`)逐点套用绘制变换 (含水平镜像的源下标映射)。用户报告的"调整系数/镜像后自适应区域错误、仍按原始数据"由此修复。 - `PopupMenu`:新增 `MenuItemKind::HEADER_ACTION`(标题 + 右侧可点击图标,图标有悬停高亮)、 `MenuItem::keepOpen`(切换项点击后不关闭菜单)、`HeaderIconRect()`, 以及无说明文字的整行输入框(占满内容区)。 验证(离屏 harness,SW_HIDE、不动鼠标键盘屏幕): - [AL] 重写/扩充:变换顺序与区间映射(含水平镜像的源下标反序)、调整行三按钮的顺序/等宽/标签不裁切、 「系数」按住不弹出 / 移开取消 / 单击展开(标题 HEADER_ACTION、无标题输入框占满整行、初始 1.00、 两位小数与更高精度回显、提交后面板保持打开、折线缓存键携带系数)、 「还原」图标位置/悬停像素变化/点击后系数回到 1.00 且面板保持打开、 「镜像」面板两行复选框(初始未勾选、keepOpen、勾选后按钮激活、取消其一保留另一)、 打开面板后按钮不再停留高亮、系数/镜像按钮的激活填充与其它启用态切换按钮逐像素同色; `BuildPolyline` 顶点范围与变换后数据范围逐项对齐(默认/系数 2/绝对值/垂直镜像/三者叠加), 水平镜像用窄视窗逐顶点核对"取的是对面端点的采样"(8/8)+ 读数跟随; 以及 Y 轴自适应:整段/可见区间自适应在系数、绝对值、垂直镜像下均跟随变换后的曲线。 - 合计 594/594 通过;make clean && make all 零警告。
This commit is contained in:
1 parent
2f243dd557
commit
2b02518f85
8 files changed
+608
-110
No files matched your search
@@ -1838,9 +1838,10 @@ namespace jdi
|
||||
float laneHeight = 0.0f; /**< 网格高度。 */
|
||||
double laneYMin = 0.0; /**< 网格幅值下界。 */
|
||||
double laneYMax = 0.0; /**< 网格幅值上界。 */
|
||||
float gain = 1.0f; /**< 绘制前的系数增益。 */
|
||||
float gain = 1.0f; /**< 绘制前的系数。 */
|
||||
bool absolute = false; /**< 绘制前是否取绝对值。 */
|
||||
bool inverted = false; /**< 绘制前是否中心反转。 */
|
||||
bool mirrorVertical = false; /**< 绘制前是否按 Y=0 上下镜像。 */
|
||||
bool mirrorHorizontal = false; /**< 是否水平镜像(取值下标反序)。 */
|
||||
std::vector<gd::PointF> points; /**< 折线顶点。 */
|
||||
std::vector<std::size_t> breaks; /**< 每段折线在 points 中的起始下标。 */
|
||||
|
||||
@@ -2004,9 +2005,10 @@ namespace jdi
|
||||
float opacity = 1.0f; /**< 不透明度。 */
|
||||
float lineWidth = 1.0f; /**< 线宽。 */
|
||||
int mode = 0; /**< 绘制模式。 */
|
||||
float gain = 1.0f; /**< 系数增益。 */
|
||||
float gain = 1.0f; /**< 系数。 */
|
||||
bool absolute = false; /**< 是否取绝对值。 */
|
||||
bool inverted = false; /**< 是否中心反转。 */
|
||||
bool mirrorVertical = false; /**< 是否按 Y=0 上下镜像。 */
|
||||
bool mirrorHorizontal = false; /**< 是否水平镜像(数据反序)。 */
|
||||
|
||||
/** @brief 相等比较(用于判断"逐信号外观样式是否变化")。 */
|
||||
bool operator==(const SceneStyleEntry& other) const
|
||||
@@ -2014,7 +2016,8 @@ namespace jdi
|
||||
return color == other.color && opacity == other.opacity &&
|
||||
lineWidth == other.lineWidth && mode == other.mode &&
|
||||
gain == other.gain && absolute == other.absolute &&
|
||||
inverted == other.inverted;
|
||||
mirrorVertical == other.mirrorVertical &&
|
||||
mirrorHorizontal == other.mirrorHorizontal;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -205,13 +205,15 @@ namespace jdi
|
||||
* @param [IN] time 时间轴。
|
||||
* @param [IN] values 采样值。
|
||||
* @param [IN] count 可用采样点数量。
|
||||
* @param [IN] style 显示外观(水平镜像时按**源下标反序**取值)。
|
||||
* @param [IN] firstIndex 第一个时间不小于查询时刻的采样下标(相交线段为 firstIndex-1 → firstIndex)。
|
||||
* @param [IN] t 查询时刻。
|
||||
* @param [OUT] out 输出取值。
|
||||
* @return 折线覆盖该时刻且取到有效值返回 true。
|
||||
*/
|
||||
bool PolylineValueAt(const std::vector<double>& time, const std::vector<double>& values,
|
||||
std::size_t count, std::size_t firstIndex, double t, double& out)
|
||||
std::size_t count, const SignalStyle& style, std::size_t firstIndex,
|
||||
double t, double& out)
|
||||
{
|
||||
if (firstIndex == 0 || firstIndex >= count)
|
||||
{
|
||||
@@ -220,8 +222,9 @@ namespace jdi
|
||||
}
|
||||
const double t0 = time[firstIndex - 1];
|
||||
const double t1 = time[firstIndex];
|
||||
const double v0 = values[firstIndex - 1];
|
||||
const double v1 = values[firstIndex];
|
||||
// 水平镜像时画面上的相邻两点取源数组中头尾对调后的两个采样。
|
||||
const double v0 = values[SourceIndexFor(style, count, firstIndex - 1)];
|
||||
const double v1 = values[SourceIndexFor(style, count, firstIndex)];
|
||||
if (!std::isfinite(t0) || !std::isfinite(t1) || !std::isfinite(v0) || !std::isfinite(v1) ||
|
||||
!(t1 > t0))
|
||||
{
|
||||
@@ -3228,7 +3231,17 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const double value = signal->values[index];
|
||||
// ⚠️ 自适应必须基于**画出来的曲线**:套用与绘制完全相同的变换
|
||||
//(系数 → 绝对值 → 镜像),否则调整系数/镜像后自适应范围会按原始数据算错
|
||||
//(用户报告"自适应区域错误,看上去还是按照原始数据来的")。
|
||||
// 水平镜像时画面上的第 index 个位置取源下标 count-1-index 的值。
|
||||
const std::size_t sourceIndex = SourceIndexFor(*m_dataSet->Style(ref), count, index);
|
||||
const double raw = signal->values[sourceIndex];
|
||||
if (!std::isfinite(raw))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double value = ApplyDrawTransform(*m_dataSet->Style(ref), raw);
|
||||
if (std::isfinite(value))
|
||||
{
|
||||
low = std::min(low, value);
|
||||
@@ -5342,10 +5355,11 @@ namespace jdi
|
||||
const std::size_t sampleCount = std::min(resolved.file->time.size(), resolved.data->values.size());
|
||||
const double viewMin = Tab().ViewMin();
|
||||
const double viewMax = Tab().ViewMax();
|
||||
// 绘制前的数值变换(增益/绝对值/中心反转)会改变顶点,必须进缓存键。
|
||||
// 绘制前的数值变换(系数/绝对值/镜像)会改变顶点,必须进缓存键。
|
||||
const float gain = (resolved.style != nullptr) ? resolved.style->gain : 1.0f;
|
||||
const bool absolute = (resolved.style != nullptr) && resolved.style->absolute;
|
||||
const bool inverted = (resolved.style != nullptr) && resolved.style->inverted;
|
||||
const bool mirrorVertical = (resolved.style != nullptr) && resolved.style->mirrorVertical;
|
||||
const bool mirrorHorizontal = (resolved.style != nullptr) && resolved.style->mirrorHorizontal;
|
||||
|
||||
for (const PolylineCacheEntry& entry : m_polylineCache)
|
||||
{
|
||||
@@ -5354,7 +5368,7 @@ namespace jdi
|
||||
entry.plotWidth == m_plotRect.Width && entry.laneY == lane.rect.Y &&
|
||||
entry.laneHeight == lane.rect.Height && entry.laneYMin == lane.yMin &&
|
||||
entry.laneYMax == lane.yMax && entry.gain == gain && entry.absolute == absolute &&
|
||||
entry.inverted == inverted)
|
||||
entry.mirrorVertical == mirrorVertical && entry.mirrorHorizontal == mirrorHorizontal)
|
||||
{
|
||||
return &entry;
|
||||
}
|
||||
@@ -5381,7 +5395,8 @@ namespace jdi
|
||||
entry.laneYMax = lane.yMax;
|
||||
entry.gain = gain;
|
||||
entry.absolute = absolute;
|
||||
entry.inverted = inverted;
|
||||
entry.mirrorVertical = mirrorVertical;
|
||||
entry.mirrorHorizontal = mirrorHorizontal;
|
||||
|
||||
BuildPolyline(resolved, lane, entry.points, entry.breaks, entry.band, entry.bandBreaks,
|
||||
entry.bandMode, entry.bandSpiky);
|
||||
@@ -5584,11 +5599,16 @@ namespace jdi
|
||||
bool broken = false;
|
||||
|
||||
// (一)落在本列区间内的采样点。
|
||||
// 水平镜像时取的是**源下标**(区间同样反转),因此先把列区间映射到源下标区间。
|
||||
const std::size_t srcA = std::min(SourceIndexFor(*resolved.style, count, i0),
|
||||
SourceIndexFor(*resolved.style, count, (i1 > 0) ? (i1 - 1) : 0));
|
||||
const std::size_t srcB = std::max(SourceIndexFor(*resolved.style, count, i0),
|
||||
SourceIndexFor(*resolved.style, count, (i1 > 0) ? (i1 - 1) : 0));
|
||||
if (i1 > i0 && hasCoarse && (i1 - i0) >= 2 * coarseStride)
|
||||
{
|
||||
const std::size_t firstBucket = i0 / coarseStride;
|
||||
const std::size_t firstBucket = srcA / coarseStride;
|
||||
const std::size_t lastBucket =
|
||||
std::min((i1 - 1) / coarseStride, coarseMin.size() - 1);
|
||||
std::min(srcB / coarseStride, coarseMin.size() - 1);
|
||||
for (std::size_t bucket = firstBucket; bucket <= lastBucket; ++bucket)
|
||||
{
|
||||
const double bucketLow = coarseMin[bucket] * coarseScale;
|
||||
@@ -5613,7 +5633,7 @@ namespace jdi
|
||||
}
|
||||
else if (i1 > i0)
|
||||
{
|
||||
for (std::size_t index = i0; index < i1; ++index)
|
||||
for (std::size_t index = srcA; index <= srcB; ++index)
|
||||
{
|
||||
const double value = ApplyDrawTransform(*resolved.style, values[index]);
|
||||
if (!std::isfinite(value))
|
||||
@@ -5647,32 +5667,33 @@ namespace jdi
|
||||
// 其极值必然出现在采样点或列边界上,因此「列内采样点 + 两侧边界取值」就是
|
||||
// 该列包络的精确解,且相邻两列在边界处取到同一个值,包络带严丝合缝。
|
||||
double boundary = 0.0;
|
||||
if (PolylineValueAt(time, values, count, i0, t0, boundary))
|
||||
if (PolylineValueAt(time, values, count, *resolved.style, i0, t0, boundary))
|
||||
{
|
||||
boundary = ApplyDrawTransform(*resolved.style, boundary);
|
||||
if (boundary < low)
|
||||
{
|
||||
low = boundary;
|
||||
lowOrder = (i0 > 0) ? (i0 - 1) : 0;
|
||||
lowOrder = (i0 > 0) ? SourceIndexFor(*resolved.style, count, i0 - 1) : 0;
|
||||
}
|
||||
if (boundary > high)
|
||||
{
|
||||
high = boundary;
|
||||
highOrder = (i0 > 0) ? (i0 - 1) : 0;
|
||||
highOrder = (i0 > 0) ? SourceIndexFor(*resolved.style, count, i0 - 1) : 0;
|
||||
}
|
||||
}
|
||||
if (PolylineValueAt(time, values, count, i1, t1, boundary))
|
||||
if (PolylineValueAt(time, values, count, *resolved.style, i1, t1, boundary))
|
||||
{
|
||||
boundary = ApplyDrawTransform(*resolved.style, boundary);
|
||||
const std::size_t order = SourceIndexFor(*resolved.style, count, (i1 > 0) ? (i1 - 1) : 0);
|
||||
if (boundary < low)
|
||||
{
|
||||
low = boundary;
|
||||
lowOrder = i1;
|
||||
lowOrder = order;
|
||||
}
|
||||
if (boundary > high)
|
||||
{
|
||||
high = boundary;
|
||||
highOrder = i1;
|
||||
highOrder = order;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5749,7 +5770,9 @@ namespace jdi
|
||||
for (std::size_t index = begin; index < end; index += stride)
|
||||
{
|
||||
const double t = time[index];
|
||||
const double value = ApplyDrawTransform(*resolved.style, values[index]);
|
||||
// 水平镜像 = 数据反序:画面上第 index 个位置取源下标 count-1-index 的值。
|
||||
const double value = ApplyDrawTransform(
|
||||
*resolved.style, values[SourceIndexFor(*resolved.style, count, index)]);
|
||||
if (!std::isfinite(t) || !std::isfinite(value))
|
||||
{
|
||||
inSegment = false;
|
||||
@@ -6413,10 +6436,11 @@ namespace jdi
|
||||
mixFloat(resolved.style->opacity);
|
||||
mixFloat(resolved.style->lineWidth);
|
||||
mix(static_cast<std::uint64_t>(resolved.style->mode));
|
||||
// 绘制前的数值变换(增益 / 取绝对值 / 中心反转)会改变波形形状。
|
||||
// 绘制前的数值变换(系数 / 绝对值 / 镜像)会改变波形形状。
|
||||
mixFloat(resolved.style->gain);
|
||||
mix(resolved.style->absolute ? 1ull : 0ull);
|
||||
mix(resolved.style->inverted ? 1ull : 0ull);
|
||||
mix(resolved.style->mirrorVertical ? 1ull : 0ull);
|
||||
mix(resolved.style->mirrorHorizontal ? 1ull : 0ull);
|
||||
}
|
||||
// 图例文案参与绘制,重命名必须让缓存失效。
|
||||
mixText(resolved.data->Title());
|
||||
@@ -6464,7 +6488,8 @@ namespace jdi
|
||||
entry.mode = static_cast<int>(resolved.style->mode);
|
||||
entry.gain = resolved.style->gain;
|
||||
entry.absolute = resolved.style->absolute;
|
||||
entry.inverted = resolved.style->inverted;
|
||||
entry.mirrorVertical = resolved.style->mirrorVertical;
|
||||
entry.mirrorHorizontal = resolved.style->mirrorHorizontal;
|
||||
}
|
||||
lane.push_back(entry);
|
||||
}
|
||||
@@ -9397,11 +9422,12 @@ namespace jdi
|
||||
|
||||
const bool interpolate = (resolved.style->mode != DrawMode::SCATTER);
|
||||
double value = 0.0;
|
||||
if (!SampleSignalAt(*resolved.file, *resolved.data, time, interpolate, value))
|
||||
if (!SampleDisplayedSignalAt(*resolved.file, *resolved.data, *resolved.style, time,
|
||||
interpolate, value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 取值读数必须与画出来的波形一致:同样套用增益/取绝对值/中心反转。
|
||||
// 取值读数必须与画出来的波形一致:同样套用系数/绝对值/镜像。
|
||||
value = ApplyDrawTransform(*resolved.style, value);
|
||||
|
||||
const float rawY = ValueToY(value, lane);
|
||||
@@ -9423,7 +9449,8 @@ namespace jdi
|
||||
if (showDelta)
|
||||
{
|
||||
double otherValue = 0.0;
|
||||
if (SampleSignalAt(*resolved.file, *resolved.data, otherTime, interpolate, otherValue))
|
||||
if (SampleDisplayedSignalAt(*resolved.file, *resolved.data, *resolved.style, otherTime,
|
||||
interpolate, otherValue))
|
||||
{
|
||||
otherValue = ApplyDrawTransform(*resolved.style, otherValue);
|
||||
const double delta = value - otherValue;
|
||||
@@ -10345,12 +10372,14 @@ namespace jdi
|
||||
|
||||
for (std::size_t index = begin; index < end; index += stride)
|
||||
{
|
||||
if (index < curveBegin || (index - curveBegin) >= curve.size())
|
||||
// 水平镜像 = 数据反序:遍历下标映射到源下标后再取曲线值。
|
||||
const std::size_t sourceIndex = SourceIndexFor(*resolved.style, count, index);
|
||||
if (sourceIndex < curveBegin || (sourceIndex - curveBegin) >= curve.size())
|
||||
{
|
||||
inSegment = false;
|
||||
continue;
|
||||
}
|
||||
const float sampled = curve[index - curveBegin];
|
||||
const float sampled = curve[sourceIndex - curveBegin];
|
||||
if (!std::isfinite(sampled))
|
||||
{
|
||||
inSegment = false;
|
||||
|
||||
@@ -35,7 +35,10 @@ namespace jdi
|
||||
CMD_MODE_BASE = 200, /**< 绘制样式基准(配合下标)。 */
|
||||
CMD_LANE_BASE = 300, /**< 网格归属基准(配合下标)。 */
|
||||
CMD_LANE_NEW = 400, /**< 新建网格。 */
|
||||
CMD_GAIN_EDIT = 500, /**< 「系数调整」二级面板里的系数输入框(幂等提交)。 */
|
||||
CMD_GAIN_EDIT = 500, /**< 「系数」二级面板里的系数输入框(幂等提交)。 */
|
||||
CMD_GAIN_RESET = 501, /**< 「系数」二级面板标题栏右侧的还原按钮:把系数重置为 1。 */
|
||||
CMD_MIRROR_HORIZONTAL = 510, /**< 「镜像」二级面板:水平镜像(数据反序)。 */
|
||||
CMD_MIRROR_VERTICAL = 511 /**< 「镜像」二级面板:垂直镜像(按 Y=0 上下翻转)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -340,9 +343,9 @@ namespace jdi
|
||||
OPACITY_VALUE, /**< 不透明度数值框(可点击输入)。 */
|
||||
WIDTH_SLIDER, /**< 线宽滑块。 */
|
||||
WIDTH_VALUE, /**< 线宽数值框(可点击输入)。 */
|
||||
GAIN_ADJUST, /**< 「系数调整」按钮(点击展开系数输入二级面板)。 */
|
||||
ABSOLUTE_TOGGLE, /**< 「取绝对值」切换按钮。 */
|
||||
INVERT_TOGGLE, /**< 「中心反转」切换按钮。 */
|
||||
GAIN_ADJUST, /**< 「系数」按钮(点击展开系数二级面板)。 */
|
||||
MIRROR_ADJUST, /**< 「镜像」按钮(点击展开镜像二级面板)。 */
|
||||
ABSOLUTE_TOGGLE, /**< 「绝对值」切换按钮。 */
|
||||
TOGGLE_ALL, /**< 全部显示/隐藏按钮。 */
|
||||
FILTER_CLEAR /**< 「筛选信号」输入框右侧的清空 ✕。 */
|
||||
};
|
||||
@@ -494,16 +497,34 @@ namespace jdi
|
||||
bool OpenColorPickerPopup(const SignalStyle& style);
|
||||
|
||||
/**
|
||||
* @brief 打开「系数调整」的**二级面板**(一个带输入框的浮动面板,锚定在该按钮下方)。
|
||||
* @brief 打开「系数」的**二级面板**(一个整行输入框 + 标题栏右侧的还原按钮)。
|
||||
*
|
||||
* 与自定义颜色入口同一套交互:点击按钮 → 抬起时展开浮动面板 → 面板内的输入框可编辑
|
||||
* 绘制前乘上的系数(默认 1),回车 / 点击其它位置提交,面板保持打开(与网格 Y 轴上下限
|
||||
* 的菜单内联输入框一致)。
|
||||
* 绘制前乘上的系数(默认 1,至少显示两位小数),回车 / 点击其它位置提交,面板保持打开
|
||||
*(与网格 Y 轴上下限的菜单内联输入框一致)。“还原”图标按钮把系数重置为 1。
|
||||
*
|
||||
* @return 面板已打开返回 true。
|
||||
*/
|
||||
bool OpenGainMenu();
|
||||
|
||||
/**
|
||||
* @brief 打开「镜像」的**二级面板**(两行带复选框的选项:水平镜像 / 垂直镜像)。
|
||||
*
|
||||
* 两项都是“切换项”,点击后勾选状态立即生效且**菜单保持打开**,方便连续勾选;
|
||||
* 只要任一项被勾选,「镜像」按钮即显示为激活态。
|
||||
*
|
||||
* @return 面板已打开返回 true。
|
||||
*/
|
||||
bool OpenMirrorMenu();
|
||||
|
||||
/**
|
||||
* @brief 清除样式区的悬停高亮。
|
||||
*
|
||||
* 二级面板(弹出菜单)一打开就会接管鼠标事件,样式区不再收到 `WM_MOUSEMOVE`,
|
||||
* 因此必须在打开面板时主动清掉悬停态,否则该按钮会一直保持"鼠标按住"般的高亮。
|
||||
*/
|
||||
void ClearStyleHover();
|
||||
|
||||
/**
|
||||
* @brief 判断当前标签页中全部信号是否均为可见状态。
|
||||
* @return 全部可见且至少有一条信号返回 true。
|
||||
@@ -658,7 +679,8 @@ namespace jdi
|
||||
bool m_pendingLaneMenu = false; /**< 左键抬起时是否弹出网格归属菜单。 */
|
||||
SignalRef m_pendingLaneRef; /**< 待弹出菜单的目标信号。 */
|
||||
bool m_pendingColorPicker = false; /**< 左键抬起时是否弹出拾色器。 */
|
||||
bool m_pendingGainMenu = false; /**< 左键抬起时是否弹出「系数调整」的系数输入二级面板。 */
|
||||
bool m_pendingGainMenu = false; /**< 左键抬起时是否弹出「系数」的二级面板。 */
|
||||
bool m_pendingMirrorMenu = false; /**< 左键抬起时是否弹出「镜像」的二级面板。 */
|
||||
|
||||
/**
|
||||
* @brief 待确认的「关闭类」按钮(✕)。
|
||||
|
||||
@@ -108,9 +108,9 @@ namespace jdi
|
||||
const float kAdjustGap = 6.0f; /**< 「调整」行三个按钮之间的间距。 */
|
||||
|
||||
/** 「调整」行的按钮下标(与 `m_adjustRects` 同序)。 */
|
||||
const int kAdjustGain = 0; /**< 系数调整(点击展开系数输入二级面板)。 */
|
||||
const int kAdjustAbsolute = 1; /**< 取绝对值。 */
|
||||
const int kAdjustInvert = 2; /**< 中心反转。 */
|
||||
const int kAdjustGain = 0; /**< 系数(点击展开系数二级面板)。 */
|
||||
const int kAdjustMirror = 1; /**< 镜像(点击展开镜像二级面板)。 */
|
||||
const int kAdjustAbsolute = 2; /**< 绝对值。 */
|
||||
const int kAdjustCount = 3; /**< 按钮数量。 */
|
||||
|
||||
const float kWidthMin = 0.5f; /**< 允许的最小线宽(px)。 */
|
||||
@@ -167,30 +167,28 @@ namespace jdi
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 把系数增益格式化为界面文本(最多 4 位小数,并去掉无意义的尾随 0 与小数点)。
|
||||
* @brief 把系数格式化为界面文本:**至少两位小数**,精度更高时按实际精度显示。
|
||||
*
|
||||
* 默认值 1 因此显示为 `1`(而不是 `1.0000`),输入 2.5 显示为 `2.5`。
|
||||
* 例:`1` → `1.00`、`2.5` → `2.50`、`0.125` → `0.125`、`1.2345678` → `1.2345678`。
|
||||
* 实现:从 2 位小数开始递增位数,直到文本能精确表示该系数(相对误差 1e-7,与 float 精度匹配)。
|
||||
*
|
||||
* @param [IN] gain 增益系数。
|
||||
* @param [IN] gain 系数。
|
||||
* @return 显示文本。
|
||||
*/
|
||||
std::wstring FormatGainText(float gain)
|
||||
{
|
||||
std::wstring text = jdi::FormatNumber(static_cast<double>(gain), 4);
|
||||
if (text.find(L'.') != std::wstring::npos)
|
||||
const double value = static_cast<double>(gain);
|
||||
const double tolerance = std::max(1.0e-9, std::fabs(value) * 1.0e-7);
|
||||
for (int decimals = 2; decimals <= 9; ++decimals)
|
||||
{
|
||||
std::size_t end = text.size();
|
||||
while (end > 0 && text[end - 1] == L'0')
|
||||
const std::wstring text = jdi::FormatNumber(value, decimals);
|
||||
const double parsed = std::wcstod(text.c_str(), nullptr);
|
||||
if (std::fabs(parsed - value) <= tolerance)
|
||||
{
|
||||
--end;
|
||||
return text;
|
||||
}
|
||||
if (end > 0 && text[end - 1] == L'.')
|
||||
{
|
||||
--end;
|
||||
}
|
||||
text.erase(end);
|
||||
}
|
||||
return text;
|
||||
return jdi::FormatNumber(value, 9);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,6 +224,7 @@ namespace jdi
|
||||
m_pendingLaneMenu = false;
|
||||
m_pendingColorPicker = false;
|
||||
m_pendingGainMenu = false;
|
||||
m_pendingMirrorMenu = false;
|
||||
m_pendingClosePart = RowPart::NONE;
|
||||
m_pendingCloseRow = -1;
|
||||
m_pendingFilterClear = false;
|
||||
@@ -307,8 +306,8 @@ namespace jdi
|
||||
const float fx = static_cast<float>(x);
|
||||
const float fy = static_cast<float>(y);
|
||||
if (UiPointInRect(m_adjustRects[kAdjustGain], fx, fy) ||
|
||||
UiPointInRect(m_adjustRects[kAdjustAbsolute], fx, fy) ||
|
||||
UiPointInRect(m_adjustRects[kAdjustInvert], fx, fy) || UiPointInRect(m_widthValueRect, fx, fy) ||
|
||||
UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy) ||
|
||||
UiPointInRect(m_adjustRects[kAdjustAbsolute], fx, fy) || UiPointInRect(m_widthValueRect, fx, fy) ||
|
||||
UiPointInRect(m_opacityValueRect, fx, fy))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
@@ -1276,8 +1275,8 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 第三行:调整(系数调整 / 取绝对值 / 中心反转,三个切换类按钮)----
|
||||
// 三者依次作用于绘制值:系数(点击展开二级面板输入)→ 取绝对值 → 中心反转。
|
||||
// ---- 第三行:调整(系数 / 镜像 / 绝对值,三个切换类按钮)----
|
||||
// 结算顺序固定为「系数 → 绝对值 → 镜像」;「系数」「镜像」点击后展开二级面板。
|
||||
{
|
||||
paintRowLabel(2, L"调整");
|
||||
|
||||
@@ -1292,10 +1291,11 @@ namespace jdi
|
||||
UiText(g, label, m_fonts->uiSmall, rect,
|
||||
checked ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
|
||||
};
|
||||
// 系数调整:仅当系数**不等于默认值 1** 时才显示为"已启用"(与其它切换按钮的启用样式一致)。
|
||||
paintToggle(kAdjustGain, L"系数调整", !GainIsDefault(style->gain), StyleHit::GAIN_ADJUST);
|
||||
paintToggle(kAdjustAbsolute, L"取绝对值", style->absolute, StyleHit::ABSOLUTE_TOGGLE);
|
||||
paintToggle(kAdjustInvert, L"中心反转", style->inverted, StyleHit::INVERT_TOGGLE);
|
||||
// 系数:仅当系数**不等于默认值 1** 时才显示为"已启用"。
|
||||
paintToggle(kAdjustGain, L"系数", !GainIsDefault(style->gain), StyleHit::GAIN_ADJUST);
|
||||
// 镜像:水平或垂直任一被勾选即显示为激活态。
|
||||
paintToggle(kAdjustMirror, L"镜像", style->Mirrored(), StyleHit::MIRROR_ADJUST);
|
||||
paintToggle(kAdjustAbsolute, L"绝对值", style->absolute, StyleHit::ABSOLUTE_TOGGLE);
|
||||
}
|
||||
|
||||
// ---- 第四行:线宽(数值可点击输入)----
|
||||
@@ -1977,18 +1977,19 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
// 二级面板:一个标题行 + 一个内联输入框(与网格 Y 轴上下限的菜单内联编辑同一套做法,
|
||||
// 提交后面板保持打开,因此可以连续微调;点击面板外或回车均会提交)。
|
||||
// 二级面板:标题行(右侧带「还原」图标按钮)+ 一整行系数输入框
|
||||
//(与网格 Y 轴上下限的菜单内联编辑同一套做法:提交后面板保持打开,可连续微调)。
|
||||
std::vector<MenuItem> items;
|
||||
|
||||
MenuItem header;
|
||||
header.kind = MenuItemKind::HEADER;
|
||||
header.kind = MenuItemKind::HEADER_ACTION;
|
||||
header.label = L"系数调整";
|
||||
header.id = CMD_GAIN_RESET; // 点击标题栏右侧的还原图标 → 系数重置为 1
|
||||
items.push_back(header);
|
||||
|
||||
MenuItem edit;
|
||||
edit.kind = MenuItemKind::EDIT;
|
||||
edit.label = L"绘制系数";
|
||||
edit.label.clear(); // 不再显示「绘制系数」说明文字:输入框独占整行
|
||||
edit.id = CMD_GAIN_EDIT;
|
||||
edit.value = FormatGainText(style->gain);
|
||||
edit.filter = EditFilter::NUMBER;
|
||||
@@ -1999,9 +2000,61 @@ namespace jdi
|
||||
const gd::RectF bounds = (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area;
|
||||
m_menu->Open(MenuScope::SIGNAL_PANEL, bounds, static_cast<int>(button.X),
|
||||
static_cast<int>(button.Y + button.Height), items);
|
||||
// ⚠️ 菜单一旦打开就会接管鼠标事件,样式区不会再收到 WM_MOUSEMOVE →
|
||||
// 必须在这里主动清掉按钮的悬停态,否则按钮会"像鼠标一直按着"一样持续高亮。
|
||||
ClearStyleHover();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SignalPanel::OpenMirrorMenu()
|
||||
{
|
||||
if (m_menu == nullptr || m_tab == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const SignalStyle* style = StyleOf(SelectedRef());
|
||||
if (style == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 两行带复选框的选项:点击即切换、且**保持菜单打开**(可连续勾选两项)。
|
||||
std::vector<MenuItem> items;
|
||||
|
||||
MenuItem horizontal;
|
||||
horizontal.kind = MenuItemKind::NORMAL;
|
||||
horizontal.label = L"水平镜像";
|
||||
horizontal.id = CMD_MIRROR_HORIZONTAL;
|
||||
horizontal.checked = style->mirrorHorizontal;
|
||||
horizontal.keepOpen = true;
|
||||
items.push_back(horizontal);
|
||||
|
||||
MenuItem vertical;
|
||||
vertical.kind = MenuItemKind::NORMAL;
|
||||
vertical.label = L"垂直镜像";
|
||||
vertical.id = CMD_MIRROR_VERTICAL;
|
||||
vertical.checked = style->mirrorVertical;
|
||||
vertical.keepOpen = true;
|
||||
items.push_back(vertical);
|
||||
|
||||
const gd::RectF& button = m_adjustRects[kAdjustMirror];
|
||||
const gd::RectF bounds = (m_clientBounds.Width > 1.0f) ? m_clientBounds : m_area;
|
||||
m_menu->Open(MenuScope::SIGNAL_PANEL, bounds, static_cast<int>(button.X),
|
||||
static_cast<int>(button.Y + button.Height), items);
|
||||
ClearStyleHover();
|
||||
return true;
|
||||
}
|
||||
|
||||
void SignalPanel::ClearStyleHover()
|
||||
{
|
||||
if (m_hoverStyle == StyleHit::NONE && m_hoverStyleIndex < 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
m_hoverStyle = StyleHit::NONE;
|
||||
m_hoverStyleIndex = -1;
|
||||
}
|
||||
|
||||
bool SignalPanel::CloseTransientUi()
|
||||
{
|
||||
bool changed = false;
|
||||
@@ -2024,6 +2077,7 @@ namespace jdi
|
||||
m_pendingCloseRow = -1;
|
||||
m_pendingFilterClear = false;
|
||||
m_pendingGainMenu = false;
|
||||
m_pendingMirrorMenu = false;
|
||||
return changed;
|
||||
}
|
||||
|
||||
@@ -2119,14 +2173,14 @@ namespace jdi
|
||||
{
|
||||
hoverStyle = StyleHit::GAIN_ADJUST;
|
||||
}
|
||||
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy))
|
||||
{
|
||||
hoverStyle = StyleHit::MIRROR_ADJUST;
|
||||
}
|
||||
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_adjustRects[kAdjustAbsolute], fx, fy))
|
||||
{
|
||||
hoverStyle = StyleHit::ABSOLUTE_TOGGLE;
|
||||
}
|
||||
if (hoverStyle == StyleHit::NONE && UiPointInRect(m_adjustRects[kAdjustInvert], fx, fy))
|
||||
{
|
||||
hoverStyle = StyleHit::INVERT_TOGGLE;
|
||||
}
|
||||
}
|
||||
|
||||
if (hoverRow == m_hoverRow && hoverStyle == m_hoverStyle && hoverStyleIndex == m_hoverStyleIndex &&
|
||||
@@ -2203,6 +2257,7 @@ namespace jdi
|
||||
m_pendingLaneMenu = false;
|
||||
m_pendingColorPicker = false;
|
||||
m_pendingGainMenu = false;
|
||||
m_pendingMirrorMenu = false;
|
||||
|
||||
// 点击当前输入框以外的位置时提交输入。
|
||||
if (m_editField != StyleEditField::NONE &&
|
||||
@@ -2268,23 +2323,23 @@ namespace jdi
|
||||
{
|
||||
return BeginEdit(StyleEditField::OPACITY);
|
||||
}
|
||||
// 「调整」行:系数调整(按下只登记,**抬起**时才展开二级面板,
|
||||
// 与自定义颜色入口同一套交互)、取绝对值 / 中心反转(单击即切换)。
|
||||
// 「调整」行:系数与镜像(按下只登记,**抬起**时才展开各自的二级面板,
|
||||
// 与自定义颜色入口同一套交互)、绝对值(单击即切换)。
|
||||
if (UiPointInRect(m_adjustRects[kAdjustGain], fx, fy))
|
||||
{
|
||||
m_pendingGainMenu = true;
|
||||
return true;
|
||||
}
|
||||
if (UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy))
|
||||
{
|
||||
m_pendingMirrorMenu = true;
|
||||
return true;
|
||||
}
|
||||
if (UiPointInRect(m_adjustRects[kAdjustAbsolute], fx, fy))
|
||||
{
|
||||
style->absolute = !style->absolute;
|
||||
return true;
|
||||
}
|
||||
if (UiPointInRect(m_adjustRects[kAdjustInvert], fx, fy))
|
||||
{
|
||||
style->inverted = !style->inverted;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 列表行。
|
||||
@@ -2424,7 +2479,7 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 「系数调整」:按下时只登记,抬起时指针仍在同一按钮上才展开二级面板
|
||||
// 「系数」/「镜像」:按下时只登记,抬起时指针仍在同一按钮上才展开二级面板
|
||||
//(与自定义颜色入口一致:按住后移开再松开视为放弃)。
|
||||
if (m_pendingGainMenu)
|
||||
{
|
||||
@@ -2436,6 +2491,16 @@ namespace jdi
|
||||
changed = OpenGainMenu() || changed;
|
||||
}
|
||||
}
|
||||
if (m_pendingMirrorMenu)
|
||||
{
|
||||
m_pendingMirrorMenu = false;
|
||||
const float fx = static_cast<float>(x);
|
||||
const float fy = static_cast<float>(y);
|
||||
if (UiPointInRect(m_adjustRects[kAdjustMirror], fx, fy) && m_tab != nullptr)
|
||||
{
|
||||
changed = OpenMirrorMenu() || changed;
|
||||
}
|
||||
}
|
||||
|
||||
if (m_pendingLaneMenu)
|
||||
{
|
||||
@@ -2755,7 +2820,7 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
// 「系数调整」二级面板里的输入框提交:就地更新绘制系数,面板保持打开
|
||||
// 「系数」二级面板里的输入框提交:就地更新绘制系数,面板保持打开
|
||||
//(与网格 Y 轴上下限的菜单内联编辑一致),并把夹取后的真实值回写到输入框上。
|
||||
if (result.editCommit && result.id == CMD_GAIN_EDIT)
|
||||
{
|
||||
@@ -2763,9 +2828,9 @@ namespace jdi
|
||||
if (style != nullptr)
|
||||
{
|
||||
wchar_t* stop = nullptr;
|
||||
const double parsed = std::wcstod(result.editText.c_str(), &stop);
|
||||
// 非法/空输入保持原值(只把真实值回写到输入框,避免用户看到错误的数字)。
|
||||
const std::wstring trimmed = Trim(result.editText);
|
||||
const double parsed = std::wcstod(trimmed.c_str(), &stop);
|
||||
// 非法/空输入保持原值(只把真实值回写到输入框,避免用户看到错误的数字)。
|
||||
if (!trimmed.empty() && stop != trimmed.c_str() && std::isfinite(parsed))
|
||||
{
|
||||
style->gain = static_cast<float>(std::max(-kGainLimit, std::min(parsed, kGainLimit)));
|
||||
@@ -2778,6 +2843,36 @@ namespace jdi
|
||||
return false;
|
||||
}
|
||||
|
||||
// 标题栏右侧的「还原」图标:把系数重置为默认值 1(面板保持打开,输入框立即回显)。
|
||||
if (result.id == CMD_GAIN_RESET)
|
||||
{
|
||||
if (SignalStyle* style = StyleOf(SelectedRef()))
|
||||
{
|
||||
style->gain = 1.0f;
|
||||
if (m_menu != nullptr)
|
||||
{
|
||||
m_menu->UpdateItemValue(CMD_GAIN_EDIT, FormatGainText(style->gain));
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// 「镜像」二级面板:两行带复选框的切换项(面板保持打开,勾选状态就地回写)。
|
||||
if (result.id == CMD_MIRROR_HORIZONTAL || result.id == CMD_MIRROR_VERTICAL)
|
||||
{
|
||||
if (SignalStyle* style = StyleOf(SelectedRef()))
|
||||
{
|
||||
const bool horizontal = (result.id == CMD_MIRROR_HORIZONTAL);
|
||||
bool& flag = horizontal ? style->mirrorHorizontal : style->mirrorVertical;
|
||||
flag = !flag;
|
||||
if (m_menu != nullptr)
|
||||
{
|
||||
m_menu->UpdateItemLabel(result.id, horizontal ? L"水平镜像" : L"垂直镜像", flag, true);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool needRelayout = false;
|
||||
|
||||
switch (result.id)
|
||||
|
||||
Reference in new issue
Block a user