feat(clip): 新增「选区截取」实验性功能 + 筛选态组复选框只作用于筛选结果 + 重采样统计不被省略
一、重采样面板统计信息不再被省略为 `…`
- 根因:EdgePanelWidth 里数值列按"典型样例"(1h 1min 2s(1.23456 GHz))估宽,
真实文本(16ms 182.614us(61.795 Hz))更长 => 绘制矩形不够宽 => 尾部被裁。
- 新增 FormatIntervalWithFrequency() 作为「间隔(频率)」文本的唯一实现,
宽度计算与绘制共用它;EdgePanelWidth 改为逐信号实测真实文本宽度取最大值。
二、筛选状态下组复选框只作用于「筛选出来的信号」
- TabConfig 新增 SetGroupVisibleSubset / GroupAllVisibleSubset / GroupHasVisibleSubset,
只读写掩码内元素,未通过筛选的信号可见性保持不变。
- SignalPanel::BuildFilterMask 提供逐信号掩码(未筛选时返回 false,走整组语义)。
- 组复选框的点击、绘制、行位图缓存键三处统一改用子集判定
(只改绘制不改缓存键会导致勾选后复选框不刷新)。
三、新增实验性功能「选区截取」(EdgePanelKind::CLIP = 6,PLOT_CMD_LANE_CLIP = 14)
- 菜单位置:实验性功能二级菜单,「重采样」之下、「自由标记」上方,带勾选。
- 面板:逐信号一行(信号色叠底条目栏 + 右侧复选框,不可展开),内容可滚动;
底部两个不滚动的按钮「截取为新信号」与「原始信号截取」;
未启用双光标时两个按钮为空矩形并给出提示。
- 截取为新信号:区间内原始数据(不烘焙)+ 继承源信号整套调整,
名称为 <源信号名> (AAAA - BBBB)(显示时刻、保留三位小数、带 s),插入源信号下方,
使用 AppendGroupAxis 新建的独立时间轴。
- 原始信号截取:DataSet::ClipSignalRange 就地裁剪时间轴与采样,
用新建+swap+shrink_to_fit 真正归还内存,并重算统计与轴的 minTime/maxTime/monotonic。
调用前先 PauseDerivedAsync() 避免后台派生读到已释放采样。
- 时间轴偏移参与区间比较:双光标是显示时刻(time+offsetTime),
BuildClippedSignal 比较时加偏移,ClipSignalInPlace 换算回采样时刻(low-offsetTime)。
- 同步点已全部补齐:LaneCarry(三处)/SplitLane/PruneLaneSignalItems/RemapAfterSignalInsert/
purgeRefs/PanelSlotKey/CollectDebugBoxes/SceneContentHash/kEdgePanelKinds/kOrder/
EdgePanelLayoutFor/EDGE_PANEL_MOVE/m_pendingPanelClose/MEAN_SCROLL/滚轮/悬停/
SetLanePanelCollapsed/LanePanelCollapsed/QueryCursor。
make release 零 warning,产物 860,160 B。
This commit is contained in:
1 parent
5d305cb10f
commit
62220477c0
18 files changed
+1263
-67
No files matched your search
@@ -127,7 +127,8 @@ namespace jdi
|
||||
PLOT_CMD_LANE_MEAN_FILTER = 10, /**< 切换本网格的「实验性功能 → 均值滤波」开关。 */
|
||||
PLOT_CMD_LANE_WEAK_PEAK = 11, /**< 切换本网格的「实验性功能 → 弱峰调整」开关。 */
|
||||
PLOT_CMD_LANE_FREE_MARKER = 12, /**< 切换本网格的「实验性功能 → 自由标记」开关。 */
|
||||
PLOT_CMD_LANE_RESAMPLE = 13 /**< 切换本网格的「实验性功能 → 重采样」开关。 */
|
||||
PLOT_CMD_LANE_RESAMPLE = 13, /**< 切换本网格的「实验性功能 → 重采样」开关。 */
|
||||
PLOT_CMD_LANE_CLIP = 14 /**< 切换本网格的「实验性功能 → 选区截取」开关。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -140,7 +141,8 @@ namespace jdi
|
||||
MEAN = 2, /**< 均值滤波。 */
|
||||
PEAK = 3, /**< 弱峰调整。 */
|
||||
MARK = 4, /**< 自由标记。 */
|
||||
RESAMPLE = 5 /**< 重采样(把非固定频率采样规整为固定频率)。 */
|
||||
RESAMPLE = 5, /**< 重采样(把非固定频率采样规整为固定频率)。 */
|
||||
CLIP = 6 /**< 选区截取(按双光标区间截取信号,必须配合双光标使用)。 */
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -1087,6 +1089,51 @@ namespace jdi
|
||||
*/
|
||||
bool SignalDataInterval(const SignalRef& ref, double& maxInterval, double& minInterval) const;
|
||||
|
||||
// ------------------------------------------------------------------ 选区截取
|
||||
/**
|
||||
* @brief 「选区截取」的单个请求(一个被勾选的信号 + 截取区间 + 是否破坏原信号)。
|
||||
*/
|
||||
struct ClipBuild
|
||||
{
|
||||
int groupIndex = -1; /**< 信号所属分组下标。 */
|
||||
int signalIndex = -1; /**< 组内信号下标。 */
|
||||
double begin = 0.0; /**< 截取区间左端(显示时刻,秒)。 */
|
||||
double end = 0.0; /**< 截取区间右端(显示时刻,秒)。 */
|
||||
/** @brief `true` = 破坏性截取原始信号;`false` = 插到源信号下方的派生信号。 */
|
||||
bool destructive = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* @brief 取出「选区截取」请求(由面板底部两个按钮触发)。
|
||||
* @param [OUT] laneIndex 触发该请求的网格下标。
|
||||
* @param [OUT] items 需要截取的信号及其区间。
|
||||
* @return 存在待处理请求返回 true(取出后清空)。
|
||||
*/
|
||||
bool TakePendingClipBuild(int& laneIndex, std::vector<ClipBuild>& items);
|
||||
|
||||
/**
|
||||
* @brief 开启 / 关闭某网格的「选区截取」功能。
|
||||
* @param [IN] laneIndex 网格下标。
|
||||
* @param [IN] enabled 是否开启。
|
||||
* @return 状态发生变化返回 true。
|
||||
*/
|
||||
bool SetLaneClip(int laneIndex, bool enabled);
|
||||
|
||||
/** @brief 查询某网格的「选区截取」开关。 */
|
||||
bool LaneClip(int laneIndex) const;
|
||||
|
||||
/** @brief 切换某网格的「选区截取」开关(右键菜单用)。 */
|
||||
bool ToggleLaneClip(int laneIndex);
|
||||
|
||||
/** @brief 切换某信号是否为选区截取的目标(面板条目栏右侧复选框)。 */
|
||||
bool ToggleLaneClipSignal(int laneIndex, const SignalRef& ref);
|
||||
|
||||
/** @brief 查询某信号是否为选区截取的目标。 */
|
||||
bool LaneClipSignalEnabled(int laneIndex, const SignalRef& ref) const;
|
||||
|
||||
/** @brief 设置「选区截取」面板内容的滚动偏移(自动夹到合法范围)。 */
|
||||
bool SetLaneClipScroll(int laneIndex, float scroll);
|
||||
|
||||
/**
|
||||
* @brief 设置某网格是否处于「自由标记点选取」(浮动面板底部的切换按钮)。
|
||||
*
|
||||
@@ -2119,6 +2166,17 @@ namespace jdi
|
||||
gd::RectF showDelta;
|
||||
/** @brief 「显示枚举文本」开关按钮(同一行的右半)。 */
|
||||
gd::RectF showEnum;
|
||||
/**
|
||||
* @brief 「选区截取」底部的两个固定按钮(**不参与滚动**)。
|
||||
*
|
||||
* `clipBuildButton` = 「截取为新信号」(在源信号下方插入派生信号);
|
||||
* `clipDestructiveButton` = 「原始信号截取」(破坏性截取,丢弃区间外的采样)。
|
||||
* 未启用双光标模式时两者都是空矩形(不可点)。
|
||||
*/
|
||||
gd::RectF clipBuildButton;
|
||||
gd::RectF clipDestructiveButton;
|
||||
/** @brief 「选区截取」需要双光标时的提示文案矩形(未启用双光标时使用)。 */
|
||||
gd::RectF clipHint;
|
||||
/**
|
||||
* @brief 「自由标记」:每个信号名右侧的「清除该信号全部固定点」✕。
|
||||
*
|
||||
@@ -3015,6 +3073,9 @@ namespace jdi
|
||||
std::vector<WeakPeakBuild> m_pendingWeakPeakItems; /**< 待构建的弱峰调整信号及其参数。 */
|
||||
int m_pendingResampleLane = -1; /**< 待构建的重采样信号所属网格下标(-1 表示无请求)。 */
|
||||
std::vector<ResampleBuild> m_pendingResampleItems; /**< 待构建的重采样信号及其间隔。 */
|
||||
|
||||
int m_pendingClipLane = -1; /**< 待截取的信号所属网格下标(-1 表示无请求)。 */
|
||||
std::vector<ClipBuild> m_pendingClipItems; /**< 待截取的信号及其区间。 */
|
||||
bool m_mouseOverPanel = false; /**< 上一帧鼠标是否落在某个实验性功能面板上。 */
|
||||
int m_peakDragLane = -1; /**< 正在拖动弱峰滑块的面板所属网格。 */
|
||||
SignalRef m_peakDragRef; /**< 正在拖动弱峰滑块的**信号**(每个信号一套参数)。 */
|
||||
|
||||
@@ -607,6 +607,21 @@ namespace jdi
|
||||
inline const double kResampleIntervalMin = 1.0e-9;
|
||||
inline const double kResampleIntervalMax = 1.0e6;
|
||||
|
||||
// ---- 「选区截取」面板 ----
|
||||
/** 「选区截取」面板标题。 */
|
||||
inline const wchar_t* const kClipTitle = L"选区截取";
|
||||
/** 底部按钮:「截取为新信号」(在源信号下方插入派生信号)。 */
|
||||
inline const wchar_t* const kClipBuildLabel = L"截取为新信号";
|
||||
/** 底部按钮:「原始信号截取」(破坏性截取,丢弃区间外的采样)。 */
|
||||
inline const wchar_t* const kClipDestructiveLabel = L"原始信号截取";
|
||||
/**
|
||||
* @brief 未启用双光标模式时面板给出的提示。
|
||||
*
|
||||
* 该功能依赖「双光标」定义的截取区间,因此单光标/无光标时无法工作。
|
||||
*/
|
||||
inline const wchar_t* const kClipNeedDoubleCursorHint =
|
||||
L"请先启用「双光标」模式(工具栏 → 光标),再回到此面板截取选区。";
|
||||
|
||||
// ---- 「公式映射」的分组包络缓存(见 `PlotView::AcquireFormulaEnvelope`)----
|
||||
/**
|
||||
* @brief 每个信号的包络分组数上限(分组宽度按采样数反算)。
|
||||
@@ -1066,7 +1081,8 @@ namespace jdi
|
||||
*/
|
||||
inline const EdgePanelKind kEdgePanelKinds[] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
|
||||
EdgePanelKind::MEAN, EdgePanelKind::PEAK,
|
||||
EdgePanelKind::MARK, EdgePanelKind::RESAMPLE};
|
||||
EdgePanelKind::MARK, EdgePanelKind::RESAMPLE,
|
||||
EdgePanelKind::CLIP};
|
||||
|
||||
/** 实验性功能浮动面板的种类数(遍历与命中测试统一用它,避免新增功能时漏改)。 */
|
||||
inline const int kEdgePanelKindCount = static_cast<int>(sizeof(kEdgePanelKinds) / sizeof(kEdgePanelKinds[0]));
|
||||
@@ -1342,6 +1358,31 @@ namespace jdi
|
||||
return FormatFrequency(hertz);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 「重采样」面板统计行的完整文本:`<间隔>(<频率>)`。
|
||||
*
|
||||
* ⚠️ **唯一实现**:面板**宽度计算**与**绘制**必须都调它,
|
||||
* 否则宽度按"典型文本"估算、实际文本更长时会被 `UiEllipsize` 截成 `…`
|
||||
* (用户 2026-11-03 报告"统计信息显示不完整")。
|
||||
*
|
||||
* 例:`3ms 696.631us(270.517 Hz)`、`16ms 182.614us(61.795 Hz)`。
|
||||
*
|
||||
* @param [IN] seconds 该统计行对应的间隔(秒)。
|
||||
* @return 完整文本;非法值返回 "—"。
|
||||
*/
|
||||
inline std::wstring FormatIntervalWithFrequency(double seconds)
|
||||
{
|
||||
if (!std::isfinite(seconds) || seconds <= 0.0)
|
||||
{
|
||||
return L"—";
|
||||
}
|
||||
std::wstring text = FormatDurationCompound(seconds);
|
||||
text += L"(";
|
||||
text += FormatFrequencyText(1.0 / seconds);
|
||||
text += L")";
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按比例缩放颜色的 alpha 通道(用于提示浮层淡出)。
|
||||
* @param [IN] argb 原始 0xAARRGGBB 颜色。
|
||||
|
||||
@@ -179,6 +179,15 @@ namespace jdi
|
||||
mix(static_cast<std::uint64_t>(item.amountPercent));
|
||||
mix(static_cast<std::uint64_t>(item.iterations));
|
||||
}
|
||||
// 「选区截取」:开关 + 每个被勾选信号(只影响面板像素,不影响网格曲线内容,
|
||||
// 但与其它面板一样放在这里以统一定义;面板自身另有 `PanelSlotKey`)。
|
||||
mix(Tab().Lanes()[laneIndex].clip ? 1ull : 0ull);
|
||||
mix(static_cast<std::uint64_t>(Tab().Lanes()[laneIndex].clipItems.size()));
|
||||
for (const Lane::ClipItem& item : Tab().Lanes()[laneIndex].clipItems)
|
||||
{
|
||||
mix(static_cast<std::uint64_t>(item.ref.groupIndex));
|
||||
mix(static_cast<std::uint64_t>(item.ref.signalIndex));
|
||||
}
|
||||
}
|
||||
mix(signals.size());
|
||||
for (const SignalRef& ref : signals)
|
||||
|
||||
@@ -1990,6 +1990,115 @@ namespace jdi
|
||||
return true;
|
||||
}
|
||||
|
||||
// ============================ 「选区截取」 ============================
|
||||
|
||||
bool PlotView::SetLaneClip(int laneIndex, bool enabled)
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
if (lane.clip == enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
lane.clip = enabled;
|
||||
if (enabled)
|
||||
{
|
||||
// 默认只勾选网格内**第一个可见信号**(与其它实验性功能一致)。
|
||||
if (lane.clipItems.empty())
|
||||
{
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data != nullptr && resolved.visible)
|
||||
{
|
||||
lane.EnsureClipItem(ref);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
lane.clipPanel.open = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
lane.clipPanel.open = false;
|
||||
}
|
||||
EnsureLaneLayersPrefetched();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::LaneClip(int laneIndex) const
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].clip;
|
||||
}
|
||||
|
||||
bool PlotView::ToggleLaneClip(int laneIndex)
|
||||
{
|
||||
const bool enabled = !LaneClip(laneIndex);
|
||||
SetLaneClip(laneIndex, enabled);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
bool PlotView::ToggleLaneClipSignal(int laneIndex, const SignalRef& ref)
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
if (lane.ClipEnabled(ref))
|
||||
{
|
||||
lane.RemoveClipItem(ref);
|
||||
return false;
|
||||
}
|
||||
lane.EnsureClipItem(ref);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::LaneClipSignalEnabled(int laneIndex, const SignalRef& ref) const
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return m_tab->Lanes()[static_cast<std::size_t>(laneIndex)].ClipEnabled(ref);
|
||||
}
|
||||
|
||||
bool PlotView::SetLaneClipScroll(int laneIndex, float scroll)
|
||||
{
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
Lane& lane = m_tab->Lanes()[static_cast<std::size_t>(laneIndex)];
|
||||
const float clamped = std::max(0.0f, scroll);
|
||||
if (std::fabs(lane.clipScroll - clamped) < 0.5f)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
lane.clipScroll = clamped;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PlotView::TakePendingClipBuild(int& laneIndex, std::vector<ClipBuild>& items)
|
||||
{
|
||||
if (m_pendingClipLane < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
laneIndex = m_pendingClipLane;
|
||||
items = m_pendingClipItems;
|
||||
m_pendingClipLane = -1;
|
||||
m_pendingClipItems.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool PlotView::SetLaneEdgeCenterScroll(int laneIndex, float scroll) {
|
||||
if (m_tab == nullptr || laneIndex < 0 || laneIndex >= static_cast<int>(m_tab->Lanes().size()))
|
||||
@@ -2098,6 +2207,9 @@ namespace jdi
|
||||
case EdgePanelKind::RESAMPLE:
|
||||
state = &lane.resamplePanel;
|
||||
break;
|
||||
case EdgePanelKind::CLIP:
|
||||
state = &lane.clipPanel;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -2130,6 +2242,8 @@ namespace jdi
|
||||
return lane.freeMarkerPanel.collapsed;
|
||||
case EdgePanelKind::RESAMPLE:
|
||||
return lane.resamplePanel.collapsed;
|
||||
case EdgePanelKind::CLIP:
|
||||
return lane.clipPanel.collapsed;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1066,6 +1066,18 @@ namespace jdi
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
// 「选区截取」底部两个按钮:可点击控件(双光标关闭时为空矩形 ⇒ 不响应)。
|
||||
if (layout.clipBuildButton.Width > 0.0f &&
|
||||
UiPointInRect(layout.clipBuildButton, static_cast<float>(x), static_cast<float>(y)))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
if (layout.clipDestructiveButton.Width > 0.0f &&
|
||||
UiPointInRect(layout.clipDestructiveButton, static_cast<float>(x),
|
||||
static_cast<float>(y)))
|
||||
{
|
||||
return LoadCursorW(nullptr, IDC_HAND);
|
||||
}
|
||||
if (layout.opacityTrack.Width > 0.0f &&
|
||||
WeakPeakSliderHit(layout.opacityTrack, m_scale, static_cast<float>(x),
|
||||
static_cast<float>(y)))
|
||||
@@ -1109,7 +1121,8 @@ namespace jdi
|
||||
const bool listCursor = (cursorKind == EdgePanelKind::MEAN ||
|
||||
cursorKind == EdgePanelKind::CENTER ||
|
||||
cursorKind == EdgePanelKind::PEAK ||
|
||||
cursorKind == EdgePanelKind::RESAMPLE);
|
||||
cursorKind == EdgePanelKind::RESAMPLE ||
|
||||
cursorKind == EdgePanelKind::CLIP);
|
||||
const Lane& cursorLane =
|
||||
Tab().Lanes()[static_cast<std::size_t>(panelLane)];
|
||||
if (listCursor)
|
||||
@@ -1136,6 +1149,10 @@ namespace jdi
|
||||
checked = cursorLane.FindResampleItem(
|
||||
layout.rowRefs[rowIndex]) != nullptr;
|
||||
}
|
||||
else if (cursorKind == EdgePanelKind::CLIP)
|
||||
{
|
||||
checked = cursorLane.ClipEnabled(layout.rowRefs[rowIndex]);
|
||||
}
|
||||
else
|
||||
{
|
||||
checked = cursorLane.FindMeanFilterItem(
|
||||
@@ -1143,6 +1160,10 @@ namespace jdi
|
||||
}
|
||||
return checked ? LoadCursorW(nullptr, IDC_HAND) : nullptr;
|
||||
}
|
||||
if (cursorKind == EdgePanelKind::CLIP)
|
||||
{
|
||||
continue; // 截取面板的条目只有复选框,没有参数行
|
||||
}
|
||||
if (cursorKind == EdgePanelKind::RESAMPLE)
|
||||
{
|
||||
// 两行输入框是可点击控件(点击进入就地编辑)。
|
||||
@@ -1734,6 +1755,10 @@ namespace jdi
|
||||
lane.resamplePanel.x = left;
|
||||
lane.resamplePanel.y = top;
|
||||
break;
|
||||
case EdgePanelKind::CLIP:
|
||||
lane.clipPanel.x = left;
|
||||
lane.clipPanel.y = top;
|
||||
break;
|
||||
case EdgePanelKind::COUNT:
|
||||
default:
|
||||
lane.edgePanel.x = left;
|
||||
@@ -1832,6 +1857,9 @@ namespace jdi
|
||||
case EdgePanelKind::RESAMPLE:
|
||||
scrolled = SetLaneResampleScroll(m_markerScrollDragLane, target);
|
||||
break;
|
||||
case EdgePanelKind::CLIP:
|
||||
scrolled = SetLaneClipScroll(m_markerScrollDragLane, target);
|
||||
break;
|
||||
default:
|
||||
scrolled = SetLaneMeanFilterScroll(m_markerScrollDragLane, target);
|
||||
break;
|
||||
@@ -2014,7 +2042,8 @@ namespace jdi
|
||||
static_cast<EdgePanelKind>(hoverPanelKind);
|
||||
if (overEdgePanel && m_tab != nullptr && panelLane >= 0 &&
|
||||
(hoverKind == EdgePanelKind::MEAN || hoverKind == EdgePanelKind::PEAK ||
|
||||
hoverKind == EdgePanelKind::CENTER || hoverKind == EdgePanelKind::RESAMPLE))
|
||||
hoverKind == EdgePanelKind::CENTER || hoverKind == EdgePanelKind::RESAMPLE ||
|
||||
hoverKind == EdgePanelKind::CLIP))
|
||||
{
|
||||
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, hoverKind);
|
||||
if (UiPointInRect(layout.content, static_cast<float>(x), static_cast<float>(y)))
|
||||
@@ -2411,16 +2440,17 @@ namespace jdi
|
||||
// (前两者是「名称 + 复选框 + 两个参数输入框」,弱峰调整是「名称 + 复选框 + 两行滑块」)。
|
||||
if ((kind == EdgePanelKind::MEAN || kind == EdgePanelKind::CENTER ||
|
||||
kind == EdgePanelKind::PEAK || kind == EdgePanelKind::COUNT ||
|
||||
kind == EdgePanelKind::RESAMPLE) &&
|
||||
kind == EdgePanelKind::RESAMPLE || kind == EdgePanelKind::CLIP) &&
|
||||
m_tab != nullptr)
|
||||
{
|
||||
const bool meanMode = (kind == EdgePanelKind::MEAN);
|
||||
const bool centerMode = (kind == EdgePanelKind::CENTER);
|
||||
const bool boostMode = (kind == EdgePanelKind::PEAK);
|
||||
const bool resampleMode = (kind == EdgePanelKind::RESAMPLE);
|
||||
const bool clipMode = (kind == EdgePanelKind::CLIP);
|
||||
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」自 2026-11-02 起共用同一套
|
||||
//**可滚动列表**交互(信号色叠底条目栏 + 右侧复选框 + 展开后的参数行)。
|
||||
const bool listMode = (meanMode || centerMode || boostMode || resampleMode);
|
||||
const bool listMode = (meanMode || centerMode || boostMode || resampleMode || clipMode);
|
||||
// 「边沿计数」= **表格**(表头 + 每信号一行),只有滚动条可拖。
|
||||
if (kind == EdgePanelKind::COUNT)
|
||||
{
|
||||
@@ -2583,6 +2613,82 @@ namespace jdi
|
||||
return true; // 面板内的其它位置:消费点击,不穿透
|
||||
}
|
||||
}
|
||||
// 「选区截取」:条目只有**右侧复选框**(不可展开),底部两个固定按钮。
|
||||
if (clipMode)
|
||||
{
|
||||
const bool doubleCursor = (Tab().CursorMode() == PlotCursorMode::DOUBLE);
|
||||
Lane& clipLane = m_tab->Lanes()[static_cast<std::size_t>(index)];
|
||||
// 条目复选框:切换该信号是否为截取目标。
|
||||
for (std::size_t rowIndex = 0; rowIndex < layout.rowRefs.size(); ++rowIndex)
|
||||
{
|
||||
if (!insideContent || rowIndex >= layout.rowChecks.size() ||
|
||||
layout.rowChecks[rowIndex].Width <= 0.0f ||
|
||||
!UiPointInRect(layout.rowChecks[rowIndex], fx, fy))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ToggleLaneClipSignal(index, layout.rowRefs[rowIndex]);
|
||||
m_meanRowHoverLane = -1;
|
||||
m_meanRowHoverRef = SignalRef();
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
// 底部按钮(仅在双光标模式下有几何)。
|
||||
const bool onBuild = layout.clipBuildButton.Width > 0.0f &&
|
||||
UiPointInRect(layout.clipBuildButton, fx, fy);
|
||||
const bool onDestructive = layout.clipDestructiveButton.Width > 0.0f &&
|
||||
UiPointInRect(layout.clipDestructiveButton, fx, fy);
|
||||
if (onBuild || onDestructive)
|
||||
{
|
||||
if (!doubleCursor)
|
||||
{
|
||||
SetHint(kClipNeedDoubleCursorHint);
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
// 截取区间 = 双光标(左 = 较小时间,右 = 较大时间)。
|
||||
const double begin = std::min(Tab().CursorTime(), Tab().CursorTimeAlt());
|
||||
const double end = std::max(Tab().CursorTime(), Tab().CursorTimeAlt());
|
||||
m_pendingClipItems.clear();
|
||||
for (const SignalRef& ref : clipLane.signals)
|
||||
{
|
||||
const ResolvedSignal resolved = ResolveSignal(ref);
|
||||
if (resolved.data == nullptr || !resolved.visible ||
|
||||
!clipLane.ClipEnabled(ref))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
ClipBuild build;
|
||||
build.groupIndex = ref.groupIndex;
|
||||
build.signalIndex = ref.signalIndex;
|
||||
build.begin = begin;
|
||||
build.end = end;
|
||||
build.destructive = onDestructive;
|
||||
m_pendingClipItems.push_back(build);
|
||||
}
|
||||
if (m_pendingClipItems.empty())
|
||||
{
|
||||
SetHint(L"选区截取:当前网格没有勾选任何信号。");
|
||||
}
|
||||
else
|
||||
{
|
||||
SetHint(onDestructive
|
||||
? L"正在按选区截取原始信号(" +
|
||||
std::to_wstring(m_pendingClipItems.size()) +
|
||||
L" 条,区间外数据将被丢弃)…"
|
||||
: L"正在按选区截取为新信号(" +
|
||||
std::to_wstring(m_pendingClipItems.size()) + L" 条)…");
|
||||
m_pendingClipLane = index;
|
||||
}
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true;
|
||||
}
|
||||
if (insideContent)
|
||||
{
|
||||
m_dragMode = PlotDragMode::NONE;
|
||||
return true; // 内容区其它位置:消费点击,不穿透
|
||||
}
|
||||
}
|
||||
// 「弱峰调整」:两行滑块 = 调整幅度(-400% … +400%)/ 迭代次数(1 … 自定上限)。
|
||||
// 语义与该面板旧式排版完全一致,只是外观改成与均值滤波同款的可滚动列表。
|
||||
if (boostMode)
|
||||
@@ -3325,6 +3431,9 @@ namespace jdi
|
||||
// 用户只能改走右键菜单取消功能(用户 2026-11-03 报告)。
|
||||
SetLaneResample(lane, false);
|
||||
break;
|
||||
case EdgePanelKind::CLIP:
|
||||
SetLaneClip(lane, false);
|
||||
break;
|
||||
case EdgePanelKind::COUNT:
|
||||
default:
|
||||
SetLaneEdgeCount(lane, false);
|
||||
@@ -3457,7 +3566,7 @@ namespace jdi
|
||||
//(上百个信号合并时必备)。
|
||||
if (hoveredKind == EdgePanelKind::MEAN || hoveredKind == EdgePanelKind::CENTER ||
|
||||
hoveredKind == EdgePanelKind::PEAK || hoveredKind == EdgePanelKind::COUNT ||
|
||||
hoveredKind == EdgePanelKind::RESAMPLE)
|
||||
hoveredKind == EdgePanelKind::RESAMPLE || hoveredKind == EdgePanelKind::CLIP)
|
||||
{
|
||||
const EdgePanelLayout layout = EdgePanelLayoutFor(panelLane, hoveredKind);
|
||||
if (layout.content.Width > 0.0f &&
|
||||
@@ -3495,6 +3604,9 @@ namespace jdi
|
||||
case EdgePanelKind::RESAMPLE:
|
||||
SetLaneResampleScroll(panelLane, target);
|
||||
break;
|
||||
case EdgePanelKind::CLIP:
|
||||
SetLaneClipScroll(panelLane, target);
|
||||
break;
|
||||
default:
|
||||
SetLaneMeanFilterScroll(panelLane, target);
|
||||
break;
|
||||
|
||||
@@ -868,6 +868,8 @@ namespace jdi
|
||||
boxes.Add(layout.opacityValue, 3);
|
||||
boxes.Add(layout.showDelta, 3);
|
||||
boxes.Add(layout.showEnum, 3);
|
||||
boxes.Add(layout.clipBuildButton, 3);
|
||||
boxes.Add(layout.clipDestructiveButton, 3);
|
||||
|
||||
boxes.AddAll(layout.rowChecks, 4);
|
||||
boxes.AddAll(layout.rowPoints, 4);
|
||||
@@ -4067,6 +4069,10 @@ namespace jdi
|
||||
mixRect(layout.button);
|
||||
mixRect(layout.showDelta);
|
||||
mixRect(layout.showEnum);
|
||||
// 「选区截取」:底部两个按钮的矩形(双光标关闭时为空 ⇒ 自动区分两种状态)。
|
||||
mixRect(layout.clipBuildButton);
|
||||
mixRect(layout.clipDestructiveButton);
|
||||
mixRect(layout.clipHint);
|
||||
mixRect(layout.content);
|
||||
mixRect(layout.tableHeader);
|
||||
for (const EdgePanelLayout::TableColumn& column : layout.tableColumns)
|
||||
@@ -4195,6 +4201,15 @@ namespace jdi
|
||||
mix(intervalBits);
|
||||
mix(resampleItem->collapsed ? 1ull : 0ull);
|
||||
}
|
||||
// 「选区截取」:只有勾选状态影响该行像素;
|
||||
// 但底部按钮是否显示取决于「双光标」模式,双光标开关变化同样要让面板重绘。
|
||||
mix(lane.ClipEnabled(ref) ? 1ull : 0ull);
|
||||
}
|
||||
// 「选区截取」的双光标状态决定底部按钮/提示的像素。
|
||||
if (kind == EdgePanelKind::CLIP)
|
||||
{
|
||||
mix(Tab().CursorMode() == PlotCursorMode::DOUBLE ? 1ull : 0ull);
|
||||
mix(lane.clipScroll > 0.0f ? 1ull : 0ull);
|
||||
}
|
||||
|
||||
// ⚠️⚠️ **边沿统计的数字本身就是面板内容,数字变化必须能让面板重绘**:
|
||||
@@ -4883,13 +4898,14 @@ namespace jdi
|
||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||
const bool freeMarker = (kind == EdgePanelKind::MARK);
|
||||
const bool resamplePanel = (kind == EdgePanelKind::RESAMPLE);
|
||||
// ⚠️ 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」= 可滚动的分组列表
|
||||
const bool clipPanel = (kind == EdgePanelKind::CLIP);
|
||||
// ⚠️ 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」「选区截取」= 可滚动的分组列表
|
||||
//(条目栏 + 展开后的参数行);
|
||||
// **「边沿计数」= 表格**(表头:信号名 / 上升沿计数 / 下降沿计数),单独一支。
|
||||
// 它们都**不能**再走旧的逐条目分支:那个分支没有任何裁切,滚动时会画到标题栏上。
|
||||
const bool listStyle = (meanFilter || centerAxis || weakPeak || resamplePanel);
|
||||
const bool listStyle = (meanFilter || centerAxis || weakPeak || resamplePanel || clipPanel);
|
||||
const bool countTable = (kind == EdgePanelKind::COUNT);
|
||||
const bool hasButton = (meanFilter || weakPeak || resamplePanel);
|
||||
const bool hasButton = (meanFilter || weakPeak || resamplePanel || clipPanel);
|
||||
{
|
||||
if (index >= count)
|
||||
{
|
||||
@@ -5088,8 +5104,11 @@ namespace jdi
|
||||
: (freeMarker ? std::wstring(L"自由标记")
|
||||
: (resamplePanel
|
||||
? std::wstring(kResampleTitle)
|
||||
: ((centerAxis ? L"中心轴边沿计数" : L"边沿计数") +
|
||||
EdgeRangeSuffix()))));
|
||||
: (clipPanel
|
||||
? std::wstring(kClipTitle)
|
||||
: ((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,
|
||||
@@ -5347,13 +5366,15 @@ namespace jdi
|
||||
: tabLane.FindMeanFilterItem(ref));
|
||||
const Lane::ResampleItem* resampleItem =
|
||||
resamplePanel ? tabLane.FindResampleItem(ref) : nullptr;
|
||||
const bool checked = resamplePanel ? (resampleItem != nullptr)
|
||||
: (weakPeak ? (peakItem != nullptr)
|
||||
: (item != nullptr));
|
||||
const bool checked = clipPanel ? tabLane.ClipEnabled(ref)
|
||||
: (resamplePanel ? (resampleItem != nullptr)
|
||||
: (weakPeak ? (peakItem != nullptr)
|
||||
: (item != nullptr)));
|
||||
const bool rowCollapsed =
|
||||
resamplePanel ? (resampleItem != nullptr && resampleItem->collapsed)
|
||||
: (weakPeak ? (peakItem != nullptr && peakItem->collapsed)
|
||||
: (item != nullptr && item->collapsed));
|
||||
clipPanel ? true
|
||||
: (resamplePanel ? (resampleItem != nullptr && resampleItem->collapsed)
|
||||
: (weakPeak ? (peakItem != nullptr && peakItem->collapsed)
|
||||
: (item != nullptr && item->collapsed)));
|
||||
|
||||
// 内容区已由 `g.SetClip(layout.content)` 裁切(行可以“半截”显示,滚动更顺滑);
|
||||
// 已经完整滚出可视范围的行在上面就 `continue` 了,这里不再额外门控。
|
||||
@@ -5414,6 +5435,11 @@ namespace jdi
|
||||
gd::RectF(row.X, row.Y, std::max(10.0f, labelWidth),
|
||||
row.Height),
|
||||
m_theme->textMuted, TextAlign::LEFT, layout.content);
|
||||
// ⚠️ 统计数字**绝不省略**(用户 2026-11-03 要求):
|
||||
// 面板宽度已按真实文本量宽(`EdgePanelWidth` 逐信号实测),
|
||||
// 因此这里用 `TextAlign::RIGHT` + 完整矩形即可;
|
||||
// 万一仍被窗口边界夹住,也宁可超出面板右缘(由内容区裁剪兜底),
|
||||
// **不要**退化成 `…` 让用户看不到真实数值。
|
||||
UiTextClipped(g, text, m_fonts->monoSmall,
|
||||
gd::RectF(row.X + labelWidth + 8.0f * m_scale, row.Y,
|
||||
std::max(10.0f, row.Width - labelWidth -
|
||||
@@ -6114,7 +6140,25 @@ namespace jdi
|
||||
UiText(g, text, m_fonts->uiSmall, inner,
|
||||
checked ? m_theme->textStrong : m_theme->text, TextAlign::CENTER);
|
||||
};
|
||||
if (hasButton)
|
||||
if (clipPanel)
|
||||
{
|
||||
// 「选区截取」:底部两个并排按钮;未启用双光标时都给提示、不画按钮。
|
||||
const bool doubleCursor = (Tab().CursorMode() == PlotCursorMode::DOUBLE);
|
||||
if (doubleCursor)
|
||||
{
|
||||
paintToggleButton(layout.clipBuildButton, kClipBuildLabel, false);
|
||||
paintToggleButton(layout.clipDestructiveButton, kClipDestructiveLabel, false);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 提示文字不参与滚动,画在内容区正中。
|
||||
const std::wstring hintText(kClipNeedDoubleCursorHint);
|
||||
UiTextClipped(g, UiEllipsize(hintText, m_fonts->uiSmall, layout.content.Width),
|
||||
m_fonts->uiSmall, layout.content, m_theme->textMuted,
|
||||
TextAlign::CENTER, layout.content);
|
||||
}
|
||||
}
|
||||
if (hasButton && !clipPanel)
|
||||
{
|
||||
paintToggleButton(layout.button, kBuildSignalLabel, false);
|
||||
}
|
||||
|
||||
@@ -29,12 +29,13 @@ namespace jdi
|
||||
const bool meanFilter = (kind == EdgePanelKind::MEAN);
|
||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||
const bool resamplePanel = (kind == EdgePanelKind::RESAMPLE);
|
||||
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」= 可滚动的分组列表
|
||||
const bool clipPanel = (kind == EdgePanelKind::CLIP);
|
||||
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」「选区截取」= 可滚动的分组列表
|
||||
//(信号色叠底条目栏 + 右侧复选框 + 展开后的参数行);
|
||||
// **「边沿计数」= 表格**(表头:信号名 / 上升沿计数 / 下降沿计数)。
|
||||
// ⚠️ 旧式「逐条目」排版(无滚动的名称+参数行)已彻底删除:上百个信号时它会算出
|
||||
// 上万像素高的面板,超出绘图区后**面板像素缓存一次都存不进去** ⇒ 每帧整块重画。
|
||||
const bool listStyle = (centerAxis || meanFilter || weakPeak || resamplePanel);
|
||||
const bool listStyle = (centerAxis || meanFilter || weakPeak || resamplePanel || clipPanel);
|
||||
const bool countTable = (kind == EdgePanelKind::COUNT);
|
||||
|
||||
// 标题行 = “GX” 徽标 + 标题(“边沿计数” / “中心轴边沿计数” / “均值滤波” / “弱峰调整”
|
||||
@@ -168,19 +169,35 @@ namespace jdi
|
||||
(kFreeMarkerScrollWidth + 4.0f) * m_scale);
|
||||
if (resamplePanel)
|
||||
{
|
||||
// 「重采样」四行 = 「标签列 + 数值列」(两行统计 + 两行输入框),
|
||||
// 标签取四者中最宽的一个,数值列按间隔 / 频率的典型文本宽度取。
|
||||
// ⚠️ 「最大/最小数据间隔」行末尾还带「(频率)」尾缀(用户 2026-11-03 要求),
|
||||
// 取值列必须按**含尾缀**的文本量宽,否则尾缀会被裁掉。
|
||||
// 「重采样」四行 = 「标签列 + 数值列」(两行统计 + 两行输入框)。
|
||||
// 标签取四者中最宽的一个;数值列必须按**真实会显示的文本**量宽
|
||||
// (用户 2026-11-03 要求:统计信息不能被省略成 `…`)。
|
||||
const float resampleLabelColumn =
|
||||
std::max(std::max(UiMeasureTextWidth(m_fonts->uiSmall, kResampleMaxIntervalLabel),
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, kResampleMinIntervalLabel)),
|
||||
std::max(UiMeasureTextWidth(m_fonts->uiSmall, kResampleIntervalLabel),
|
||||
UiMeasureTextWidth(m_fonts->uiSmall, kResampleFrequencyLabel)));
|
||||
const float resampleValueColumn =
|
||||
std::max(std::max(UiMeasureTextWidth(m_fonts->monoSmall, L"1.23456 GHz"),
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, L"1h 1min 2s")),
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, L"1h 1min 2s(1.23456 GHz)"));
|
||||
// ⚠️ 逐信号实测「最大 / 最小数据间隔」的真实文本宽度(含(频率)尾缀),
|
||||
// 而不是按 `1h 1min 2s(1.23456 GHz)` 这类**典型样例**估算 ——
|
||||
// 真实值(如 `16ms 182.614us(61.795 Hz)`)往往更长,估算不足就会被截断。
|
||||
float resampleValueColumn = 0.0f;
|
||||
for (const SignalRef& ref : lane.signals)
|
||||
{
|
||||
const Lane::ResampleItem* item = lane.FindResampleItem(ref);
|
||||
if (item == nullptr)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
resampleValueColumn = std::max(
|
||||
resampleValueColumn,
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, FormatIntervalWithFrequency(item->maxInterval)));
|
||||
resampleValueColumn = std::max(
|
||||
resampleValueColumn,
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, FormatIntervalWithFrequency(item->minInterval)));
|
||||
}
|
||||
// 兜底下限:没有勾选任何信号时也保持一个可用的最小宽度。
|
||||
resampleValueColumn = std::max(resampleValueColumn,
|
||||
UiMeasureTextWidth(m_fonts->monoSmall, L"1h 1min 2s(1.23456 GHz)"));
|
||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + resampleLabelColumn +
|
||||
8.0f * m_scale + resampleValueColumn +
|
||||
(kFreeMarkerScrollWidth + 4.0f) * m_scale);
|
||||
@@ -205,6 +222,16 @@ namespace jdi
|
||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + buttonWidth);
|
||||
}
|
||||
|
||||
if (clipPanel)
|
||||
{
|
||||
// 「选区截取」底部是**两个并排**按钮,宽度必须容得下两段文字 + 间隙 + 内外边距。
|
||||
const float gap = 6.0f * m_scale;
|
||||
const float twoButtons =
|
||||
(UiMeasureTextWidth(m_fonts->uiSmall, kClipBuildLabel) + 28.0f * m_scale) + gap +
|
||||
(UiMeasureTextWidth(m_fonts->uiSmall, kClipDestructiveLabel) + 28.0f * m_scale);
|
||||
width = std::max(width, kEdgePanelPad * 2.0f * m_scale + twoButtons);
|
||||
}
|
||||
|
||||
if (countTable)
|
||||
{
|
||||
// 「边沿计数」表格:右侧要给内容滚动条留位(上百个信号时内容必然超高)。
|
||||
@@ -300,25 +327,31 @@ namespace jdi
|
||||
const bool weakPeak = (kind == EdgePanelKind::PEAK);
|
||||
const bool freeMarker = (kind == EdgePanelKind::MARK);
|
||||
const bool resamplePanel = (kind == EdgePanelKind::RESAMPLE);
|
||||
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」共用同一套**可滚动分组列表**排布
|
||||
//(信号色叠底条目栏 + 展开后的参数行);「边沿计数」自 2026-11-02 起走**表格**分支。
|
||||
const bool listStyle = (meanFilter || centerAxis || weakPeak || resamplePanel);
|
||||
const bool clipPanel = (kind == EdgePanelKind::CLIP);
|
||||
// 「均值滤波」「中心轴边沿计数」「弱峰调整」「重采样」「选区截取」共用同一套
|
||||
//**可滚动分组列表**排布(信号色叠底条目栏 + 展开后的参数行);
|
||||
//「边沿计数」自 2026-11-02 起走**表格**分支。
|
||||
//⚠️ 「选区截取」的条目**不可展开**(只需右侧复选框),因此只占一行。
|
||||
const bool listStyle = (meanFilter || centerAxis || weakPeak || resamplePanel || clipPanel);
|
||||
const bool countTable = (kind == EdgePanelKind::COUNT);
|
||||
const bool hasButton = (meanFilter || weakPeak || resamplePanel);
|
||||
const bool hasButton = (meanFilter || weakPeak || resamplePanel || clipPanel);
|
||||
const bool enabled = meanFilter ? lane.meanFilter
|
||||
: (weakPeak ? lane.weakPeak
|
||||
: (freeMarker ? lane.freeMarker
|
||||
: (resamplePanel ? lane.resample
|
||||
: (centerAxis ? lane.edgeCenter
|
||||
: lane.edgeCount))));
|
||||
: (clipPanel ? lane.clip
|
||||
: (centerAxis ? lane.edgeCenter
|
||||
: lane.edgeCount)))));
|
||||
const bool open = meanFilter ? lane.meanFilterPanel.open
|
||||
: (weakPeak ? lane.weakPeakPanel.open
|
||||
: (freeMarker ? lane.freeMarkerPanel.open
|
||||
: (resamplePanel
|
||||
? lane.resamplePanel.open
|
||||
: (centerAxis
|
||||
? lane.edgeCenterPanel.open
|
||||
: lane.edgePanel.open))));
|
||||
: (clipPanel
|
||||
? lane.clipPanel.open
|
||||
: (centerAxis
|
||||
? lane.edgeCenterPanel.open
|
||||
: lane.edgePanel.open)))));
|
||||
if (!enabled || !open)
|
||||
{
|
||||
return layout;
|
||||
@@ -329,9 +362,11 @@ namespace jdi
|
||||
: (freeMarker ? lane.freeMarkerPanel.collapsed
|
||||
: (resamplePanel
|
||||
? lane.resamplePanel.collapsed
|
||||
: (centerAxis
|
||||
? lane.edgeCenterPanel.collapsed
|
||||
: lane.edgePanel.collapsed))));
|
||||
: (clipPanel
|
||||
? lane.clipPanel.collapsed
|
||||
: (centerAxis
|
||||
? lane.edgeCenterPanel.collapsed
|
||||
: lane.edgePanel.collapsed)))));
|
||||
layout.collapsed = collapsed;
|
||||
|
||||
int rowCount = 0;
|
||||
@@ -356,7 +391,11 @@ namespace jdi
|
||||
// 因此不再有独立的底部编辑行;「边沿计数」走独立的**表格**分支。
|
||||
const float buttonHeight = hasButton ? (kEdgePanelButtonHeight * m_scale) : 0.0f;
|
||||
// 面板底部(内容区之下)占的纵向空间:有按钮时 = pad + 按钮 + pad,无按钮时只有 pad。
|
||||
const float footerHeight = hasButton ? (buttonHeight + pad) : 0.0f;
|
||||
// ⚠️ 「选区截取」底部是**两个**按钮行(截取为新信号 / 原始信号截取),
|
||||
// 因此 footer 要按两行算(用户 2026-11-03 要求)。
|
||||
const float footerHeight =
|
||||
clipPanel ? (buttonHeight * 2.0f + 6.0f * m_scale + pad)
|
||||
: (hasButton ? (buttonHeight + pad) : 0.0f);
|
||||
const float bottomPad = pad;
|
||||
|
||||
const float weakPeakSliderRow = kWeakPeakSliderRowHeight * m_scale;
|
||||
@@ -397,6 +436,10 @@ namespace jdi
|
||||
continue;
|
||||
}
|
||||
content += groupRow;
|
||||
if (clipPanel)
|
||||
{
|
||||
continue; // 「选区截取」的条目**不可展开**,每条只占一行
|
||||
}
|
||||
bool expanded = false;
|
||||
if (resamplePanel)
|
||||
{
|
||||
@@ -534,17 +577,21 @@ namespace jdi
|
||||
: (freeMarker ? lane.freeMarkerPanel.x
|
||||
: (resamplePanel
|
||||
? lane.resamplePanel.x
|
||||
: (centerAxis
|
||||
? lane.edgeCenterPanel.x
|
||||
: lane.edgePanel.x))));
|
||||
: (clipPanel
|
||||
? lane.clipPanel.x
|
||||
: (centerAxis
|
||||
? lane.edgeCenterPanel.x
|
||||
: lane.edgePanel.x)))));
|
||||
const double storedY = meanFilter ? lane.meanFilterPanel.y
|
||||
: (weakPeak ? lane.weakPeakPanel.y
|
||||
: (freeMarker ? lane.freeMarkerPanel.y
|
||||
: (resamplePanel
|
||||
? lane.resamplePanel.y
|
||||
: (centerAxis
|
||||
? lane.edgeCenterPanel.y
|
||||
: lane.edgePanel.y))));
|
||||
: (clipPanel
|
||||
? lane.clipPanel.y
|
||||
: (centerAxis
|
||||
? lane.edgeCenterPanel.y
|
||||
: lane.edgePanel.y)))));
|
||||
int openPanelOrdinal = 0;
|
||||
{
|
||||
// ⚠️ 错开量用「**当前真正显示出来的面板**中排在本面板之前的个数」:
|
||||
@@ -570,12 +617,15 @@ namespace jdi
|
||||
return lane.freeMarker && lane.freeMarkerPanel.open;
|
||||
case EdgePanelKind::RESAMPLE:
|
||||
return lane.resample && lane.resamplePanel.open;
|
||||
case EdgePanelKind::CLIP:
|
||||
return lane.clip && lane.clipPanel.open;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
static const EdgePanelKind kOrder[6] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
|
||||
static const EdgePanelKind kOrder[7] = {EdgePanelKind::COUNT, EdgePanelKind::CENTER,
|
||||
EdgePanelKind::MEAN, EdgePanelKind::PEAK,
|
||||
EdgePanelKind::MARK, EdgePanelKind::RESAMPLE};
|
||||
EdgePanelKind::MARK, EdgePanelKind::RESAMPLE,
|
||||
EdgePanelKind::CLIP};
|
||||
int openSeen = 0;
|
||||
bool found = false;
|
||||
for (std::size_t lane = 0; lane < m_lanes.size() && !found; ++lane)
|
||||
@@ -737,7 +787,22 @@ namespace jdi
|
||||
// 「均值滤波 / 弱峰调整」:可滚动内容区(信号色叠底条目栏 + 展开后的两行滑块)
|
||||
// +(均值滤波 / 弱峰调整)底部按钮。
|
||||
const float buttonRowHeight = kEdgePanelButtonHeight * m_scale;
|
||||
if (hasButton)
|
||||
if (clipPanel)
|
||||
{
|
||||
// 「选区截取」:底部**两个**并排的固定按钮(不参与滚动)。
|
||||
// ⚠️ 未启用双光标模式时两者都是空矩形 ⇒ 不可点(面板只显示提示文字)。
|
||||
const bool doubleCursor = (Tab().CursorMode() == PlotCursorMode::DOUBLE);
|
||||
if (doubleCursor)
|
||||
{
|
||||
const float gap = 6.0f * m_scale;
|
||||
const float halfWidth = std::max(0.0f, (width - pad * 2.0f - gap) * 0.5f);
|
||||
const float buttonTop = top + height - pad - buttonRowHeight;
|
||||
layout.clipBuildButton = gd::RectF(left + pad, buttonTop, halfWidth, buttonRowHeight);
|
||||
layout.clipDestructiveButton =
|
||||
gd::RectF(left + pad + halfWidth + gap, buttonTop, halfWidth, buttonRowHeight);
|
||||
}
|
||||
}
|
||||
else if (hasButton)
|
||||
{
|
||||
layout.button = gd::RectF(left + pad, top + height - pad - buttonRowHeight,
|
||||
std::max(0.0f, width - pad * 2.0f), buttonRowHeight);
|
||||
@@ -746,7 +811,9 @@ namespace jdi
|
||||
const float groupRow = kEdgePanelMeanRowHeight * m_scale;
|
||||
const float sliderRow = kWeakPeakSliderRowHeight * m_scale;
|
||||
const float scrollW = kFreeMarkerScrollWidth * m_scale;
|
||||
const float footerHeight = hasButton ? (buttonRowHeight + pad) : 0.0f;
|
||||
const float footerHeight =
|
||||
clipPanel ? (buttonRowHeight * 2.0f + 6.0f * m_scale + pad)
|
||||
: (hasButton ? (buttonRowHeight + pad) : 0.0f);
|
||||
const float viewHeight =
|
||||
std::max(0.0f, height - titleHeight - pad - pad - footerHeight);
|
||||
const bool scrollable = meanContentHeight > viewHeight + 0.5f;
|
||||
@@ -754,7 +821,8 @@ namespace jdi
|
||||
const float storedScroll = meanFilter ? lane.meanFilterScroll
|
||||
: (weakPeak ? lane.weakPeakScroll
|
||||
: (resamplePanel ? lane.resampleScroll
|
||||
: lane.edgeCenterScroll));
|
||||
: (clipPanel ? lane.clipScroll
|
||||
: lane.edgeCenterScroll)));
|
||||
const float scroll = std::max(0.0f, std::min(storedScroll, maxScroll));
|
||||
const float innerWidth =
|
||||
std::max(0.0f, width - pad * 2.0f - (scrollable ? (scrollW + 4.0f * m_scale) : 0.0f));
|
||||
@@ -812,7 +880,12 @@ namespace jdi
|
||||
}
|
||||
bool checked = false;
|
||||
bool itemCollapsed = false;
|
||||
if (resamplePanel)
|
||||
if (clipPanel)
|
||||
{
|
||||
// 「选区截取」:只有勾选状态,条目**不可展开**(用户 2026-11-03 要求)。
|
||||
checked = lane.ClipEnabled(ref);
|
||||
}
|
||||
else if (resamplePanel)
|
||||
{
|
||||
const Lane::ResampleItem* item = lane.FindResampleItem(ref);
|
||||
checked = (item != nullptr);
|
||||
@@ -842,11 +915,12 @@ namespace jdi
|
||||
gd::RectF(contentLeft, rowY, innerWidth, groupRow));
|
||||
layout.rowNameRects.push_back(gd::RectF(contentLeft, rowY, innerWidth, groupRow));
|
||||
layout.rowTriangles.push_back(
|
||||
checked ? gd::RectF(contentLeft + 3.0f * m_scale,
|
||||
rowY + (groupRow - kEdgePanelRowTriangleSize * m_scale) * 0.5f,
|
||||
kEdgePanelRowTriangleSize * m_scale,
|
||||
kEdgePanelRowTriangleSize * m_scale)
|
||||
: gd::RectF());
|
||||
(checked && !clipPanel)
|
||||
? gd::RectF(contentLeft + 3.0f * m_scale,
|
||||
rowY + (groupRow - kEdgePanelRowTriangleSize * m_scale) * 0.5f,
|
||||
kEdgePanelRowTriangleSize * m_scale,
|
||||
kEdgePanelRowTriangleSize * m_scale)
|
||||
: gd::RectF());
|
||||
layout.rowChecks.push_back(
|
||||
gd::RectF(contentLeft + innerWidth - closeSize - 4.0f * m_scale,
|
||||
rowY + (groupRow - closeSize) * 0.5f, closeSize, closeSize));
|
||||
@@ -856,7 +930,7 @@ namespace jdi
|
||||
rowY += groupRow;
|
||||
|
||||
// 参数行:只有勾选且未收起时才有几何(未勾选的行两者都是空矩形)。
|
||||
if (!checked || itemCollapsed)
|
||||
if (!checked || itemCollapsed || clipPanel)
|
||||
{
|
||||
layout.rowLevelTracks.push_back(gd::RectF());
|
||||
layout.rowLevelValues.push_back(gd::RectF());
|
||||
|
||||
Reference in new issue
Block a user