fix: 修复图例改名不同步、细化网格标尺并支持标签页重排与曲线实色跟随拖拽
- 图例跟随重命名:绘图区图例文本改用信号当前显示名(Title), 左侧列表改名后网格图例立即同步。 - 标签页重排与改名:新增拖拽排序(按住标签左右拖动并显示插入指示条, 重排后保持原激活页)与双击内联改名,新增 MoveTab 正确处理激活下标。 修复改名后未重新布局导致标签页整条消失的问题(SetTabs 会重置标签矩形)。 双击判定改为自行按双击时间识别,不再依赖系统 WM_LBUTTONDBLCLK。 - 信号拖拽:虚影改用原信号颜色与线宽(不再画成虚线),保持原有线段/散点样式; 横向与纵向均自由跟随鼠标位移;拖拽首帧位移为零,虚影始终贴合在鼠标下方。 - 网格与标尺细化:时间轴与幅值轴期望刻度数提高,并在相邻主刻度之间 补绘 5 等分细线,便于目视估读。 - 图例定位与交互:图例移到各网格左上角,与标尺数字冲突时优先显示图例 (标尺数值主动避让);图例条目支持单击选中信号(与左侧列表联动、 样式区随之切换目标)与双击重命名信号。 - 网格划分:合并网格后新勾选的信号不再被并入那个唯一的合并网格, 而是自动切换为自定义划分并独占一个新网格。 - 工具栏:「每信号一格」改名为「拆分信号网格」。 - 清理:移除已无引用的 UiDrawArrowIcon 与 m_styleContentRect。
This commit is contained in:
1 parent
261b8ac07d
commit
4e295a2a7d
12 files changed
+800
-109
No files matched your search
@@ -11,8 +11,9 @@ namespace jdi
|
||||
|
||||
namespace
|
||||
{
|
||||
const int kTimeTickTarget = 8; /**< 时间轴期望刻度数。 */
|
||||
const int kYTickTarget = 4; /**< 幅值轴期望刻度数。 */
|
||||
const int kTimeTickTarget = 10; /**< 时间轴期望刻度数。 */
|
||||
const int kYTickTarget = 6; /**< 幅值轴期望刻度数。 */
|
||||
const int kMinorDivisions = 5; /**< 相邻主刻度之间的等分数(用于估读)。 */
|
||||
const float kScrollBarThickness = 9.0f; /**< 滚动条宽度(逻辑像素)。 */
|
||||
const float kLanePadding = 8.0f; /**< 网格内边距。 */
|
||||
const size_t kMaxPolylinePoints = 20000; /**< 单条折线最大顶点数。 */
|
||||
@@ -25,6 +26,10 @@ namespace jdi
|
||||
const float kLaneDividerWidth = 2.0f; /**< 网格之间分界线的宽度。 */
|
||||
const float kSignalHitTolerance = 6.0f; /**< 信号曲线命中容差(逻辑像素)。 */
|
||||
const float kLaneLabelPad = 6.0f; /**< 网格内侧标尺带的内边距。 */
|
||||
const float kLegendHeight = 19.0f; /**< 图例条目高度(逻辑像素)。 */
|
||||
const float kLegendPad = 5.0f; /**< 图例相对网格左上角的内缩。 */
|
||||
const float kLegendSwatchSize = 8.0f; /**< 图例色块尺寸。 */
|
||||
const float kLegendItemGap = 14.0f; /**< 图例条目之间的间距。 */
|
||||
|
||||
/**
|
||||
* @brief 将数值限制到合法区间。
|
||||
@@ -47,13 +52,13 @@ namespace jdi
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 组装图例文本:直接使用列名原文(已含序号与括号描述)。
|
||||
* @brief 组装图例文本:使用信号当前显示名(用户重命名后立即生效)。
|
||||
* @param [IN] data 信号数据。
|
||||
* @return 图例文本。
|
||||
*/
|
||||
std::wstring BuildLegendText(const SignalData& data)
|
||||
{
|
||||
return data.name;
|
||||
return data.Title();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -837,6 +842,17 @@ namespace jdi
|
||||
return gd::RectF(left, m_plotRect.Y + 2.0f * m_scale, width, height);
|
||||
}
|
||||
|
||||
if (m_editTarget == PlotEditTarget::SIGNAL_NAME)
|
||||
{
|
||||
// 图例改名:输入框直接落在被双击的图例条目位置上。
|
||||
if (m_editFieldRect.Width <= 0.0f)
|
||||
{
|
||||
return gd::RectF();
|
||||
}
|
||||
const float nameWidth = std::max(width, m_editFieldRect.Width);
|
||||
return gd::RectF(m_editFieldRect.X, m_editFieldRect.Y, nameWidth, height);
|
||||
}
|
||||
|
||||
// 网格 Y 轴上下限输入框贴在该网格内侧标尺带下方。
|
||||
if (m_editLane < 0 || m_editLane >= static_cast<int>(m_lanes.size()))
|
||||
{
|
||||
@@ -854,6 +870,12 @@ namespace jdi
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 切换编辑目标时清掉上一轮残留(图例改名需由调用方在进入前先设置 m_editSignal)。
|
||||
if (target != PlotEditTarget::SIGNAL_NAME)
|
||||
{
|
||||
m_editSignal = SignalRef();
|
||||
m_editFieldRect = gd::RectF();
|
||||
}
|
||||
m_editTarget = target;
|
||||
m_editLane = laneIndex;
|
||||
m_editText = initialText;
|
||||
@@ -861,6 +883,20 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::TakePendingRename(SignalRef& ref, std::wstring& name)
|
||||
{
|
||||
if (!m_pendingRename)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
ref = m_pendingRenameRef;
|
||||
name = m_pendingRenameName;
|
||||
m_pendingRename = false;
|
||||
m_pendingRenameRef = SignalRef();
|
||||
m_pendingRenameName.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::EndEdit(bool commit)
|
||||
{
|
||||
if (m_editTarget == PlotEditTarget::NONE)
|
||||
@@ -870,8 +906,28 @@ namespace jdi
|
||||
|
||||
const PlotEditTarget target = m_editTarget;
|
||||
const int laneIndex = m_editLane;
|
||||
const SignalRef editSignal = m_editSignal;
|
||||
const std::wstring text = Trim(m_editText);
|
||||
|
||||
m_editTarget = PlotEditTarget::NONE;
|
||||
m_editLane = -1;
|
||||
m_editFresh = false;
|
||||
m_editSignal = SignalRef();
|
||||
m_editFieldRect = gd::RectF();
|
||||
m_editText.clear();
|
||||
|
||||
// 信号改名:交给宿主窗口落到数据层,绘图区只持有只读数据。
|
||||
if (target == PlotEditTarget::SIGNAL_NAME)
|
||||
{
|
||||
if (commit && !text.empty() && editSignal.IsValid())
|
||||
{
|
||||
m_pendingRenameRef = editSignal;
|
||||
m_pendingRenameName = text;
|
||||
m_pendingRename = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
double parsed = 0.0;
|
||||
bool parsedOk = false;
|
||||
if (commit && !text.empty())
|
||||
@@ -881,11 +937,6 @@ namespace jdi
|
||||
parsedOk = (stop != text.c_str()) && (parsed == parsed) && std::isfinite(parsed);
|
||||
}
|
||||
|
||||
m_editTarget = PlotEditTarget::NONE;
|
||||
m_editLane = -1;
|
||||
m_editFresh = false;
|
||||
m_editText.clear();
|
||||
|
||||
if (!parsedOk)
|
||||
{
|
||||
return true;
|
||||
@@ -965,6 +1016,25 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
// 信号改名接受任意可见字符(保留中文、空格与括号等原始信息)。
|
||||
if (m_editTarget == PlotEditTarget::SIGNAL_NAME)
|
||||
{
|
||||
if (character < L' ')
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (m_editFresh)
|
||||
{
|
||||
m_editText.clear();
|
||||
m_editFresh = false;
|
||||
}
|
||||
if (m_editText.size() < 96)
|
||||
{
|
||||
m_editText.push_back(character);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const bool digit = (character >= L'0' && character <= L'9');
|
||||
const bool special = (character == L'.' || character == L'-' || character == L'+' || character == L'e' ||
|
||||
character == L'E');
|
||||
@@ -1185,6 +1255,7 @@ namespace jdi
|
||||
{
|
||||
const int target = LaneAtY(y);
|
||||
m_dragMoved = true;
|
||||
m_dragMouseX = x;
|
||||
m_dragMouseY = y;
|
||||
if (target != m_dragTargetLane)
|
||||
{
|
||||
@@ -1215,10 +1286,15 @@ namespace jdi
|
||||
}
|
||||
|
||||
// 常规模式下精确检测鼠标是否贴近某条信号曲线,用于给出可拖拽提示。
|
||||
// 图例优先于曲线:图例条目本身就是信号的可点选目标(与左侧信号列表一致)。
|
||||
SignalRef hoverSignal;
|
||||
if (Tab().ZoomMode() == PlotZoomMode::NONE && !m_cursorHover)
|
||||
{
|
||||
hoverSignal = HitTestSignal(x, y);
|
||||
hoverSignal = LegendAtPoint(x, y);
|
||||
if (!hoverSignal.IsValid())
|
||||
{
|
||||
hoverSignal = HitTestSignal(x, y);
|
||||
}
|
||||
}
|
||||
if (hoverSignal != m_hoverSignal)
|
||||
{
|
||||
@@ -1298,12 +1374,27 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
// 常规模式下:点击图例条目等价于在左侧列表中选中该信号(样式区随之切换目标)。
|
||||
if (mode == PlotZoomMode::NONE)
|
||||
{
|
||||
const SignalRef legendHit = LegendAtPoint(x, y);
|
||||
if (legendHit.IsValid())
|
||||
{
|
||||
Tab().SetSelected(legendHit);
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 常规模式下:先检测是否落在信号曲线上;只有命中提示时才允许拖拽该信号。
|
||||
const SignalRef hit = HitTestSignal(x, y);
|
||||
if (hit.IsValid())
|
||||
{
|
||||
m_dragSignal = hit;
|
||||
m_dragTargetLane = Tab().LaneOfSignal(hit);
|
||||
Tab().SetSelected(hit);
|
||||
m_dragMouseX = x;
|
||||
m_dragMouseY = y;
|
||||
m_dragMode = PlotDragMode::MOVE_SIGNAL;
|
||||
return true;
|
||||
}
|
||||
@@ -1407,6 +1498,29 @@ namespace jdi
|
||||
return BeginEdit(PlotEditTarget::CURSOR_TIME, -1, FormatValue(Tab().CursorTime()));
|
||||
}
|
||||
|
||||
// 命中图例条目:进入信号改名(与左侧列表双击改名的交互一致)。
|
||||
const SignalRef legendHit = LegendAtPoint(x, y);
|
||||
if (legendHit.IsValid())
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(legendHit);
|
||||
if (resolved.data != nullptr)
|
||||
{
|
||||
Tab().SetSelected(legendHit);
|
||||
m_editSignal = legendHit;
|
||||
|
||||
// 先把输入框位置落在被双击的条目上,再进入编辑态。
|
||||
for (const LegendHit& hit : m_legendHits)
|
||||
{
|
||||
if (hit.ref == legendHit)
|
||||
{
|
||||
m_editFieldRect = hit.rect;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return BeginEdit(PlotEditTarget::SIGNAL_NAME, -1, resolved.data->Title());
|
||||
}
|
||||
}
|
||||
|
||||
// 其它位置:回到初始状态(自适应 + 退出缩放模式)。
|
||||
FitAll();
|
||||
return true;
|
||||
@@ -1560,6 +1674,9 @@ namespace jdi
|
||||
// 波形区域底色。
|
||||
UiFillRect(g, m_plotRect, m_theme->cardBg);
|
||||
|
||||
// 图例命中矩形每帧重建,避免残留已消失的信号。
|
||||
m_legendHits.clear();
|
||||
|
||||
for (std::size_t index = 0; index < m_lanes.size(); ++index)
|
||||
{
|
||||
PaintLane(g, static_cast<int>(index), m_lanes[index], timeTicks);
|
||||
@@ -1657,6 +1774,28 @@ namespace jdi
|
||||
std::vector<double> yTicks;
|
||||
BuildTicks(lane.yMin, lane.yMax, yStep, yTicks);
|
||||
|
||||
// 五等分估读线:在相邻主刻度之间再细分,便于目视读取中间值。
|
||||
const double yMinorStep = yStep / static_cast<double>(kMinorDivisions);
|
||||
std::vector<double> yMinorTicks;
|
||||
BuildTicks(lane.yMin, lane.yMax, yMinorStep, yMinorTicks);
|
||||
for (const double tick : yMinorTicks)
|
||||
{
|
||||
// 与主刻度重合的位置跳过(主刻度会以更重的线条绘制)。
|
||||
const double majorIndex = tick / yStep;
|
||||
if (std::fabs(majorIndex - std::floor(majorIndex + 0.5)) < 1.0e-6)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float y = ValueToY(tick, lane);
|
||||
if (y < lane.rect.Y - 1.0f || y > lane.rect.Y + lane.rect.Height + 1.0f)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
UiDrawLine(g, lane.rect.X, y, lane.rect.X + lane.rect.Width, y,
|
||||
WithAlpha(m_theme->gridWeak, 0x70), 1.0f);
|
||||
}
|
||||
|
||||
for (const double tick : yTicks)
|
||||
{
|
||||
const float y = ValueToY(tick, lane);
|
||||
@@ -1670,7 +1809,29 @@ namespace jdi
|
||||
zeroLine ? m_theme->gridStrong : m_theme->gridWeak, 1.0f);
|
||||
}
|
||||
|
||||
// 时间网格线。
|
||||
// 时间网格线(同样补充五等分估读线)。
|
||||
const double timeSpan = Tab().ViewMax() - Tab().ViewMin();
|
||||
const double timeMajorStep = NiceStep((timeSpan > 0.0) ? timeSpan : 1.0, kTimeTickTarget);
|
||||
const double timeMinorStep = timeMajorStep / static_cast<double>(kMinorDivisions);
|
||||
std::vector<double> timeMinorTicks;
|
||||
BuildTicks(Tab().ViewMin(), Tab().ViewMax(), timeMinorStep, timeMinorTicks);
|
||||
for (const double tick : timeMinorTicks)
|
||||
{
|
||||
const double majorIndex = tick / timeMajorStep;
|
||||
if (std::fabs(majorIndex - std::floor(majorIndex + 0.5)) < 1.0e-6)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
const float x = TimeToX(tick);
|
||||
if (x < lane.rect.X || x > lane.rect.X + lane.rect.Width)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
UiDrawLine(g, x, lane.rect.Y, x, lane.rect.Y + lane.rect.Height,
|
||||
WithAlpha(m_theme->gridWeak, 0x70), 1.0f);
|
||||
}
|
||||
|
||||
for (const double tick : timeTicks)
|
||||
{
|
||||
const float x = TimeToX(tick);
|
||||
@@ -1784,6 +1945,9 @@ namespace jdi
|
||||
// 内侧纵轴标尺作为覆盖层绘制:每个刻度数值带半透明底,保证在波形之上仍清晰可读,
|
||||
// 同时只在数值背后遮挡波形,不牺牲波形本身的可见范围。
|
||||
{
|
||||
// 图例固定占用网格左上角,冲突时优先显示图例,故落在图例带内的刻度数值直接跳过。
|
||||
const gd::RectF legendBand = LegendBandRect(laneIndex, lane);
|
||||
|
||||
const bool showLabels = (lane.rect.Height >= 34.0f * m_scale);
|
||||
float lastLabelY = -1.0e9f;
|
||||
|
||||
@@ -1816,6 +1980,15 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 与图例带冲突时让位(图例优先)。
|
||||
if (legendBand.Width > 0.0f &&
|
||||
pill.X < legendBand.X + legendBand.Width &&
|
||||
pill.X + pill.Width > legendBand.X &&
|
||||
pill.Y < legendBand.Y + legendBand.Height &&
|
||||
pill.Y + pill.Height > legendBand.Y)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 文本必须完整落在可见区内,否则会越过绘图区顶部(GDI 文本不受 GDI+ 裁剪约束)。
|
||||
if (!IsFullyVisible(pill, clipTop, clipBottom))
|
||||
{
|
||||
@@ -1842,6 +2015,57 @@ namespace jdi
|
||||
g.Restore(saveState);
|
||||
}
|
||||
|
||||
gd::RectF PlotView::LegendBandRect(int laneIndex, const LaneVisual& lane) const
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return gd::RectF();
|
||||
}
|
||||
|
||||
// 逐条累加已可见图例的宽度,得到整条图例带(用于让标尺数字避让)。
|
||||
float width = 0.0f;
|
||||
for (const SignalRef& ref : m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr || !resolved.style->visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
std::wstring label = BuildLegendText(*resolved.data);
|
||||
if (LaneSpansMultipleFiles(laneIndex) && resolved.file != nullptr)
|
||||
{
|
||||
label += L" · " + resolved.file->Title();
|
||||
}
|
||||
const float textWidth = UiMeasureTextWidth(m_fonts->uiSmall, label);
|
||||
width += kLegendSwatchSize * m_scale + 5.0f * m_scale + textWidth + kLegendItemGap * m_scale;
|
||||
}
|
||||
|
||||
if (width <= 0.0f)
|
||||
{
|
||||
return gd::RectF();
|
||||
}
|
||||
|
||||
const float left = lane.rect.X + kLegendPad * m_scale;
|
||||
const float maxWidth = std::max(0.0f, lane.rect.Width - kLegendPad * 2.0f * m_scale);
|
||||
return gd::RectF(left, lane.rect.Y + kLegendPad * m_scale - 1.0f * m_scale,
|
||||
std::min(width + 6.0f * m_scale, maxWidth), kLegendHeight * m_scale + 2.0f * m_scale);
|
||||
}
|
||||
|
||||
SignalRef PlotView::LegendAtPoint(int x, int y) const
|
||||
{
|
||||
const float fx = static_cast<float>(x);
|
||||
const float fy = static_cast<float>(y);
|
||||
for (const LegendHit& hit : m_legendHits)
|
||||
{
|
||||
if (UiPointInRect(hit.rect, fx, fy))
|
||||
{
|
||||
return hit.ref;
|
||||
}
|
||||
}
|
||||
return SignalRef();
|
||||
}
|
||||
|
||||
void PlotView::PaintLaneLegend(gd::Graphics& g, int laneIndex, const LaneVisual& lane, float visibleTop,
|
||||
float visibleBottom)
|
||||
{
|
||||
@@ -1850,9 +2074,10 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
|
||||
// 图例固定在网格顶部:网格被滚动出可视区时整体跳过,避免文字溢出到工具栏。
|
||||
const gd::RectF legendBand(lane.rect.X, lane.rect.Y + 4.0f * m_scale, lane.rect.Width,
|
||||
18.0f * m_scale);
|
||||
// 图例固定在网格左上角:网格被滚动出可视区时整体跳过,避免文字溢出到工具栏。
|
||||
const gd::RectF legendBand(lane.rect.X + kLegendPad * m_scale,
|
||||
lane.rect.Y + kLegendPad * m_scale, lane.rect.Width,
|
||||
kLegendHeight * m_scale);
|
||||
if (!IsFullyVisible(legendBand, visibleTop, visibleBottom))
|
||||
{
|
||||
return;
|
||||
@@ -1861,10 +2086,9 @@ namespace jdi
|
||||
const std::vector<SignalRef>& laneSignals = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].signals;
|
||||
const bool multiFile = LaneSpansMultipleFiles(laneIndex);
|
||||
|
||||
const float top = lane.rect.Y + 4.0f * m_scale;
|
||||
const float height = 18.0f * m_scale;
|
||||
// 图例从内侧标尺带的右侧开始,避免与纵轴刻度重叠。
|
||||
float cursorX = lane.rect.X + m_labelBandWidth + 6.0f * m_scale;
|
||||
const float top = legendBand.Y;
|
||||
const float height = kLegendHeight * m_scale;
|
||||
float cursorX = legendBand.X;
|
||||
const float maxX = lane.rect.X + lane.rect.Width - kLanePadding * m_scale;
|
||||
|
||||
bool anyVisible = false;
|
||||
@@ -1886,8 +2110,8 @@ namespace jdi
|
||||
{
|
||||
label += L" · " + resolved.file->Title();
|
||||
}
|
||||
const float itemWidth = 13.0f * m_scale + UiTextWidth(g, label, m_fonts->uiSmall) +
|
||||
14.0f * m_scale;
|
||||
const float itemWidth = kLegendSwatchSize * m_scale + 5.0f * m_scale +
|
||||
UiTextWidth(g, label, m_fonts->uiSmall) + kLegendItemGap * m_scale;
|
||||
if (cursorX + legendWidth + itemWidth > maxX)
|
||||
{
|
||||
break;
|
||||
@@ -1896,9 +2120,22 @@ namespace jdi
|
||||
}
|
||||
if (legendWidth > 0.0f)
|
||||
{
|
||||
UiFillRoundRect(g, gd::RectF(cursorX - 5.0f * m_scale, top - 1.0f * m_scale,
|
||||
legendWidth + 6.0f * m_scale, height + 2.0f * m_scale),
|
||||
3.0f * m_scale, WithAlpha(m_theme->cardBg, 0xD8));
|
||||
UiFillRoundRect(g, gd::RectF(cursorX - 4.0f * m_scale, top - 2.0f * m_scale,
|
||||
legendWidth + 6.0f * m_scale, height + 4.0f * m_scale),
|
||||
3.0f * m_scale, WithAlpha(m_theme->cardBg, 0xE8));
|
||||
}
|
||||
|
||||
// 记录本次绘制命中的条目矩形,供点击选中与双击改名使用。
|
||||
for (auto iterator = m_legendHits.begin(); iterator != m_legendHits.end();)
|
||||
{
|
||||
if (m_tab->LaneOfSignal(iterator->ref) == laneIndex)
|
||||
{
|
||||
iterator = m_legendHits.erase(iterator);
|
||||
}
|
||||
else
|
||||
{
|
||||
++iterator;
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制图例条目(与测量阶段保持一致的裁剪条件)。
|
||||
@@ -1919,29 +2156,59 @@ namespace jdi
|
||||
label += L" · " + resolved.file->Title();
|
||||
}
|
||||
const float textWidth = UiTextWidth(g, label, m_fonts->uiSmall);
|
||||
const float itemWidth = 13.0f * m_scale + textWidth + 14.0f * m_scale;
|
||||
const float itemWidth =
|
||||
kLegendSwatchSize * m_scale + 5.0f * m_scale + textWidth + kLegendItemGap * m_scale;
|
||||
if (drawnWidth + itemWidth > legendWidth + 0.5f)
|
||||
{
|
||||
break;
|
||||
}
|
||||
drawnWidth += itemWidth;
|
||||
|
||||
const float swatchSize = 8.0f * m_scale;
|
||||
const float itemLeft = cursorX;
|
||||
const float swatchSize = kLegendSwatchSize * m_scale;
|
||||
|
||||
// 图例条目与被选中的信号保持一致的高亮反馈(与左侧列表选中态呼应)。
|
||||
const bool selected = (m_tab->Selected() == ref);
|
||||
const bool hovered = (ref == m_hoverSignal) || (ref == m_dragSignal);
|
||||
if (selected || hovered)
|
||||
{
|
||||
UiFillRoundRect(g, gd::RectF(itemLeft - 3.0f * m_scale, top - 1.0f * m_scale,
|
||||
itemWidth + 2.0f * m_scale, height + 2.0f * m_scale),
|
||||
3.0f * m_scale, selected ? m_theme->selection : m_theme->hover);
|
||||
}
|
||||
|
||||
UiDrawSwatch(g, gd::RectF(cursorX, top + (height - swatchSize) * 0.5f, swatchSize, swatchSize),
|
||||
resolved.style->color, resolved.style->opacity, 1.5f * m_scale, m_scale);
|
||||
cursorX += swatchSize + 5.0f * m_scale;
|
||||
|
||||
UiText(g, label, m_fonts->uiSmall, gd::RectF(cursorX, top, textWidth + 2.0f, height),
|
||||
m_theme->text, TextAlign::LEFT);
|
||||
cursorX += textWidth + 14.0f * m_scale;
|
||||
// 改名编辑态:在原位置绘制输入框。
|
||||
LegendHit hit;
|
||||
hit.ref = ref;
|
||||
hit.rect = gd::RectF(itemLeft - 3.0f * m_scale, top - 2.0f * m_scale,
|
||||
itemWidth + 2.0f * m_scale, height + 4.0f * m_scale);
|
||||
m_legendHits.push_back(hit);
|
||||
|
||||
if (m_editTarget == PlotEditTarget::SIGNAL_NAME && m_editSignal == ref)
|
||||
{
|
||||
m_editFieldRect = hit.rect;
|
||||
UiDrawEditField(g, hit.rect, m_editText, m_editFresh, m_fonts->uiSmall, m_scale);
|
||||
cursorX += itemWidth;
|
||||
++drawnCount;
|
||||
continue;
|
||||
}
|
||||
|
||||
cursorX += swatchSize + 5.0f * m_scale;
|
||||
UiText(g, label, m_fonts->uiSmall,
|
||||
gd::RectF(cursorX, top, std::max(8.0f, textWidth + 2.0f), height),
|
||||
selected ? m_theme->textStrong : m_theme->text, TextAlign::LEFT);
|
||||
cursorX += textWidth + kLegendItemGap * m_scale;
|
||||
++drawnCount;
|
||||
}
|
||||
|
||||
if (!anyVisible)
|
||||
{
|
||||
UiText(g, L"(无可见信号)", m_fonts->uiSmall,
|
||||
gd::RectF(lane.rect.X + m_labelBandWidth + 6.0f * m_scale, top, lane.rect.Width, height),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
gd::RectF(legendBand.X, top, lane.rect.Width, height), m_theme->textMuted,
|
||||
TextAlign::LEFT);
|
||||
}
|
||||
else if (drawnCount < visibleCount)
|
||||
{
|
||||
@@ -2238,36 +2505,71 @@ namespace jdi
|
||||
return;
|
||||
}
|
||||
|
||||
// 虚影:以源网格的幅值映射构建波形,并整体平移跟随鼠标,形成“拖着曲线走”的观感。
|
||||
LaneVisual ghostLane = m_lanes[static_cast<std::size_t>(sourceLane)];
|
||||
const float follow = static_cast<float>(m_dragMouseY - m_dragStartY);
|
||||
ghostLane.rect.Y += follow;
|
||||
// 虚影:以源网格的幅值映射构建波形,再按鼠标位移整体平移。
|
||||
// 第一帧位移为 0,因此虚影必然精确落在鼠标下方(曲线形态与抓住时完全一致);
|
||||
// 之后纵向与横向均自由跟随,形成“拖着曲线走”的观感。
|
||||
const LaneVisual& sourceVisual = m_lanes[static_cast<std::size_t>(sourceLane)];
|
||||
const float offsetX = static_cast<float>(m_dragMouseX - m_dragStartX);
|
||||
const float offsetY = static_cast<float>(m_dragMouseY - m_dragStartY);
|
||||
|
||||
{
|
||||
const float clipTop = std::max(m_plotRect.Y, m_plotRect.Y);
|
||||
const float clipHeight = m_plotRect.Height;
|
||||
const int saveState = g.Save();
|
||||
g.SetClip(gd::RectF(m_plotRect.X, clipTop, m_plotRect.Width, clipHeight),
|
||||
g.SetClip(gd::RectF(m_plotRect.X, m_plotRect.Y, m_plotRect.Width, m_plotRect.Height),
|
||||
gd::CombineModeIntersect);
|
||||
|
||||
std::vector<gd::PointF> points;
|
||||
std::vector<std::size_t> breaks;
|
||||
BuildPolyline(resolved, ghostLane, points, breaks);
|
||||
BuildPolyline(resolved, sourceVisual, points, breaks);
|
||||
|
||||
gd::Pen ghost(ToGdiColor(WithAlpha(m_theme->accent, 0xC0)),
|
||||
std::max(1.8f, resolved.style->lineWidth) * m_scale);
|
||||
ghost.SetDashStyle(gd::DashStyleDash);
|
||||
ghost.SetStartCap(gd::LineCapRound);
|
||||
ghost.SetEndCap(gd::LineCapRound);
|
||||
for (std::size_t k = 0; k < breaks.size(); ++k)
|
||||
for (gd::PointF& point : points)
|
||||
{
|
||||
const std::size_t start = breaks[k];
|
||||
const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size();
|
||||
if (stop <= start + 1)
|
||||
point.X += offsetX;
|
||||
point.Y += offsetY;
|
||||
}
|
||||
|
||||
const DrawMode mode = resolved.style->mode;
|
||||
const bool drawLine = (mode != DrawMode::SCATTER);
|
||||
const bool drawScatter = (mode == DrawMode::SCATTER || mode == DrawMode::LINE_SCATTER);
|
||||
|
||||
// 虚影保持原信号的颜色与样式(不再是虚线),仅以轻微透明区分“尚未落地”。
|
||||
const std::uint32_t ghostColor = WithAlpha(resolved.style->color,
|
||||
static_cast<unsigned int>(
|
||||
std::max(0.35, static_cast<double>(resolved.style->opacity)) *
|
||||
255.0 + 0.5));
|
||||
|
||||
if (drawLine && !breaks.empty())
|
||||
{
|
||||
gd::Pen ghost(ToGdiColor(ghostColor),
|
||||
std::max(1.0f, resolved.style->lineWidth) * m_scale);
|
||||
ghost.SetLineJoin(gd::LineJoinRound);
|
||||
ghost.SetStartCap(gd::LineCapRound);
|
||||
ghost.SetEndCap(gd::LineCapRound);
|
||||
for (std::size_t k = 0; k < breaks.size(); ++k)
|
||||
{
|
||||
continue;
|
||||
const std::size_t start = breaks[k];
|
||||
const std::size_t stop = (k + 1 < breaks.size()) ? breaks[k + 1] : points.size();
|
||||
if (stop <= start + 1)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
g.DrawLines(&ghost, points.data() + start, static_cast<INT>(stop - start));
|
||||
}
|
||||
}
|
||||
|
||||
if (drawScatter && !points.empty())
|
||||
{
|
||||
const float radius = static_cast<float>(
|
||||
ClampDouble(static_cast<double>(resolved.style->lineWidth) * kScatterRadiusScale,
|
||||
kMinScatterRadius, kMaxScatterRadius) *
|
||||
m_scale);
|
||||
const std::size_t scatterStride =
|
||||
std::max<std::size_t>(1, points.size() / kMaxScatterPoints);
|
||||
gd::SolidBrush brush(ToGdiColor(ghostColor));
|
||||
for (std::size_t k = 0; k < points.size(); k += scatterStride)
|
||||
{
|
||||
g.FillEllipse(&brush, points[k].X - radius, points[k].Y - radius, radius * 2.0f,
|
||||
radius * 2.0f);
|
||||
}
|
||||
g.DrawLines(&ghost, points.data() + start, static_cast<INT>(stop - start));
|
||||
}
|
||||
|
||||
g.Restore(saveState);
|
||||
|
||||
Reference in new issue
Block a user