Files
Just-DataInspector/Utilities/Src/Utf8Util.cpp
T
DeepSeek V4.1 Flash fc77b0262f 「调整」行:系数/偏移 按钮 + 偏移调整(Y 轴/时间轴)二级面板;结算顺序加入偏移;派生信号继承调整变换
按钮与面板:
- 「系数」按钮改名为「系数/偏移」;点击展开的二级面板在原「系数调整」输入框下方新增「偏移调整」:
  与「系数调整」同一套绘制(标题 + 标题栏右侧的还原图标),下面两行分别是「Y轴」「时间轴」,
  每行左侧说明文字、右侧内联输入框(整行数值框)。
- 「Y轴」默认 0.00,非编辑态**始终带正负号**(+0.00 / -12.50),小数不足两位时补两位、更高精度保留更多位。
- 「时间轴」默认 0.00,非编辑态显示与双光标差值同一套复合单位文本(`Δt:500ms`、`Δt:1h 1min 2s`);
  进入编辑态时显示以**秒**为单位的数值,并在输入框最右侧提示单位 `s`。
- 「偏移调整」的还原图标把两个偏移恢复为默认值 0;「系数调整」的还原图标仍只重置系数。
- 面板/输入框的编辑初值与显示文本分离(`MenuItem::editValue` / `editSuffix`),
  非编辑态(`Δt:…`)与编辑态(`3662`)不一致时也能正确回显。

语义与结算顺序:
- 结算顺序变为 **系数 → 绝对值 → 镜像 → 偏移**(前三步不变)。
- Y 偏移:给绘制值加常数(波形上下移动);时间偏移:采样绘制在 `t + offsetTime` 处
  (例如 t=1s、偏移 -0.5s → 画在 0.5s)。
- ⚠️ Y 偏移以**未镜像空间**为准:垂直镜像开关变化时同步取反偏移量,
  使绘制等价于「先加上偏移再镜像」(开关来回切换可逆)。
- 绘制链路全部跟随偏移:`BuildPolyline`(逐点描边 / 像素列包络 / 跨列边界插值 / 粗粒度分组层,
  保序的 Y 偏移直接加在分组极值上)、派生预览曲线、光标取值读数(含 Δt)、
  可见区间 Y 轴自适应、整段自适应的区间映射;折线缓存键、场景指纹、样式快照与图层指纹都纳入两个偏移。

派生信号(实验性功能「构建为新信号」):
- 修复「预览是调整后的波形、构建出来却按原始状态绘制」:新信号**继承源信号的调整变换**
  (系数/绝对值/两个镜像/两个偏移)——网格预览绘制的是 `变换(滤波结果)`,
  而构建出的信号只存了滤波结果,因此必须把变换一并继承;
  时间轴偏移**只能**靠继承(派生信号与源信号共用同一文件时间轴,无法把时间平移烘进数据)。

离屏验证(不干扰用户):
- ⚠️ harness 现在在每次注入鼠标消息后**立即释放本窗口的鼠标捕获**
  (宿主 WM_LBUTTONDOWN 会 SetCapture,隐藏窗口同样会把用户的真实鼠标输入吸走);
  后续消息本来就是直接投递,无需捕获。
- 新增 [AP]:按钮标签不裁切、面板 5 行顺序与「偏移调整」位于系数输入框下方、
  Y轴/时间轴行的标签与输入框同行且默认值/单位提示正确、时间轴编辑态显示秒值并在最右侧提示 `s`、
  Y 偏移提交/显示正负号/波形整体平移量、时间偏移提交/Δt 复合单位(含 3662 → Δt:1h 1min 2s)/
  编辑态为秒、**按"偏移若干个采样点"构造的精确几何验证**(采样被画到移位后的像素位置、且不再画在原处)、
  读数跟随、两个还原图标、垂直镜像对 Y 偏移取反(可逆)、
  以及派生信号继承变换;[AL] 补充「系数→绝对值→镜像→偏移」的顺序与区间映射(含 Y 偏移)。
- 合计 645/645 通过;make clean && make all 零警告。
2026-10-13 17:56:00 +08:00

