diff --git a/Components/PlotView/Src/PlotView.cpp b/Components/PlotView/Src/PlotView.cpp index 92cc0f0..baf427f 100644 --- a/Components/PlotView/Src/PlotView.cpp +++ b/Components/PlotView/Src/PlotView.cpp @@ -31,6 +31,9 @@ namespace jdi const float kCursorHandleReserve = 9.0f; /**< 光标顶部三角把手占用的高度(数据点贴边时需避让)。 */ const float kMarkerEdgeInset = 7.0f; /**< 数据点越界贴边时距网格可见边界的间距。 */ const float kMarkerInsideMargin = 2.0f; /**< 数据点在视界内时允许贴到边界的最小余量。 */ + const float kMarkerTriangleArm = 5.0f; /**< 越界方向三角"半宽/半高"(垂直于朝向的半边长)。 */ + /** 越界方向三角沿朝向的深度(与光标把手同比例)。 */ + const float kMarkerTriangleDepth = kMarkerTriangleArm * 1.4f; const size_t kMaxScatterPoints = 2500; /**< 单条散点最大绘制点数。 */ const size_t kPolylineCacheLimit = 64; /**< 折线顶点缓存条目上限。 */ const float kMinScatterRadius = 1.6f; /**< 散点最小半径(逻辑像素)。 */ @@ -883,21 +886,35 @@ namespace jdi Tab().ViewMax() = high; Tab().ViewInitialized() = true; Tab().LaneScroll() = 0.0f; - // 自适应即回到初始状态:退出拖拽缩放模式并放下进行中的选区与拖拽。 - Tab().ZoomMode() = PlotZoomMode::NONE; + // 只放下进行中的选区与拖拽,**不改变「选区放大」等缩放模式的启用状态** + // (用户明确要求:点适应视图后仍保持已启用的缩放模式)。 m_rubberRect = gd::RectF(); m_dragMode = PlotDragMode::NONE; m_dragSignal = SignalRef(); m_dragTargetLane = -1; m_hoverSignal = SignalRef(); + SetHint(Tab().ZoomMode() == PlotZoomMode::NONE + ? std::wstring() + : L"横向或纵向拖动以放大选择区域,ESC退出选区模式"); if (m_tab != nullptr) { - // 适应视图 = 回到初始状态:全部网格恢复整段数据的自动自适应。 - // 「选区 Y 轴自适应」是用户设置项,这里保持不变。 - for (Lane& lane : m_tab->Lanes()) + // 适应视图 = 回到初始状态:恢复整段数据的自动自适应。 + // 但**开启了「选区 Y 轴自适应」的网格必须继续跟随时间视图**: + // 直接置 autoRange 会让 ComputeLaneRange 忽略已经算好的时间段幅值, + // 表现为"点一下适应视图就把 Y 轴自适应关掉了"。这里改为按新的 + // (整段)时间视图重新匹配一次,开关状态与视觉效果都保持。 + const int laneCount = static_cast(m_tab->Lanes().size()); + for (int index = 0; index < laneCount; ++index) { - lane.autoRange = true; + if (m_tab->Lanes()[static_cast(index)].fitYToSelection) + { + AutoRangeLaneToView(index); + } + else + { + m_tab->Lanes()[static_cast(index)].autoRange = true; + } } } // 适应视图已回到初始状态,这里不再移动光标(光标允许停留在可视区外)。 @@ -1152,6 +1169,10 @@ namespace jdi // 否则展开后会看到“时间对了但幅值区间不合适”的波形。 AutoRangeLaneToView(laneIndex); + // 其它开启了「选区 Y 轴自适应」的网格同样要跟新的时间视图重新匹配: + // 时间视图变了,只让被展开的那个网格合适、其余网格停留在旧幅值区间并不符合预期。 + ApplySelectionAutoFit(); + // 注意:不在这里移动光标——光标允许滞留在网格可视区域之外。 BuildLaneVisuals(); return true; @@ -5787,9 +5808,10 @@ namespace jdi // 依次向下(再向上)避让已占用的标注,保证不会互相覆盖。 PlaceCursorLabel(labelRect, labelTop, textHeight, clipTop, clipBottom); - // 上下越界的方向三角与最终落位的取值标注纵向居中对齐 - // (用户明确要求“超过上下视界时三角需要与显示的数值居中对齐”)。 - if (marker.offUp || marker.offDown) + // 越界方向三角与最终落位的取值标注纵向居中对齐 + // (用户明确要求“超过上下视界时三角需要与显示的数值居中对齐”; + // 左右越界时同样对齐,避免三角与数值错行、看起来不协调)。 + if (marker.offUp || marker.offDown || marker.offLeft || marker.offRight) { marker.y = labelRect.Y + labelRect.Height * 0.5f; } @@ -5865,8 +5887,9 @@ namespace jdi std::min(centerTop, clipBottom - textHeight - 1.0f * m_scale)); gd::RectF centerRect(centerLeft, centerTop, centerTextWidth, textHeight); PlaceCursorLabel(centerRect, centerTop, textHeight, clipTop, clipBottom); - // 与原始信号同一规则:上下越界的方向三角与取值标注纵向居中对齐。 - if (centerMarker.offUp || centerMarker.offDown) + // 与原始信号同一规则:越界方向三角与取值标注纵向居中对齐。 + if (centerMarker.offUp || centerMarker.offDown || centerMarker.offLeft || + centerMarker.offRight) { centerMarker.y = centerRect.Y + centerRect.Height * 0.5f; } @@ -6084,6 +6107,20 @@ namespace jdi marker.offDown = (rawY > clipBottom); marker.x = pinX; + // 左右越界时三角沿水平方向**向外**伸出 `kMarkerTriangleDepth`: + // 光标贴边时 pinX 只离边界 1px,若直接把锚点放在那里,三角会有一大半画到 + // 网格绘制区之外(被裁剪后只看到残缺的一角,描边也随之断开)。 + // 因此把锚点沿水平方向**向内**推 keepIn,保证三角完整落在绘制区内。 + const float keepIn = (kMarkerTriangleDepth + 1.0f) * m_scale; + if (marker.offLeft) + { + marker.x = std::max(pinX, left + keepIn); + } + else if (marker.offRight) + { + marker.x = std::min(pinX, right - keepIn); + } + if (marker.offUp) { // 上越界:为方向三角与顶部把手预留高度,避免压住把手。 @@ -6108,8 +6145,8 @@ namespace jdi { gd::SolidBrush ring(ToGdiColor(ringColor)); gd::SolidBrush fill(ToGdiColor(fillColor)); - const float arm = 5.0f * m_scale; /**< 三角“半宽/半高”(垂直于朝向的半边长)。 */ - const float depth = arm * 1.4f; /**< 三角沿朝向的深度(与光标把手同比例)。 */ + const float arm = kMarkerTriangleArm * m_scale; /**< 三角“半宽/半高”(垂直于朝向的半边长)。 */ + const float depth = kMarkerTriangleDepth * m_scale; /**< 三角沿朝向的深度(与光标把手同比例)。 */ // 越界三角:**尖端朝外**(指向数据点所在的方向,即视界之外),底边在网格内侧; // 三角外接矩形在横/纵两个方向都与锚点**居中**(上下越界时因此与取值标注纵向居中)。 diff --git a/Utilities/Src/ColorPickerPopup.cpp b/Utilities/Src/ColorPickerPopup.cpp index a5f2494..91deabd 100644 --- a/Utilities/Src/ColorPickerPopup.cpp +++ b/Utilities/Src/ColorPickerPopup.cpp @@ -29,6 +29,8 @@ namespace jdi const float kHueWidth = 14.0f; /**< 色相条宽度。 */ const float kGamutGap = 10.0f; /**< 色域方块与色相条间距。 */ const float kColumnGap = 12.0f; /**< 色相条与滑块列间距。 */ + const float kSwatchWidth = 52.0f; /**< 预览色块宽度。 */ + const float kSwatchHexGap = 8.0f; /**< 预览色块与色号间距。 */ const float kGamutBorder = 1.0f; /**< 色域方块/色相条描边宽度。 */ /** @brief 通道显示名称。 */ @@ -216,12 +218,13 @@ namespace jdi void ColorPickerPopup::BuildLayout(const gd::RectF& bounds, const gd::RectF& anchor) { + // 布局:左侧一列放"色域方块 + 色相条",右侧一列放 R/G/B 三行 + //(每行 = 通道标签 + 滑块 + 数值),两列下方再放"预览色块 + 色号"一行。 + // 所有控件都从面板内边距起算,因此不会出现越界。 const float width = kPanelWidth * m_scale; - // 高度:标题 + 内容区。内容区里"色域方块"是最高的控件, - // 预览行/色号行落在它与滑块列的下方,仍在其纵向范围内,不再额外叠加高度。 - const float contentHeight = std::max(kGamutSize, kRowHeight * 3.0f + kRowGap * 2.0f + - kPreviewGap + kPreviewHeight); - const float height = (kPadV * 2.0f + kTitleHeight + kTitleGap + contentHeight) * m_scale; + const float height = (kPadV * 2.0f + kTitleHeight + kTitleGap + kGamutSize + kPreviewGap + + kPreviewHeight) * + m_scale; const float gap = 4.0f * m_scale; @@ -247,38 +250,46 @@ namespace jdi width - kPadH * 2.0f * m_scale, kTitleHeight * m_scale); const float contentTop = m_titleRect.Y + m_titleRect.Height + kTitleGap * m_scale; + const float contentLeft = left + kPadH * m_scale; + const float contentRight = left + width - kPadH * m_scale; - // 左侧:色域方块(横轴饱和度、纵轴明度)+ 色相条。 - m_gamutRect = gd::RectF(left + kPadH * m_scale, contentTop, kGamutSize * m_scale, - kGamutSize * m_scale); + // 左列:色域方块(横轴饱和度、纵轴明度)+ 色相条。 + m_gamutRect = gd::RectF(contentLeft, contentTop, kGamutSize * m_scale, kGamutSize * m_scale); m_hueRect = gd::RectF(m_gamutRect.X + m_gamutRect.Width + kGamutGap * m_scale, contentTop, kHueWidth * m_scale, kGamutSize * m_scale); - // 右侧:R/G/B 滑块 + 数值,紧随色相条之后。 - const float columnLeft = m_hueRect.X + m_hueRect.Width + kColumnGap * m_scale; - const float columnWidth = - std::max(80.0f * m_scale, m_rect.X + m_rect.Width - kPadH * m_scale - columnLeft); + // 右列:R/G/B 三行。整列在方块高度内纵向居中,与左侧方块视觉平衡。 + const float leftColumnWidth = + (kGamutSize + kGamutGap + kHueWidth) * m_scale; + const float columnLeft = m_gamutRect.X + leftColumnWidth + kColumnGap * m_scale; + const float columnWidth = std::max(60.0f * m_scale, contentRight - columnLeft); + const float rowsHeight = (kRowHeight * 3.0f + kRowGap * 2.0f) * m_scale; + const float firstRowTop = + contentTop + std::max(0.0f, (kGamutSize * m_scale - rowsHeight) * 0.5f); + const float trackLeft = columnLeft + (kLabelWidth + kLabelGap) * m_scale; const float trackWidth = - std::max(24.0f, columnWidth - (kLabelWidth + kLabelGap + kValueWidth) * m_scale); + std::max(24.0f, columnWidth - (kLabelWidth + kLabelGap + kValueWidth + 6.0f) * m_scale); for (int index = 0; index < 3; ++index) { - const float rowTop = - contentTop + static_cast(index) * (kRowHeight + kRowGap) * m_scale; + const float rowTop = firstRowTop + static_cast(index) * (kRowHeight + kRowGap) * m_scale; m_tracks[static_cast(index)] = gd::RectF(trackLeft, rowTop + (kRowHeight * m_scale - kTrackHeight * m_scale) * 0.5f, trackWidth, kTrackHeight * m_scale); m_values[static_cast(index)] = - gd::RectF(trackLeft + trackWidth + 6.0f * m_scale, rowTop, - kValueWidth * m_scale - 6.0f * m_scale, kRowHeight * m_scale); + gd::RectF(trackLeft + trackWidth + 6.0f * m_scale, rowTop, kValueWidth * m_scale, + kRowHeight * m_scale); } - const float previewTop = - contentTop + (3.0f * (kRowHeight + kRowGap) + kPreviewGap) * m_scale; - m_previewRect = gd::RectF(trackLeft, previewTop + 2.0f * m_scale, 52.0f * m_scale, 20.0f * m_scale); - m_hexRect = gd::RectF(m_previewRect.X + m_previewRect.Width + 8.0f * m_scale, - previewTop + 2.0f * m_scale, 96.0f * m_scale, 20.0f * m_scale); + // 下行:预览色块 + 色号,与上方的"方块 + 色相条"同宽。 + const float previewTop = contentTop + (kGamutSize + kPreviewGap) * m_scale; + m_previewRect = + gd::RectF(contentLeft, previewTop, kSwatchWidth * m_scale, kPreviewHeight * m_scale); + m_hexRect = gd::RectF(m_previewRect.X + m_previewRect.Width + kSwatchHexGap * m_scale, + previewTop, + std::max(40.0f * m_scale, leftColumnWidth - (kSwatchWidth + kSwatchHexGap) * m_scale), + kPreviewHeight * m_scale); } void ColorPickerPopup::Open(const gd::RectF& bounds, const gd::RectF& anchor, std::uint32_t color) @@ -911,43 +922,55 @@ namespace jdi } UiDrawRoundRect(g, m_gamutRect, 2.0f * m_scale, m_theme->borderStrong, kGamutBorder); - // ---- 色相条(纵向 0-1,用 6 段线性渐变近似色相环)---- + // ---- 色相条(纵向 0-1):6 段线性渐变拼出完整色相环 ---- { - const Hsv hsv = ToHsv(m_channels); - const double saturation = (hsv.s > 1.0e-4) ? hsv.s : 1.0; - const double value = (hsv.v > 1.0e-4) ? hsv.v : 1.0; - const int bands = std::max(12, static_cast(std::lround(m_hueRect.Height))); - for (int index = 0; index < bands; ++index) + for (int segment = 0; segment < 6; ++segment) { - const double hue = (static_cast(index) + 0.5) / static_cast(bands); - const std::array rgb = FromHsv(hue, 1.0, 1.0); - const std::uint32_t band = 0xFF000000u | (ToByte(rgb[0]) << 16) | (ToByte(rgb[1]) << 8) | - ToByte(rgb[2]); - const float top = m_hueRect.Y + m_hueRect.Height * index / static_cast(bands); - const float bottom = - m_hueRect.Y + m_hueRect.Height * (index + 1) / static_cast(bands); - UiFillRect(g, gd::RectF(m_hueRect.X, top, m_hueRect.Width, bottom - top), band); + const double hue0 = static_cast(segment) / 6.0; + const double hue1 = static_cast(segment + 1) / 6.0; + const std::array rgb0 = FromHsv(hue0, 1.0, 1.0); + const std::array rgb1 = FromHsv(hue1, 1.0, 1.0); + const std::uint32_t color0 = + 0xFF000000u | (ToByte(rgb0[0]) << 16) | (ToByte(rgb0[1]) << 8) | ToByte(rgb0[2]); + const std::uint32_t color1 = + 0xFF000000u | (ToByte(rgb1[0]) << 16) | (ToByte(rgb1[1]) << 8) | ToByte(rgb1[2]); + + const float top = m_hueRect.Y + m_hueRect.Height * static_cast(hue0); + const float bottom = m_hueRect.Y + m_hueRect.Height * static_cast(hue1); + const gd::Point from(static_cast(std::lround(m_hueRect.X)), + static_cast(std::lround(top))); + const gd::Point to(static_cast(std::lround(m_hueRect.X)), + static_cast(std::lround(bottom))); + gd::LinearGradientBrush brush(from, to, ToGdiColor(color0), ToGdiColor(color1)); + // 渐变端点会各自向两侧延伸半段,逐段微重叠地填充即可无缝拼接。 + g.FillRectangle(&brush, m_hueRect.X, top, m_hueRect.Width, bottom - top); } - UiDrawRoundRect(g, m_hueRect, 2.0f * m_scale, m_theme->borderStrong, kGamutBorder); + UiDrawRoundRect(g, m_hueRect, 2.0f * m_scale, m_theme->border, kGamutBorder); - // 控件指示器:色域方块上标出 (S, V),色相条上标出当前色相。 - // 两者都由 m_channels 反推,因此与 R/G/B 滑块、十六进制输入天然联动。 - const float markerX = m_gamutRect.X + static_cast(saturation) * m_gamutRect.Width; - const float markerY = m_gamutRect.Y + static_cast(1.0 - value) * m_gamutRect.Height; - gd::Pen outerPen(ToGdiColor(0xFF1B1B1Bu), 2.0f * m_scale); - gd::Pen innerPen(ToGdiColor(0xFFFFFFFFu), 1.4f * m_scale); - const float outerRadius = 5.0f * m_scale; - g.DrawEllipse(&outerPen, markerX - outerRadius, markerY - outerRadius, outerRadius * 2.0f, - outerRadius * 2.0f); - const float innerRadius = 3.4f * m_scale; - g.DrawEllipse(&innerPen, markerX - innerRadius, markerY - innerRadius, innerRadius * 2.0f, - innerRadius * 2.0f); + // ---- 色域方块上的选点:中心就是当前选中的颜色,外面只加一圈白色描边 ---- + const Hsv hsv = ToHsv(m_channels); + const float markerX = m_gamutRect.X + static_cast(hsv.s) * m_gamutRect.Width; + const float markerY = m_gamutRect.Y + static_cast(1.0 - hsv.v) * m_gamutRect.Height; + const float markerRadius = 5.5f * m_scale; + gd::SolidBrush markerBrush(ToGdiColor(CurrentColor())); + g.FillEllipse(&markerBrush, markerX - markerRadius, markerY - markerRadius, + markerRadius * 2.0f, markerRadius * 2.0f); + gd::Pen markerPen(ToGdiColor(0xFFFFFFFFu), 2.0f * m_scale); + g.DrawEllipse(&markerPen, markerX - markerRadius, markerY - markerRadius, + markerRadius * 2.0f, markerRadius * 2.0f); - const float hueMarkerY = m_hueRect.Y + static_cast(CurrentHue()) * m_hueRect.Height; - UiFillRect(g, gd::RectF(m_hueRect.X - 2.0f * m_scale, hueMarkerY - 1.5f * m_scale, - m_hueRect.Width + 4.0f * m_scale, 3.0f * m_scale), - 0xFF1B1B1Bu); - UiDrawRoundRect(g, m_hueRect, 2.0f * m_scale, m_theme->borderStrong, kGamutBorder); + // ---- 色相条上的指示器:白色描边的圆角长方条,底色就是该位置的颜色 ---- + const double hue = CurrentHue(); + const std::array hueRgb = FromHsv(hue, 1.0, 1.0); + const std::uint32_t hueColor = + 0xFF000000u | (ToByte(hueRgb[0]) << 16) | (ToByte(hueRgb[1]) << 8) | ToByte(hueRgb[2]); + const float hueMarkerY = m_hueRect.Y + static_cast(hue) * m_hueRect.Height; + const float pillHeight = 10.0f * m_scale; + const float pillWidth = m_hueRect.Width + 9.0f * m_scale; + const gd::RectF pill(m_hueRect.X + (m_hueRect.Width - pillWidth) * 0.5f, + hueMarkerY - pillHeight * 0.5f, pillWidth, pillHeight); + UiFillRoundRect(g, pill, pillHeight * 0.5f, hueColor); + UiDrawRoundRect(g, pill, pillHeight * 0.5f, 0xFFFFFFFFu, 1.8f * m_scale); } for (int index = 0; index < 3; ++index) @@ -955,7 +978,8 @@ namespace jdi const std::size_t slot = static_cast(index); const gd::RectF& track = m_tracks[slot]; - const gd::RectF labelRect(m_rect.X + kPadH * m_scale, track.Y - 9.0f * m_scale, + // 通道标签放在滑块同一行的左侧,与轨道/数值纵向对齐。 + const gd::RectF labelRect(track.X - (kLabelWidth + kLabelGap) * m_scale, m_values[slot].Y, kLabelWidth * m_scale, kRowHeight * m_scale); UiText(g, kChannelNames[index], m_fonts->uiSmall, labelRect, m_theme->textMuted, TextAlign::LEFT); diff --git a/Utilities/Src/DataModel.cpp b/Utilities/Src/DataModel.cpp index 29f769b..e3fbcef 100644 --- a/Utilities/Src/DataModel.cpp +++ b/Utilities/Src/DataModel.cpp @@ -1252,12 +1252,35 @@ namespace jdi return; } std::vector& fileVisible = m_visible[static_cast(fileIndex)]; + // 合并模式下“新增”的信号同样应各自独占网格(与 SetVisible 一致): + // 先切到自定义划分,RebuildLanes 会把尚未分配网格的可见信号追加到最下方。 + // 只有确实存在“原本不可见”的信号时才切模式,避免重复点击默默改掉划分策略。 + const bool anyHidden = + std::find(fileVisible.begin(), fileVisible.end(), false) != fileVisible.end(); + if (visible && anyHidden && m_laneMode == LaneMode::MERGED) + { + m_laneMode = LaneMode::CUSTOM; + } std::fill(fileVisible.begin(), fileVisible.end(), visible); SyncLanes(); } void TabConfig::SetAllVisible(bool visible) { + bool anyHidden = false; + for (const std::vector& fileVisible : m_visible) + { + if (std::find(fileVisible.begin(), fileVisible.end(), false) != fileVisible.end()) + { + anyHidden = true; + break; + } + } + // 同上:合并模式下新增的信号各自独占网格,且排列在最下方。 + if (visible && anyHidden && m_laneMode == LaneMode::MERGED) + { + m_laneMode = LaneMode::CUSTOM; + } for (std::vector& fileVisible : m_visible) { std::fill(fileVisible.begin(), fileVisible.end(), visible);