新增实验性功能「自由标记」:网格内跟随鼠标的纵向光标 + 单击固定数据点 + 浮动面板
需求(用户):网格右键菜单「实验性功能」里新增带 √ 的「自由标记」。开启后网格内跟随鼠标
出现一条仅限本网格的纵向光标(上下各一个把手、不显示时间值),穿过信号时只标注离鼠标
最近的数据点;左键单击把该点固定(带 ✕ 可移除,鼠标移开仍显示、超出视界直接不显示),
后续固定点显示为相对前一个的增量;面板按信号分组列出「值(±增量) = 枚举」+「t:… Δt:…」
(两列右对齐、可双击按秒编辑并自动换位);命中自动点线判据的信号要有吸附提示;
两级 ✕ 都要悬停提示;与选区放大共存时原地单击 = 固定、按下拖动 = 框选。
一、数据层
- Lane 新增 freeMarker 开关、freeMarkerItems(FreeMarkerItem{ref, time})、freeMarkerPanel。
time 是「显示时刻」(画在 TimeToX(time) 处),与光标同一口径;取值必须走
SampleDisplayedSignalAt(内部处理时间偏移与水平镜像)。
- 新增 AddFreeMarker / RemoveFreeMarker / RemoveFreeMarkerAt / RemoveFreeMarkersOf /
FindFreeMarker / HasFreeMarkers。同一信号同一时刻不重复加入(相对容差 1e-12);
freeMarkerItems 保持插入顺序,显示顺序与「前一个固定点」一律由 SortedFreeMarkerIndices
按时间排序得出(删除某项不打乱其它项下标)。
- LaneCarry 同步携带,勾选/取消勾选信号导致网格重建时不会丢掉已固定的点。
二、网格内交互
- ProbeFreeMarker:仅当鼠标所在网格开启该功能时有效;在网格内全部可见信号里挑屏幕纵向
距离鼠标最近的数据点。MinorCellSparse 成立时经 SnapTimeToSample 吸附到真实采样时刻,
绘制侧画一圈圆环作为吸附提示。吸附只看判据,不看当前绘制方式。
- PaintFreeMarker(接在 paintOverlays 里、浮动面板之前):纵向线只在本网格内,上把手尖端
朝下(与常规光标顶部把手一致)、下把手尖端朝上,不画时间读数;已固定点画低透明度细线 +
圆点 + 「值(±增量) = 枚举」胶囊 + 行尾 ✕(悬停高亮 + 提示气泡);
超出视界直接不显示(不复用常规数据点的贴边定格 + 方向三角)。
- 网格内固定点 ✕ 的命中矩形由绘制时登记(m_freeMarkerHits),因为它的位置取决于取值文本宽度。
- OnMouseLeave 清跟随光标与三处悬停态(已固定的点保留)。
三、单击 vs 框选
- OnLButtonDown 在开启该功能的网格上只登记 m_freeMarkerPendingFix,并把拖拽模式设为
「选区放大 ? RUBBER_SELECT : PAN」;OnLButtonUp 里只有「整段操作没有拖动且选区轴向未定」
才 AddFreeMarker,否则照旧框选/平移。
四、浮动面板(EdgePanelKind::MARK / PLOT_CMD_LANE_FREE_MARKER = 12)
- 每个可见信号一行名称(右侧带清除该信号全部固定点的 ✕),名称行下方按时间升序列出该信号的
固定点;没有任何固定点时第一行给灰色提示。
- 行 = 左侧「值(±增量) = 枚举文本」(无尖头括号、首项无增量)+ 右侧两列各自右对齐的
「t:<时刻>s」与「Δt:+<复合单位>」(自动单位匹配)+ 行尾 ✕。
两列右边界存进 EdgePanelLayout(markerTimeRight / markerDeltaRight),保证所有条目纵向对齐。
- 双击行:左半编辑自身时刻(FREE_MARKER_TIME),右半编辑相对前一固定点的间隔
(FREE_MARKER_DELTA,最左边的点没有前一项所以不进入编辑)。提交后只改 time,
顺序由排序函数自动重排(越过后续点即交换条目位置)。
- PanelSlotKey 把每个固定点的信号 + 时刻、全部 marker 行矩形与两列边界一起 mix。
五、一并修掉的两个自伤
1. EdgePanelWidth 的 MARK 分支原本逐行取采样值算文本宽度 —— 它被 EdgePanelLayoutFor 调用,
而后者在 TopPanelAtPoint / SyncPanelOrder 里每次鼠标移动调用数次,等于「鼠标一动就做几十次
采样查询」。改为固定宽度 kFreeMarkerPanelWidth(340 逻辑像素)+ 绘制侧 UiEllipsize。
2. PaintFreeMarker 与跟随光标的胶囊绘制原本是两份重复代码,抽 PaintFreeMarkerCapsule 共用;
FreeMarkerLabelRects 增加 text 出参,避免绘制侧重复求解一次取值。
验证(全离屏,不占用鼠标/键盘/屏幕)
- 新增 .preflight/markcheck 31/31:探测有效与就近选择、原地单击恰好新增 1 个固定点、
选区放大共存(单击固定不缩放 / 拖动缩放不固定)、面板行数与时间升序、首项无增量、
后续项增量与 Δt 正确、两列右对齐、行尾 ✕ 只删 1 项、信号名 ✕ 清空该信号、
编辑 Δt 后移动并重排、深放大时吸附且时刻精确等于采样时刻、视界外固定点不绘制。
- 回归:editcheck 20/20、round3check 31/31、round4check 34/34、round5check 5/5、
convcheck 18/18;check 136/137(唯一失败项是本轮之前就存在的不稳定项,上一轮已用
git stash 在基线复现同一数值);真实 MF4 仍 283/8/18/24 个信号。
- make release / make debug 零警告。
- TargetCheck.exe 833,536 字节(814.0 KiB)—— 比上一轮 +24 KB,首次超出旧的 800 KiB
红线约 14 KB。已做的瘦身见上;剩余为新功能的真实代码量,若要回到 800 KiB 以内需要
改变取舍(动态 CRT / UPX / 砍其它功能),本轮按「实现功能优先」未动。
文档:Documents/agent_mem.md 同步 §10 变更日志、§11.2 第 181-188 条硬约束
与文末本轮小节。
This commit is contained in:
1 parent
53b981a2a9
commit
dd8f63aa0e
10 files changed
+1735
-16
No files matched your search
@@ -142,6 +142,16 @@ namespace jdi
|
||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + buttonWidth);
|
||||
}
|
||||
|
||||
if (kind == EdgePanelKind::MARK)
|
||||
{
|
||||
// 「自由标记」面板:**宽度用固定值**,不做逐行文本测量。
|
||||
// ⚠️ `EdgePanelWidth` 被 `EdgePanelLayoutFor` 调用,而后者又在 `TopPanelAtPoint` /
|
||||
// `SyncPanelOrder` 里被高频调用(每次鼠标移动数次)——若在这里逐行取采样值算文本宽度,
|
||||
// 就变成"鼠标一动就做几十次采样查询",会明显拖慢交互。
|
||||
// 取值的文本超长时由绘制侧的 `UiEllipsize` 截断即可。
|
||||
width = std::max(width, kFreeMarkerPanelWidth * m_scale);
|
||||
}
|
||||
|
||||
// 夹在绘图区宽度之内(过窄的网格也不至于把面板挤到外面)。
|
||||
return std::min(width, std::max(120.0f * m_scale, m_plotRect.Width - 8.0f * m_scale));
|
||||
}
|
||||
@@ -764,6 +774,9 @@ namespace jdi
|
||||
PaintLegendHighlight(g);
|
||||
PaintRubberBand(g);
|
||||
PaintCursor(g, m_lanes);
|
||||
// 「自由标记」的跟随光标与已固定数据点(在统计面板**之下**绘制,
|
||||
// 这样浮动面板仍压在最上层,与其它实验性功能的层序一致)。
|
||||
PaintFreeMarker(g, m_lanes);
|
||||
PaintEdgeCursor(g, m_lanes);
|
||||
PaintSignalInteraction(g);
|
||||
PaintHint(g);
|
||||
@@ -5311,15 +5324,20 @@ namespace jdi
|
||||
const bool centerAxis = (kind == EdgePanelKind::CENTER);
|
||||
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||
const bool freeMarker = (kind == EdgePanelKind::MARK);
|
||||
// 「均值滤波」与「弱峰调整」共用同一套「逐条目 + 底部按钮」排布。
|
||||
const bool itemized = (centerAxis || meanFilter || weakPeak);
|
||||
const bool hasButton = (meanFilter || weakPeak);
|
||||
const bool enabled = meanFilter ? lane.meanFilter
|
||||
: (weakPeak ? lane.weakPeak
|
||||
: (centerAxis ? lane.edgeCenter : lane.edgeCount));
|
||||
: (freeMarker ? lane.freeMarker
|
||||
: (centerAxis ? lane.edgeCenter
|
||||
: lane.edgeCount)));
|
||||
const bool open = meanFilter ? lane.meanFilterPanel.open
|
||||
: (weakPeak ? lane.weakPeakPanel.open
|
||||
: (centerAxis ? lane.edgeCenterPanel.open : lane.edgePanel.open));
|
||||
: (freeMarker ? lane.freeMarkerPanel.open
|
||||
: (centerAxis ? lane.edgeCenterPanel.open
|
||||
: lane.edgePanel.open)));
|
||||
if (!enabled || !open)
|
||||
{
|
||||
return layout;
|
||||
@@ -5397,6 +5415,41 @@ namespace jdi
|
||||
height = titleHeight + pad + static_cast<float>(rowCount) * rowHeight * rowsPerSignal + pad;
|
||||
}
|
||||
|
||||
// 「自由标记」面板:每个可见信号一行名称,下面跟着它的全部固定点;
|
||||
// 没有任何固定点时在名称行下方给一行灰色提示。
|
||||
bool markerAny = false;
|
||||
for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems)
|
||||
{
|
||||
if (item.ref.IsValid())
|
||||
{
|
||||
markerAny = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (freeMarker)
|
||||
{
|
||||
const float nameRow = kFreeMarkerNameRowHeight * m_scale;
|
||||
const float itemRow = kFreeMarkerItemRowHeight * m_scale;
|
||||
float content = 0.0f;
|
||||
std::vector<std::size_t> order;
|
||||
if (!markerAny)
|
||||
{
|
||||
content += nameRow;
|
||||
}
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || !resolved.visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
content += nameRow;
|
||||
SortedFreeMarkerIndices(lane, ref, order);
|
||||
content += static_cast<float>(order.size()) * itemRow;
|
||||
}
|
||||
height = titleHeight + pad + content + pad;
|
||||
}
|
||||
|
||||
// 首次使用(位置为负)时自动放到绘图区**右上角**,并按网格在**当前可见网格中的序数**
|
||||
// 依次错开,避免完全重叠。
|
||||
// ⚠️ 旧实现用 `laneIndex` 错开 —— 上百个网格时 `laneIndex` 很大,
|
||||
@@ -5406,10 +5459,14 @@ namespace jdi
|
||||
// 中间偏下”以内,于是面板始终停**右上区域**,同时保留“依次错开”的观感。
|
||||
const double storedX = meanFilter ? lane.meanFilterPanel.x
|
||||
: (weakPeak ? lane.weakPeakPanel.x
|
||||
: (centerAxis ? lane.edgeCenterPanel.x : lane.edgePanel.x));
|
||||
: (freeMarker ? lane.freeMarkerPanel.x
|
||||
: (centerAxis ? lane.edgeCenterPanel.x
|
||||
: lane.edgePanel.x)));
|
||||
const double storedY = meanFilter ? lane.meanFilterPanel.y
|
||||
: (weakPeak ? lane.weakPeakPanel.y
|
||||
: (centerAxis ? lane.edgeCenterPanel.y : lane.edgePanel.y));
|
||||
: (freeMarker ? lane.freeMarkerPanel.y
|
||||
: (centerAxis ? lane.edgeCenterPanel.y
|
||||
: lane.edgePanel.y)));
|
||||
int openPanelOrdinal = 0;
|
||||
{
|
||||
// ⚠️ 错开量用「**当前真正显示出来的面板**中排在本面板之前的个数」:
|
||||
@@ -5431,11 +5488,14 @@ namespace jdi
|
||||
return lane.meanFilter && lane.meanFilterPanel.open;
|
||||
case EdgePanelKind::PEAK:
|
||||
return lane.weakPeak && lane.weakPeakPanel.open;
|
||||
case EdgePanelKind::MARK:
|
||||
return lane.freeMarker && lane.freeMarkerPanel.open;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
static const EdgePanelKind kOrder[4] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
|
||||
EdgePanelKind::MEAN, EdgePanelKind::PEAK};
|
||||
static const EdgePanelKind kOrder[5] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
|
||||
EdgePanelKind::MEAN, EdgePanelKind::PEAK,
|
||||
EdgePanelKind::MARK};
|
||||
int openSeen = 0;
|
||||
bool found = false;
|
||||
for (std::size_t lane = 0; lane < m_lanes.size() && !found; ++lane)
|
||||
@@ -5617,6 +5677,61 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (freeMarker)
|
||||
{
|
||||
// 「自由标记」面板:每个可见信号一行名称(右侧带「清除本信号全部固定点」✕),
|
||||
// 下面逐行列出它的固定点(组内按时间升序 == 从左到右),行尾再带一个 ✕。
|
||||
const float nameRow = kFreeMarkerNameRowHeight * m_scale;
|
||||
const float itemRow = kFreeMarkerItemRowHeight * m_scale;
|
||||
const float closeSize = kFreeMarkerCloseSize * m_scale;
|
||||
const float gap = 4.0f * m_scale;
|
||||
// 时间两列右对齐:Δt 列贴面板右内边界(✕ 左侧),t 列在它左边。
|
||||
layout.markerDeltaRight = left + width - pad - closeSize - gap;
|
||||
layout.markerTimeRight = layout.markerDeltaRight - 74.0f * m_scale;
|
||||
|
||||
float rowY = top + titleHeight + pad;
|
||||
std::vector<std::size_t> order;
|
||||
bool markerAny = false;
|
||||
for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems)
|
||||
{
|
||||
markerAny = markerAny || item.ref.IsValid();
|
||||
}
|
||||
if (!markerAny)
|
||||
{
|
||||
// 还没有任何固定点:第一行留作提示文案(绘制时画「在网格内单击以固定数据点」)。
|
||||
rowY += nameRow;
|
||||
}
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || !resolved.visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
layout.rowRefs.push_back(ref);
|
||||
SortedFreeMarkerIndices(lane, ref, order);
|
||||
const bool hasMarkers = !order.empty();
|
||||
layout.rowNameRects.push_back(gd::RectF(left + pad, rowY,
|
||||
std::max(0.0f, width - pad * 2.0f - closeSize - gap),
|
||||
nameRow));
|
||||
layout.rowNameCloses.push_back(
|
||||
hasMarkers ? gd::RectF(left + width - pad - closeSize,
|
||||
rowY + (nameRow - closeSize) * 0.5f, closeSize, closeSize)
|
||||
: gd::RectF());
|
||||
rowY += nameRow;
|
||||
for (const std::size_t itemIndex : order)
|
||||
{
|
||||
EdgePanelLayout::MarkerRow markerRow;
|
||||
markerRow.itemIndex = itemIndex;
|
||||
markerRow.rect = gd::RectF(left + pad, rowY, width - pad * 2.0f, itemRow);
|
||||
markerRow.close = gd::RectF(left + width - pad - closeSize,
|
||||
rowY + (itemRow - closeSize) * 0.5f, closeSize, closeSize);
|
||||
layout.markerRows.push_back(markerRow);
|
||||
rowY += itemRow;
|
||||
}
|
||||
}
|
||||
}
|
||||
return layout;
|
||||
}
|
||||
|
||||
@@ -6098,8 +6213,34 @@ namespace jdi
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
// 「自由标记」:固定点行/名称 ✕ 的矩形、时间列边界与每个固定点的身份 + 时刻都要入键。
|
||||
for (const EdgePanelLayout::MarkerRow& row : layout.markerRows)
|
||||
{
|
||||
mixRect(row.rect);
|
||||
mixRect(row.close);
|
||||
mix(static_cast<std::uint64_t>(row.itemIndex + 2));
|
||||
}
|
||||
for (const gd::RectF& rect : layout.rowNameCloses)
|
||||
{
|
||||
mixRect(rect);
|
||||
}
|
||||
mixFloat(layout.markerTimeRight);
|
||||
mixFloat(layout.markerDeltaRight);
|
||||
|
||||
const Lane& lane = Tab().Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
if (kind == EdgePanelKind::MARK)
|
||||
{
|
||||
// 固定点的**内容**(哪个信号、什么时刻)直接决定面板文字与行数,必须进键;
|
||||
// 否则新增/删除一个固定点后可能贴回旧像素。
|
||||
for (const Lane::FreeMarkerItem& item : lane.freeMarkerItems)
|
||||
{
|
||||
mix(static_cast<std::uint64_t>(item.ref.groupIndex + 2));
|
||||
mix(static_cast<std::uint64_t>(item.ref.signalIndex + 2));
|
||||
std::uint64_t bits = 0;
|
||||
std::memcpy(&bits, &item.time, sizeof(bits));
|
||||
mix(bits);
|
||||
}
|
||||
}
|
||||
for (const SignalRef& ref : layout.rowRefs)
|
||||
{
|
||||
mix(ref.IsValid() ? 1ull : 0ull);
|
||||
@@ -6214,6 +6355,445 @@ namespace jdi
|
||||
return hash;
|
||||
}
|
||||
|
||||
bool PlotView::FreeMarkerValue(const Lane& lane, const Lane::FreeMarkerItem& item, double& value,
|
||||
double& enumValue) const
|
||||
{
|
||||
if (m_dataSet == nullptr)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const ResolvedSignal resolved = ResolveSignal(item.ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr || resolved.axis == nullptr ||
|
||||
!resolved.visible)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
// 固定点存的是**显示时刻**,因此要用 `SampleDisplayedSignalAt`(内部处理时间偏移与
|
||||
// 水平镜像)。散点模式下不插值,取最近采样点,与光标读数的规则一致。
|
||||
const bool interpolate = (resolved.style->mode != DrawMode::SCATTER);
|
||||
double sampled = 0.0;
|
||||
if (!SampleDisplayedSignalAt(*resolved.axis, *resolved.data, *resolved.style, item.time,
|
||||
interpolate, sampled) ||
|
||||
!std::isfinite(sampled))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
(void)lane;
|
||||
enumValue = sampled; // 枚举按**原值**查
|
||||
value = ApplyDrawTransform(*resolved.style, sampled); // 读数与画出来的波形一致
|
||||
return std::isfinite(value);
|
||||
}
|
||||
|
||||
bool PlotView::FreeMarkerDelta(const Lane& lane, std::size_t itemIndex, double value, bool& hasPrev,
|
||||
double& delta, double& timeDelta) const
|
||||
{
|
||||
hasPrev = false;
|
||||
delta = 0.0;
|
||||
timeDelta = 0.0;
|
||||
if (itemIndex >= lane.freeMarkerItems.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const Lane::FreeMarkerItem& item = lane.freeMarkerItems[itemIndex];
|
||||
std::vector<std::size_t> order;
|
||||
SortedFreeMarkerIndices(lane, item.ref, order);
|
||||
// 找到自己在**排序后序列**里的位置;前一个即「左边最近的那个固定点」。
|
||||
std::size_t position = order.size();
|
||||
for (std::size_t index = 0; index < order.size(); ++index)
|
||||
{
|
||||
if (order[index] == itemIndex)
|
||||
{
|
||||
position = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (position == order.size() || position == 0)
|
||||
{
|
||||
return true; // 最左边(或找不到):没有增量
|
||||
}
|
||||
const Lane::FreeMarkerItem& previous = lane.freeMarkerItems[order[position - 1]];
|
||||
double previousValue = 0.0;
|
||||
double previousEnum = 0.0;
|
||||
if (!FreeMarkerValue(lane, previous, previousValue, previousEnum))
|
||||
{
|
||||
return true; // 前一个点没有有效取值:不显示增量(避免编造一个数)
|
||||
}
|
||||
hasPrev = true;
|
||||
delta = value - previousValue;
|
||||
timeDelta = item.time - previous.time;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::FreeMarkerLabelRects(int laneIndex, std::size_t itemIndex, gd::RectF& label,
|
||||
gd::RectF& close, std::wstring& text) const
|
||||
{
|
||||
label = gd::RectF();
|
||||
close = gd::RectF();
|
||||
text.clear();
|
||||
if (m_tab == nullptr || m_fonts == nullptr || laneIndex < 0 ||
|
||||
laneIndex >= static_cast<int>(m_tab->Lanes().size()) ||
|
||||
laneIndex >= static_cast<int>(m_lanes.size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
if (itemIndex >= lane.freeMarkerItems.size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const Lane::FreeMarkerItem& item = lane.freeMarkerItems[itemIndex];
|
||||
double value = 0.0;
|
||||
double enumValue = 0.0;
|
||||
if (!FreeMarkerValue(lane, item, value, enumValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool hasPrev = false;
|
||||
double delta = 0.0;
|
||||
double timeDelta = 0.0;
|
||||
FreeMarkerDelta(lane, itemIndex, value, hasPrev, delta, timeDelta);
|
||||
|
||||
// 超出视界**直接不显示**(用户明确要求:不按常规数据点那样贴边定格 + 三角指示)。
|
||||
const float rawX = TimeToX(item.time);
|
||||
if (rawX < m_plotRect.X - 0.5f || rawX > m_plotRect.X + m_plotRect.Width + 0.5f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const LaneVisual& visual = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||||
const float clipTop = std::max(visual.rect.Y, m_plotRect.Y);
|
||||
const float clipBottom =
|
||||
std::min(visual.rect.Y + visual.rect.Height, m_plotRect.Y + m_plotRect.Height);
|
||||
const float rawY = ValueToY(value, visual);
|
||||
if (!std::isfinite(rawY) || rawY < clipTop - 0.5f || rawY > clipBottom + 0.5f)
|
||||
{
|
||||
return false; // 纵向越界同样直接不显示
|
||||
}
|
||||
|
||||
// 枚举文本按**采样原值**查找(与光标读数完全一致)。
|
||||
const ResolvedSignal labelResolved = ResolveSignal(item.ref);
|
||||
const std::wstring* enumText = (labelResolved.data != nullptr)
|
||||
? labelResolved.data->enumText.Find(enumValue)
|
||||
: nullptr;
|
||||
text = FreeMarkerValueText(value, hasPrev, delta, enumText);
|
||||
|
||||
const float closeSize = kFreeMarkerCloseSize * m_scale;
|
||||
const float gap = kFreeMarkerCloseGap * m_scale;
|
||||
const float textWidth = UiMeasureTextWidth(m_fonts->monoSmall, text) + 10.0f * m_scale;
|
||||
const float textHeight = 17.0f * m_scale;
|
||||
const float totalWidth = textWidth + gap + closeSize;
|
||||
|
||||
float left = rawX + 6.0f * m_scale;
|
||||
if (left + totalWidth > m_plotRect.X + m_plotRect.Width - 2.0f * m_scale)
|
||||
{
|
||||
left = rawX - 6.0f * m_scale - totalWidth;
|
||||
}
|
||||
left = std::max(m_plotRect.X + 2.0f * m_scale,
|
||||
std::min(left, m_plotRect.X + m_plotRect.Width - totalWidth - 2.0f * m_scale));
|
||||
float top = rawY - textHeight * 0.5f;
|
||||
top = std::max(clipTop + 1.0f * m_scale, std::min(top, clipBottom - textHeight - 1.0f * m_scale));
|
||||
|
||||
label = gd::RectF(left, top, textWidth, textHeight);
|
||||
close = gd::RectF(left + textWidth + gap, top + (textHeight - closeSize) * 0.5f, closeSize,
|
||||
closeSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
void PlotView::PaintFreeMarkerCapsule(gd::Graphics& g, const gd::RectF& label,
|
||||
const std::wstring& text, std::uint32_t color,
|
||||
const gd::RectF& close, bool closeHot)
|
||||
{
|
||||
const UiInk ink = UiContrastInk(color);
|
||||
UiFillRoundRect(g, label, 3.0f * m_scale, WithAlpha(color, 0xF0));
|
||||
DrawLabelOutline(g, label, 3.0f * m_scale, ink.outline);
|
||||
UiText(g, text, m_fonts->monoSmall, label, ink.text, TextAlign::CENTER);
|
||||
if (close.Width <= 0.0f)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// ✕ 按钮:悬停时用 danger 底色 + 叉(与面板里的 ✕ 同一套语义)。
|
||||
UiFillRoundRect(g, close, 3.0f * m_scale,
|
||||
closeHot ? WithAlpha(m_theme->danger, 0x66) : WithAlpha(color, 0xC0));
|
||||
const float crossPad = 3.5f * m_scale;
|
||||
const std::uint32_t crossColor = closeHot ? m_theme->danger : ink.text;
|
||||
UiDrawLine(g, close.X + crossPad, close.Y + crossPad, close.X + close.Width - crossPad,
|
||||
close.Y + close.Height - crossPad, crossColor, 1.4f * m_scale);
|
||||
UiDrawLine(g, close.X + close.Width - crossPad, close.Y + crossPad, close.X + crossPad,
|
||||
close.Y + close.Height - crossPad, crossColor, 1.4f * m_scale);
|
||||
}
|
||||
|
||||
void PlotView::PaintFreeMarker(gd::Graphics& g, const std::vector<LaneVisual>& lanes)
|
||||
{
|
||||
m_freeMarkerHits.clear();
|
||||
if (m_tab == nullptr || m_dataSet == nullptr || m_fonts == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const std::uint32_t lineColor = m_theme->accent;
|
||||
const int saveState = g.Save();
|
||||
g.SetClip(m_plotRect, gd::CombineModeIntersect);
|
||||
|
||||
const std::vector<Lane>& tabLanes = m_tab->Lanes();
|
||||
const std::size_t count = std::min(lanes.size(), tabLanes.size());
|
||||
|
||||
// ---- (1) 已固定的数据点(含各自的 ✕)----
|
||||
for (std::size_t laneIndex = 0; laneIndex < count; ++laneIndex)
|
||||
{
|
||||
const Lane& lane = tabLanes[laneIndex];
|
||||
if (!lane.freeMarker || lane.freeMarkerItems.empty())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
for (std::size_t itemIndex = 0; itemIndex < lane.freeMarkerItems.size(); ++itemIndex)
|
||||
{
|
||||
gd::RectF label;
|
||||
gd::RectF close;
|
||||
std::wstring text;
|
||||
if (!FreeMarkerLabelRects(static_cast<int>(laneIndex), itemIndex, label, close, text))
|
||||
{
|
||||
continue; // 超出视界直接不显示(不贴边、不画三角)
|
||||
}
|
||||
const Lane::FreeMarkerItem& item = lane.freeMarkerItems[itemIndex];
|
||||
const ResolvedSignal resolved = ResolveSignal(item.ref);
|
||||
if (resolved.style == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
const LaneVisual& visual = lanes[laneIndex];
|
||||
const float clipTop = std::max(visual.rect.Y, m_plotRect.Y);
|
||||
const float clipBottom =
|
||||
std::min(visual.rect.Y + visual.rect.Height, m_plotRect.Y + m_plotRect.Height);
|
||||
|
||||
double value = 0.0;
|
||||
double enumValue = 0.0;
|
||||
const bool valueOk = FreeMarkerValue(lane, item, value, enumValue);
|
||||
|
||||
// 纵向细线(仅限本网格,低透明度):让用户看得出固定点在哪一刻。
|
||||
const float x = TimeToX(item.time);
|
||||
UiDrawLine(g, x, clipTop, x, clipBottom, WithAlpha(signalColor, 0x70),
|
||||
1.0f * m_scale);
|
||||
|
||||
// 数据点:与光标的数据点同一画法(圆点)。
|
||||
const float radius = 3.4f * m_scale;
|
||||
gd::SolidBrush dot(ToGdiColor(signalColor));
|
||||
const float dotY = valueOk ? ValueToY(value, visual) : (clipTop + clipBottom) * 0.5f;
|
||||
g.FillEllipse(&dot, x - radius, dotY - radius, radius * 2.0f, radius * 2.0f);
|
||||
|
||||
// ✕ 按钮的悬停态决定它的配色,因此先算再画胶囊。
|
||||
const bool hot = (m_freeMarkerHoverLane == static_cast<int>(laneIndex) &&
|
||||
m_freeMarkerHoverItem == itemIndex);
|
||||
PaintFreeMarkerCapsule(g, label, text, signalColor, close, hot);
|
||||
|
||||
FreeMarkerHit hit;
|
||||
hit.laneIndex = static_cast<int>(laneIndex);
|
||||
hit.itemIndex = itemIndex;
|
||||
hit.close = close;
|
||||
m_freeMarkerHits.push_back(hit);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- (2) 跟随鼠标的纵向光标(仅限本网格,上下各一个三角把手)----
|
||||
const FreeMarkerProbe& probe = m_freeMarker;
|
||||
if (probe.valid && probe.laneIndex >= 0 &&
|
||||
probe.laneIndex < static_cast<int>(lanes.size()))
|
||||
{
|
||||
const LaneVisual& visual = lanes[static_cast<std::size_t>(probe.laneIndex)];
|
||||
const float clipTop = std::max(visual.rect.Y, m_plotRect.Y);
|
||||
const float clipBottom =
|
||||
std::min(visual.rect.Y + visual.rect.Height, m_plotRect.Y + m_plotRect.Height);
|
||||
float x = probe.x;
|
||||
x = std::max(visual.rect.X + kFreeMarkerLineInset * m_scale,
|
||||
std::min(x, visual.rect.X + visual.rect.Width - kFreeMarkerLineInset * m_scale));
|
||||
if (clipBottom > clipTop)
|
||||
{
|
||||
UiDrawLine(g, x, clipTop, x, clipBottom, lineColor, 1.4f * m_scale);
|
||||
// 上把手:尖端朝下(与常规光标的顶部把手一致);下把手:尖端朝上(用户明确要求)。
|
||||
const float arm = 5.0f * m_scale;
|
||||
gd::SolidBrush handle(ToGdiColor(lineColor));
|
||||
gd::PointF top[3];
|
||||
top[0] = gd::PointF(x - arm, clipTop);
|
||||
top[1] = gd::PointF(x + arm, clipTop);
|
||||
top[2] = gd::PointF(x, clipTop + arm * 1.4f);
|
||||
g.FillPolygon(&handle, top, 3);
|
||||
gd::PointF bottom[3];
|
||||
bottom[0] = gd::PointF(x - arm, clipBottom);
|
||||
bottom[1] = gd::PointF(x + arm, clipBottom);
|
||||
bottom[2] = gd::PointF(x, clipBottom - arm * 1.4f);
|
||||
g.FillPolygon(&handle, bottom, 3);
|
||||
}
|
||||
|
||||
// 就近数据点的取值胶囊(**不显示时间读数**,用户明确要求)。
|
||||
if (probe.ref.IsValid() && probe.time >= Tab().ViewMin() && probe.time <= Tab().ViewMax())
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(probe.ref);
|
||||
if (resolved.data != nullptr && resolved.style != nullptr)
|
||||
{
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
const std::wstring* enumText = resolved.data->enumText.Find(probe.enumValue);
|
||||
const std::wstring text = FreeMarkerValueText(probe.value, false, 0.0, enumText);
|
||||
const float textWidth =
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, text) + 10.0f * m_scale;
|
||||
const float textHeight = 17.0f * m_scale;
|
||||
float left = x + 6.0f * m_scale;
|
||||
if (left + textWidth > m_plotRect.X + m_plotRect.Width - 2.0f * m_scale)
|
||||
{
|
||||
left = x - 6.0f * m_scale - textWidth;
|
||||
}
|
||||
left = std::max(m_plotRect.X + 2.0f * m_scale,
|
||||
std::min(left, m_plotRect.X + m_plotRect.Width - textWidth -
|
||||
2.0f * m_scale));
|
||||
float top = probe.y - textHeight * 0.5f;
|
||||
top = std::max(clipTop + 1.0f * m_scale,
|
||||
std::min(top, clipBottom - textHeight - 1.0f * m_scale));
|
||||
const gd::RectF label(left, top, textWidth, textHeight);
|
||||
PaintFreeMarkerCapsule(g, label, text, signalColor, gd::RectF(), false);
|
||||
|
||||
// 吸附提示:命中自动点线判据时数据点被吸附到采样点,用一圈描边示意
|
||||
//(与"可拖拽"的光晕区分:这里只是单点圆环,不描整条曲线)。
|
||||
const float radius = 3.4f * m_scale;
|
||||
gd::SolidBrush dot(ToGdiColor(signalColor));
|
||||
g.FillEllipse(&dot, x - radius, probe.y - radius, radius * 2.0f, radius * 2.0f);
|
||||
if (probe.snapped)
|
||||
{
|
||||
gd::Pen ring(ToGdiColor(lineColor), 1.2f * m_scale);
|
||||
g.DrawEllipse(&ring, x - radius - 3.0f * m_scale, probe.y - radius - 3.0f * m_scale,
|
||||
(radius + 3.0f * m_scale) * 2.0f, (radius + 3.0f * m_scale) * 2.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g.Restore(saveState);
|
||||
|
||||
// 悬停在固定点 ✕ 上时给出提示(与面板 ✕ 的提示风格一致)。
|
||||
if (m_freeMarkerHoverLane >= 0 && m_freeMarkerHoverItem != static_cast<std::size_t>(-1))
|
||||
{
|
||||
PaintFreeMarkerTooltip(g, L"点击移除此固定数据点");
|
||||
}
|
||||
}
|
||||
|
||||
void PlotView::PaintFreeMarkerTooltip(gd::Graphics& g, const std::wstring& text)
|
||||
{
|
||||
if (text.empty() || m_fonts == nullptr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
const float pad = 5.0f * m_scale;
|
||||
const float width = UiMeasureTextWidth(m_fonts->uiSmall, text) + pad * 2.0f;
|
||||
const float height = 18.0f * m_scale;
|
||||
float left = static_cast<float>(m_mouseX) + 12.0f * m_scale;
|
||||
float top = static_cast<float>(m_mouseY) + 16.0f * m_scale;
|
||||
if (left + width > m_plotRect.X + m_plotRect.Width - 2.0f * m_scale)
|
||||
{
|
||||
left = m_plotRect.X + m_plotRect.Width - width - 2.0f * m_scale;
|
||||
}
|
||||
if (top + height > m_plotRect.Y + m_plotRect.Height - 2.0f * m_scale)
|
||||
{
|
||||
top = static_cast<float>(m_mouseY) - height - 10.0f * m_scale;
|
||||
}
|
||||
const gd::RectF bubble(left, top, width, height);
|
||||
UiFillRoundRect(g, bubble, 4.0f * m_scale, m_theme->readoutBg);
|
||||
UiDrawRoundRect(g, bubble, 4.0f * m_scale, m_theme->borderStrong, 1.0f);
|
||||
UiText(g, text, m_fonts->uiSmall, bubble, m_theme->textStrong, TextAlign::CENTER);
|
||||
}
|
||||
|
||||
PlotView::FreeMarkerProbe PlotView::ProbeFreeMarker(int mouseX, int mouseY) const
|
||||
{
|
||||
FreeMarkerProbe probe;
|
||||
if (m_tab == nullptr || m_dataSet == nullptr ||
|
||||
!UiPointInRect(m_plotRect, static_cast<float>(mouseX), static_cast<float>(mouseY)))
|
||||
{
|
||||
return probe;
|
||||
}
|
||||
const int laneIndex = LaneAtY(mouseY);
|
||||
if (laneIndex < 0 || laneIndex >= static_cast<int>(m_lanes.size()) ||
|
||||
laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return probe;
|
||||
}
|
||||
const Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
if (!lane.freeMarker)
|
||||
{
|
||||
return probe;
|
||||
}
|
||||
const LaneVisual& visual = m_lanes[static_cast<std::size_t>(laneIndex)];
|
||||
|
||||
probe.valid = true;
|
||||
probe.laneIndex = laneIndex;
|
||||
probe.time = XToTime(static_cast<float>(mouseX));
|
||||
probe.x = static_cast<float>(mouseX);
|
||||
|
||||
// 在网格内所有可见信号中找**离鼠标最近**的那个数据点。
|
||||
double bestDistance = std::numeric_limits<double>::infinity();
|
||||
double bestTime = probe.time;
|
||||
double bestValue = 0.0;
|
||||
double bestEnum = 0.0;
|
||||
float bestY = 0.0f;
|
||||
bool bestSnapped = false;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr || resolved.axis == nullptr ||
|
||||
!resolved.visible)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 「自动点线」判据成立时吸附到最近采样点(与光标吸附同一规则)。
|
||||
// ⚠️ 用户明确要求:只要判据成立就吸附,**不**管当前是散点还是用户自己改过的样式。
|
||||
const bool sparse = MinorCellSparse(*resolved.axis, *resolved.data, resolved.style->offsetTime);
|
||||
double time = probe.time;
|
||||
bool snapped = false;
|
||||
if (sparse)
|
||||
{
|
||||
double candidate = 0.0;
|
||||
if (SnapTimeToSample(probe.time, candidate))
|
||||
{
|
||||
time = candidate;
|
||||
snapped = true;
|
||||
}
|
||||
}
|
||||
const bool interpolate = (resolved.style->mode != DrawMode::SCATTER);
|
||||
double sampled = 0.0;
|
||||
if (!SampleDisplayedSignalAt(*resolved.axis, *resolved.data, *resolved.style, time,
|
||||
interpolate, sampled) ||
|
||||
!std::isfinite(sampled))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const double value = ApplyDrawTransform(*resolved.style, sampled);
|
||||
if (!std::isfinite(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const float y = ValueToY(value, visual);
|
||||
const double distance = std::fabs(static_cast<double>(y) - static_cast<double>(mouseY));
|
||||
if (distance >= bestDistance)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
bestDistance = distance;
|
||||
bestTime = time;
|
||||
bestValue = value;
|
||||
bestEnum = sampled;
|
||||
bestY = y;
|
||||
bestSnapped = snapped;
|
||||
probe.ref = ref;
|
||||
}
|
||||
|
||||
if (!probe.ref.IsValid())
|
||||
{
|
||||
return probe; // 网格内没有任何可用数据点:只画纵向线
|
||||
}
|
||||
probe.time = bestTime;
|
||||
probe.x = TimeToX(bestTime);
|
||||
probe.value = bestValue;
|
||||
probe.enumValue = bestEnum;
|
||||
probe.y = bestY;
|
||||
probe.snapped = bestSnapped;
|
||||
return probe;
|
||||
}
|
||||
|
||||
void PlotView::PaintEdgeCursor(gd::Graphics& g, const std::vector<LaneVisual>& lanes)
|
||||
{
|
||||
if (m_tab == nullptr || m_dataSet == nullptr)
|
||||
@@ -6363,6 +6943,7 @@ namespace jdi
|
||||
const bool centerAxis = (kind == EdgePanelKind::CENTER);
|
||||
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||
const bool freeMarker = (kind == EdgePanelKind::MARK);
|
||||
// 「均值滤波」与「弱峰调整」共用「逐条目 + 底部按钮」排布。
|
||||
const bool itemized = (centerAxis || meanFilter || weakPeak);
|
||||
const bool hasButton = (meanFilter || weakPeak);
|
||||
@@ -6470,8 +7051,9 @@ namespace jdi
|
||||
const std::wstring title =
|
||||
meanFilter ? std::wstring(L"均值滤波")
|
||||
: (weakPeak ? std::wstring(kWeakPeakTitle)
|
||||
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") +
|
||||
EdgeRangeSuffix()));
|
||||
: (freeMarker ? std::wstring(L"自由标记")
|
||||
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") +
|
||||
EdgeRangeSuffix())));
|
||||
UiText(g, title, m_fonts->uiSmall,
|
||||
gd::RectF(badgeRect.X + badgeRect.Width + 8.0f * m_scale, panel.Y,
|
||||
panel.Width - badgeRect.Width - 8.0f * m_scale - titleHeight,
|
||||
@@ -6719,6 +7301,138 @@ namespace jdi
|
||||
}
|
||||
}
|
||||
|
||||
// 「自由标记」面板:逐信号一行名称(带清除全部固定点的 ✕),
|
||||
// 下面按时间升序列出该信号的固定点(`值(±增量) = 枚举` + 右对齐的 `t:… Δt:…` + 行尾 ✕)。
|
||||
if (freeMarker)
|
||||
{
|
||||
const float closeSize = kFreeMarkerCloseSize * m_scale;
|
||||
const float gap = 4.0f * m_scale;
|
||||
bool markerAny = false;
|
||||
for (const Lane::FreeMarkerItem& item : tabLane.freeMarkerItems)
|
||||
{
|
||||
markerAny = markerAny || item.ref.IsValid();
|
||||
}
|
||||
if (!markerAny)
|
||||
{
|
||||
UiText(g, L"在网格内单击可固定数据点",
|
||||
m_fonts->uiSmall,
|
||||
gd::RectF(panel.X + pad, panel.Y + titleHeight + pad,
|
||||
panel.Width - pad * 2.0f, kFreeMarkerNameRowHeight * m_scale),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
}
|
||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||
{
|
||||
const SignalRef& ref = layout.rowRefs[rowIndex];
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
// 名称行矩形直接取自布局(与 ✕ 同一套几何,绘制侧不反推)。
|
||||
const gd::RectF nameRect =
|
||||
(rowIndex < layout.rowNameRects.size())
|
||||
? layout.rowNameRects[rowIndex]
|
||||
: gd::RectF(panel.X + pad, panel.Y + titleHeight + pad,
|
||||
panel.Width - pad * 2.0f - (closeSize + gap),
|
||||
kFreeMarkerNameRowHeight * m_scale);
|
||||
UiText(g, UiEllipsize(resolved.data->name, m_fonts->uiSmall, nameRect.Width),
|
||||
m_fonts->uiSmall, nameRect, signalColor, TextAlign::LEFT);
|
||||
|
||||
// 信号名右侧的 ✕:清除该信号的**全部**固定点。
|
||||
if (rowIndex < layout.rowNameCloses.size() &&
|
||||
layout.rowNameCloses[rowIndex].Width > 0.0f)
|
||||
{
|
||||
const gd::RectF& closeRect = layout.rowNameCloses[rowIndex];
|
||||
const bool hot = (m_freeMarkerPanelHoverLane == static_cast<int>(index) &&
|
||||
m_freeMarkerPanelHoverSignal == ref &&
|
||||
m_freeMarkerPanelHoverItem == static_cast<std::size_t>(-1));
|
||||
UiFillRoundRect(g, closeRect, 3.0f * m_scale,
|
||||
hot ? WithAlpha(m_theme->danger, 0x66) : WithAlpha(signalColor, 0xC0));
|
||||
const float crossPad = 3.5f * m_scale;
|
||||
const std::uint32_t crossColor = hot ? m_theme->danger : m_theme->textOnAccent;
|
||||
UiDrawLine(g, closeRect.X + crossPad, closeRect.Y + crossPad,
|
||||
closeRect.X + closeRect.Width - crossPad,
|
||||
closeRect.Y + closeRect.Height - crossPad, crossColor,
|
||||
1.4f * m_scale);
|
||||
UiDrawLine(g, closeRect.X + closeRect.Width - crossPad,
|
||||
closeRect.Y + crossPad, closeRect.X + crossPad,
|
||||
closeRect.Y + closeRect.Height - crossPad, crossColor,
|
||||
1.4f * m_scale);
|
||||
}
|
||||
}
|
||||
|
||||
// 固定点行(已按信号分组 + 组内时间升序展开)。
|
||||
for (const EdgePanelLayout::MarkerRow& markerRow : layout.markerRows)
|
||||
{
|
||||
const Lane::FreeMarkerItem& item = tabLane.freeMarkerItems[markerRow.itemIndex];
|
||||
const ResolvedSignal resolved = ResolveSignal(item.ref);
|
||||
if (resolved.data == nullptr || resolved.style == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
double value = 0.0;
|
||||
double enumValue = 0.0;
|
||||
const bool valueOk = FreeMarkerValue(tabLane, item, value, enumValue);
|
||||
bool hasPrev = false;
|
||||
double delta = 0.0;
|
||||
double timeDelta = 0.0;
|
||||
if (valueOk)
|
||||
{
|
||||
FreeMarkerDelta(tabLane, markerRow.itemIndex, value, hasPrev, delta,
|
||||
timeDelta);
|
||||
}
|
||||
const std::wstring* enumText =
|
||||
resolved.data->enumText.Find(enumValue);
|
||||
const std::wstring valueText =
|
||||
FreeMarkerValueText(valueOk ? value : 0.0, hasPrev, delta, enumText);
|
||||
const std::wstring timeText = FreeMarkerTimeText(item.time);
|
||||
const std::wstring deltaText = FreeMarkerDeltaText(hasPrev, timeDelta);
|
||||
|
||||
const float timeRight = layout.markerTimeRight;
|
||||
const float deltaRight = layout.markerDeltaRight;
|
||||
const float valueWidth =
|
||||
std::max(0.0f, timeRight - gap * 2.0f - (panel.X + pad));
|
||||
UiText(g, UiEllipsize(valueText, m_fonts->monoSmall, valueWidth),
|
||||
m_fonts->monoSmall,
|
||||
gd::RectF(panel.X + pad, markerRow.rect.Y, valueWidth,
|
||||
markerRow.rect.Height),
|
||||
m_theme->text, TextAlign::LEFT);
|
||||
|
||||
// 时间列各自右对齐 ⇒ 所有条目上下对齐(用户明确要求)。
|
||||
const float timeWidth = UiMeasureTextWidth(m_fonts->monoSmall, timeText);
|
||||
UiText(g, timeText, m_fonts->monoSmall,
|
||||
gd::RectF(timeRight - timeWidth, markerRow.rect.Y, timeWidth,
|
||||
markerRow.rect.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
if (!deltaText.empty())
|
||||
{
|
||||
const float deltaWidth = UiMeasureTextWidth(m_fonts->monoSmall, deltaText);
|
||||
UiText(g, deltaText, m_fonts->monoSmall,
|
||||
gd::RectF(deltaRight - deltaWidth, markerRow.rect.Y, deltaWidth,
|
||||
markerRow.rect.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT);
|
||||
}
|
||||
|
||||
// 行尾 ✕:移除该固定点。
|
||||
const gd::RectF& closeRect = markerRow.close;
|
||||
const bool hot = (m_freeMarkerPanelHoverLane == static_cast<int>(index) &&
|
||||
m_freeMarkerPanelHoverItem == markerRow.itemIndex);
|
||||
UiFillRoundRect(g, closeRect, 3.0f * m_scale,
|
||||
hot ? WithAlpha(m_theme->danger, 0x66) : m_theme->cardBg);
|
||||
UiDrawRoundRect(g, closeRect, 3.0f * m_scale,
|
||||
hot ? m_theme->danger : m_theme->border, 1.0f);
|
||||
const float crossPad = 3.5f * m_scale;
|
||||
const std::uint32_t crossColor = hot ? m_theme->danger : m_theme->textMuted;
|
||||
UiDrawLine(g, closeRect.X + crossPad, closeRect.Y + crossPad,
|
||||
closeRect.X + closeRect.Width - crossPad,
|
||||
closeRect.Y + closeRect.Height - crossPad, crossColor, 1.2f * m_scale);
|
||||
UiDrawLine(g, closeRect.X + closeRect.Width - crossPad, closeRect.Y + crossPad,
|
||||
closeRect.X + crossPad, closeRect.Y + closeRect.Height - crossPad,
|
||||
crossColor, 1.2f * m_scale);
|
||||
}
|
||||
}
|
||||
|
||||
float rowY = panel.Y + titleHeight + pad;
|
||||
for (const SignalRef& ref : tabLane.signals)
|
||||
{
|
||||
@@ -6727,9 +7441,9 @@ namespace jdi
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if (itemized)
|
||||
if (itemized || freeMarker)
|
||||
{
|
||||
// 逐条目绘制已在上面完成。
|
||||
// 逐条目绘制已在上面完成(自由标记同样是逐条目排布)。
|
||||
continue;
|
||||
}
|
||||
const std::uint32_t signalColor = StyleColor(*resolved.style);
|
||||
|
||||
Reference in new issue
Block a user