723 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "Utf8Util.hpp"
#include <windows.h>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
namespace jdi
{
namespace
{
/** @brief 判断字符是否为需要裁掉的空白字符。 */
bool IsSpaceChar(wchar_t c)
{
return c == L' ' || c == L'\t' || c == L'\r' || c == L'\n' || c == L'\v' || c == L'\f' ||
c == static_cast<wchar_t>(0x00A0);
}
/** @brief 判断字符是否为 ASCII 大写字母。 */
bool IsUpperAscii(wchar_t c)
{
return c >= L'A' && c <= L'Z';
}
/** @brief 时间间隔的显示单位。 */
struct DurationUnit
{
double scale = 1.0; /**< 1 个该单位等于多少秒。 */
const wchar_t* name = L"s"; /**< 单位名称。 */
};
}
std::wstring Utf8ToWide(const char* text, int length)
{
if (text == nullptr)
{
return std::wstring();
}
int byteCount = (length < 0) ? static_cast<int>(std::strlen(text)) : length;
if (byteCount <= 0)
{
return std::wstring();
}
int wideCount = MultiByteToWideChar(CP_UTF8, 0, text, byteCount, nullptr, 0);
if (wideCount <= 0)
{
return std::wstring();
}
std::wstring result(static_cast<size_t>(wideCount), L'\0');
MultiByteToWideChar(CP_UTF8, 0, text, byteCount, &result[0], wideCount);
return result;
}
std::wstring Trim(const std::wstring& text)
{
size_t begin = 0;
size_t end = text.size();
while (begin < end && IsSpaceChar(text[begin]))
{
++begin;
}
while (end > begin && IsSpaceChar(text[end - 1]))
{
--end;
}
return text.substr(begin, end - begin);
}
std::wstring StripQuotes(const std::wstring& text)
{
std::wstring trimmed = Trim(text);
if (trimmed.size() >= 2)
{
const wchar_t first = trimmed.front();
const wchar_t last = trimmed.back();
if ((first == L'"' && last == L'"') || (first == L'\'' && last == L'\''))
{
trimmed = trimmed.substr(1, trimmed.size() - 2);
}
}
return trimmed;
}
std::wstring ToLowerAscii(const std::wstring& text)
{
std::wstring result = text;
for (wchar_t& c : result)
{
if (IsUpperAscii(c))
{
c = static_cast<wchar_t>(c - L'A' + L'a');
}
}
return result;
}
bool ContainsInsensitive(const std::wstring& text, const std::wstring& needle)
{
if (needle.empty())
{
return true;
}
if (needle.size() > text.size())
{
return false;
}
const size_t last = text.size() - needle.size();
for (size_t i = 0; i <= last; ++i)
{
size_t j = 0;
while (j < needle.size())
{
wchar_t a = text[i + j];
wchar_t b = needle[j];
if (IsUpperAscii(a))
{
a = static_cast<wchar_t>(a - L'A' + L'a');
}
if (IsUpperAscii(b))
{
b = static_cast<wchar_t>(b - L'A' + L'a');
}
if (a != b)
{
break;
}
++j;
}
if (j == needle.size())
{
return true;
}
}
return false;
}
/** @brief 快速解析路径允许保留的最大有效位数(超过则交回 strtod 以保证正确舍入)。 */
const int kMaxFastDigits = 17;
/** @brief 快速解析路径允许使用的最大十进制指数(超出查表范围,交回 strtod)。 */
const int kMaxFastExponent = 22;
/** @brief 10 的 0-22 次幂精确表(该范围内每位都是 double 可精确表示的十进制数)。 */
const double kPow10Table[23] = {1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11,
1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22};
/**
* @brief 标准库解析路径:去掉首尾空白后交给 strtod。
*
* 作为快速路径的保底实现,保证十六进制浮点、inf/nan、超范围指数、
* 带尾部杂字符(如 "12abc")等输入的处理与改造前完全一致。
*
* @param [IN] begin 文本起始指针。
* @param [IN] end 文本结束指针(不包含)。
* @param [OUT] value 解析结果。
* @return 解析出数值返回 true。
*/
bool ParseDoubleSlow(const char* begin, const char* end, double& value)
{
const char* p = begin;
while (p < end && (*p == ' ' || *p == '\t'))
{
++p;
}
const char* q = end;
while (q > p && (q[-1] == ' ' || q[-1] == '\t' || q[-1] == '\r' || q[-1] == '\n'))
{
--q;
}
if (p >= q)
{
return false;
}
char buffer[96];
const size_t length = static_cast<size_t>(q - p);
const char* text = nullptr;
std::string dynamic;
if (length < sizeof(buffer))
{
std::memcpy(buffer, p, length);
buffer[length] = '\0';
text = buffer;
}
else
{
dynamic.assign(p, q);
text = dynamic.c_str();
}
char* stop = nullptr;
const double parsed = std::strtod(text, &stop);
if (stop == text)
{
return false;
}
value = parsed;
return true;
}
bool TryParseDouble(const char* begin, const char* end, double& value)
{
if (begin == nullptr || end == nullptr || begin >= end)
{
return false;
}
// ===== 快速路径:纯十进制数值 =====
// 大文件解析的绝对瓶颈就在这里:ucrt 的 strtod 要处理本地化、十六进制浮点、
// 无穷/NaN 等全部语法,实测每次调用约 335ns;百万行级文件需调用上千万次,
// 单解析就可能耗掉几十秒。下面这条路径只处理最常见的十进制写法(实测约 13ns,
// 快 25 倍),其余写法(含尾部杂字符)原样交回 strtod,
// 因此输入接受范围与原先完全一致。
const char* head = begin;
while (head < end && (*head == ' ' || *head == '\t'))
{
++head;
}
const char* tail = end;
while (tail > head && (tail[-1] == ' ' || tail[-1] == '\t' || tail[-1] == '\r' || tail[-1] == '\n'))
{
--tail;
}
if (head >= tail)
{
return false;
}
const char* cursor = head;
bool negative = false;
if (*cursor == '+' || *cursor == '-')
{
negative = (*cursor == '-');
++cursor;
}
unsigned long long mantissa = 0;
int digits = 0;
int exponent = 0;
// 整数部分:超过有效位数的高位数字只影响量级,累加进指数。
while (cursor < tail && *cursor >= '0' && *cursor <= '9')
{
if (digits < kMaxFastDigits)
{
mantissa = mantissa * 10ull + static_cast<unsigned>(*cursor - '0');
++digits;
}
else
{
++exponent;
}
++cursor;
}
// 小数部分:每保留一位就记一次小数位数;超出有效位数的直接丢弃(已低于 double 精度)。
int fractionDigits = 0;
if (cursor < tail && *cursor == '.')
{
++cursor;
while (cursor < tail && *cursor >= '0' && *cursor <= '9')
{
if (digits < kMaxFastDigits)
{
mantissa = mantissa * 10ull + static_cast<unsigned>(*cursor - '0');
++digits;
++fractionDigits;
}
++cursor;
}
}
if (digits == 0)
{
return ParseDoubleSlow(begin, end, value);
}
exponent -= fractionDigits;
if (cursor < tail && (*cursor == 'e' || *cursor == 'E'))
{
++cursor;
bool expNegative = false;
if (cursor < tail && (*cursor == '+' || *cursor == '-'))
{
expNegative = (*cursor == '-');
++cursor;
}
if (cursor >= tail || *cursor < '0' || *cursor > '9')
{
return ParseDoubleSlow(begin, end, value);
}
int expValue = 0;
while (cursor < tail && *cursor >= '0' && *cursor <= '9')
{
if (expValue < 1000000)
{
expValue = expValue * 10 + (*cursor - '0');
}
++cursor;
}
exponent += expNegative ? -expValue : expValue;
}
// 存在未消费字符(如 "12abc"、"1,5")或指数超出查表范围时交回标准库:
// 既保持原先 strtod 的宽松接受语义,也保证极端量级的正确舍入。
if (cursor != tail || exponent > kMaxFastExponent || exponent < -kMaxFastExponent)
{
return ParseDoubleSlow(begin, end, value);
}
double result = static_cast<double>(mantissa);
if (exponent > 0)
{
result *= kPow10Table[exponent];
}
else if (exponent < 0)
{
result /= kPow10Table[-exponent];
}
// mantissa 超过 2^53(16 位以上有效数字)时这里会比 strtod 多一次舍入,
// 相对误差不超过 1 ULP(实测 ≤2.3e-16),在波形显示与标尺(约 6 位有效数字)上不可见。
value = negative ? -result : result;
return true;
}
std::wstring FormatNumber(double value, int decimals)
{
if (decimals < 0)
{
decimals = 0;
}
if (decimals > 12)
{
decimals = 12;
}
wchar_t buffer[96];
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.*f", decimals, value);
return std::wstring(buffer);
}
std::wstring FormatDurationCompound(double seconds)
{
if (!std::isfinite(seconds))
{
return L"—";
}
const double value = std::fabs(seconds);
if (value <= 0.0)
{
return L"0s";
}
// ---- 顶级单位高于 s(h / min):整合为 h/min/s(整数秒)----
// 注意条件必须是 >= 60s:1~60s 属于"顶级单位为 s"的情况,需按下一条规则处理
// (非整数秒要递进到 ms,例如 5.06s → "5s 60ms")。
if (value >= 60.0)
{
const long long totalSeconds = static_cast<long long>(std::llround(value));
long long hours = 0;
long long minutes = 0;
long long secs = totalSeconds;
if (totalSeconds >= 3600)
{
hours = totalSeconds / 3600;
secs = totalSeconds % 3600;
}
if (secs >= 60)
{
minutes = secs / 60;
secs = secs % 60;
}
std::wstring text;
const auto append = [&text](long long amount, const wchar_t* unit)
{
if (amount <= 0)
{
return;
}
if (!text.empty())
{
text += L" ";
}
text += std::to_wstring(amount);
text += unit;
};
append(hours, L"h");
append(minutes, L"min");
append(secs, L"s");
if (!text.empty())
{
return text;
}
}
// ---- 顶级单位为 s:整数秒 + 可选 ms ----
if (value >= 1.0)
{
const double whole = std::floor(value);
const double fraction = value - whole;
std::wstring text = FormatValue(whole) + L"s";
const double remainderMs = fraction * 1000.0;
if (remainderMs >= 1.0e-6)
{
text += L" ";
text += FormatValue(remainderMs);
text += L"ms";
}
return text;
}
// ---- 顶级单位低于 s:ms / us / ns / ps ----
const DurationUnit subUnits[] = {
{1.0e-3, L"ms"}, {1.0e-6, L"us"}, {1.0e-9, L"ns"}, {1.0e-12, L"ps"},
};
const std::size_t subCount = sizeof(subUnits) / sizeof(subUnits[0]);
std::size_t index = subCount - 1;
for (std::size_t i = 0; i < subCount; ++i)
{
if (value >= subUnits[i].scale)
{
index = i;
break;
}
}
// 顶级单位为 ps:不再递进次级单位(保留小数)。
if (index + 1 >= subCount)
{
return FormatValue(value / subUnits[index].scale) + L" " + subUnits[index].name;
}
const double scaled = value / subUnits[index].scale;
double whole = std::floor(scaled);
double fraction = scaled - whole;
// 浮点误差可能导致 fraction 极接近 1(例如 4.9999999),归一化一下。
if (fraction >= 1.0 - 1.0e-9)
{
whole += 1.0;
fraction = 0.0;
}
std::wstring text = FormatValue(whole);
text += subUnits[index].name;
// 仍是该单位的整数 → 不显示下级单位。
const double remainder = fraction * subUnits[index].scale;
const double inNext = remainder / subUnits[index + 1].scale;
if (inNext >= 1.0e-6)
{
text += L" ";
text += FormatValue(inNext);
text += subUnits[index + 1].name;
}
return text;
}
std::wstring FormatDurationSeconds(double seconds)
{
if (!std::isfinite(seconds))
{
return L"0";
}
return FormatValue(std::fabs(seconds));
}
std::wstring FormatByteSize(unsigned long long bytes)
{ // 单位自适应:GB / MB / KB(阈值按 1024 进制)。
constexpr double kKilo = 1024.0;
constexpr double kMega = kKilo * 1024.0;
constexpr double kGiga = kMega * 1024.0;
const double value = static_cast<double>(bytes);
const wchar_t* unit = L"KB";
double scaled = value / kKilo;
if (value >= kGiga)
{
unit = L"GB";
scaled = value / kGiga;
}
else if (value >= kMega)
{
unit = L"MB";
scaled = value / kMega;
}
wchar_t buffer[64];
// ⚠️ 宽字符格式化必须用 `%ls`:MinGW 的 swprintf 按 C99 语义处理 `%s`
//(在宽格式串里 `%s` 表示**窄**字符串),写成 `%s` 会把宽字符串按单字节读取,
// 于是 "KB" 只打印出 "K"。
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.2f %ls", scaled, unit);
return std::wstring(buffer);
}
std::wstring FormatValue(double value)
{
if (std::isnan(value))
{
return L"NaN";
}
if (std::isinf(value))
{
return (value > 0.0) ? L"+Inf" : L"-Inf";
}
wchar_t buffer[96];
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.6g", value);
return std::wstring(buffer);
}
std::wstring FormatTick(double value, double step)
{
if (value == 0.0)
{
return L"0";
}
if (!std::isfinite(value))
{
return FormatValue(value);
}
int decimals = 0;
if (step > 0.0 && std::isfinite(step))
{
const double magnitude = std::ceil(-std::log10(step) - 1.0e-9);
if (magnitude > 0.0)
{
decimals = static_cast<int>(magnitude);
}
}
if (decimals > 9)
{
decimals = 9;
}
wchar_t buffer[96];
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.*f", decimals, value);
std::wstring text(buffer);
if (text.size() > 15)
{
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%.4e", value);
text.assign(buffer);
}
return text;
}
std::wstring FormatCount(unsigned long long value)
{
wchar_t buffer[32];
swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%llu", value);
std::wstring digits(buffer);
std::wstring result;
result.reserve(digits.size() + digits.size() / 3);
const size_t length = digits.size();
for (size_t i = 0; i < length; ++i)
{
if (i > 0 && ((length - i) % 3u) == 0u)
{
result.push_back(L',');
}
result.push_back(digits[i]);
}
return result;
}
double NiceStep(double range, int targetTicks)
{
if (!(range > 0.0) || !std::isfinite(range) || targetTicks <= 0)
{
return 1.0;
}
const double raw = range / static_cast<double>(targetTicks);
const double magnitude = std::pow(10.0, std::floor(std::log10(raw)));
const double normalized = raw / magnitude;
double nice = 10.0;
if (normalized <= 1.0)
{
nice = 1.0;
}
else if (normalized <= 2.0)
{
nice = 2.0;
}
else if (normalized <= 5.0)
{
nice = 5.0;
}
return nice * magnitude;
}
void BuildTicks(double minValue, double maxValue, double step, std::vector<double>& ticks)
{
ticks.clear();
if (!std::isfinite(minValue) || !std::isfinite(maxValue) || !(step > 0.0) || !std::isfinite(step))
{
return;
}
if (maxValue < minValue)
{
const double temp = minValue;
minValue = maxValue;
maxValue = temp;
}
const long long first = static_cast<long long>(std::ceil(minValue / step - 1.0e-9));
const long long last = static_cast<long long>(std::floor(maxValue / step + 1.0e-9));
if (last < first)
{
return;
}
const long long count = last - first + 1;
if (count > 512)
{
return;
}
ticks.reserve(static_cast<size_t>(count));
for (long long index = first; index <= last; ++index)
{
ticks.push_back(static_cast<double>(index) * step);
}
}
bool CopyTextToClipboard(HWND owner, const std::wstring& text)
{
if (text.empty())
{
return false;
}
if (OpenClipboard(owner) == FALSE)
{
return false;
}
bool success = false;
if (EmptyClipboard() != FALSE)
{
const SIZE_T bytes = (text.size() + 1) * sizeof(wchar_t);
HGLOBAL handle = GlobalAlloc(GMEM_MOVEABLE, bytes);
if (handle != nullptr)
{
void* target = GlobalLock(handle);
if (target != nullptr)
{
std::memcpy(target, text.c_str(), bytes);
GlobalUnlock(handle);
if (SetClipboardData(CF_UNICODETEXT, handle) != nullptr)
{
success = true;
}
else
{
GlobalFree(handle);
}
}
else
{
GlobalFree(handle);
}
}
}
CloseClipboard();
return success;
}
bool PasteTextFromClipboard(HWND owner, std::wstring& text)
{
text.clear();
if (OpenClipboard(owner) == FALSE)
{
return false;
}
bool success = false;
if (IsClipboardFormatAvailable(CF_UNICODETEXT) != FALSE)
{
HANDLE handle = GetClipboardData(CF_UNICODETEXT);
if (handle != nullptr)
{
const void* source = GlobalLock(handle);
if (source != nullptr)
{
const wchar_t* wide = static_cast<const wchar_t*>(source);
text.assign(wide);
GlobalUnlock(handle);
success = !text.empty();
}
}
}
CloseClipboard();
// 单行输入框:把换行与制表符折叠为空格,避免内容被截断显示。
if (success)
{
for (wchar_t& c : text)
{
if (c == L'\r' || c == L'\n' || c == L'\t')
{
c = L' ';
}
}
}
return success;
}
} // namespace jdi