diff --git a/.gitignore b/.gitignore index 51db085..d526a0c 100644 --- a/.gitignore +++ b/.gitignore @@ -15,8 +15,10 @@ Makefile* moc_*.cpp moc_*.h -qrc_*.cpp -ui_*.h +qrc_*.cpp +*_resource.rc +*_resource.res +ui_*.h *.autosave # 构建目录 diff --git a/CloudUtils/CloudUtils.pro b/CloudUtils/CloudUtils.pro index d62a055..70fb86f 100644 --- a/CloudUtils/CloudUtils.pro +++ b/CloudUtils/CloudUtils.pro @@ -4,10 +4,17 @@ QT += core # 鎸囧畾椤圭洰妯℃澘涓洪潤鎬佸簱 -TEMPLATE = lib -CONFIG += staticlib - -win32-msvc { +TEMPLATE = lib +CONFIG += staticlib + +win32:CONFIG(debug, debug|release) { + DESTDIR = $$PWD/debug +} +win32:CONFIG(release, debug|release) { + DESTDIR = $$PWD/release +} + +win32-msvc { QMAKE_CXXFLAGS += /utf-8 } diff --git a/CloudUtils/Src/LaserDataLoader.cpp b/CloudUtils/Src/LaserDataLoader.cpp index d66c23d..052be43 100644 --- a/CloudUtils/Src/LaserDataLoader.cpp +++ b/CloudUtils/Src/LaserDataLoader.cpp @@ -1,244 +1,275 @@ -#include "LaserDataLoader.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "VrLog.h" - -// 辅助函数:去除字符串末尾的 \r 字符(处理 Windows 格式的 CR LF 换行符) -static inline void TrimCarriageReturn(std::string& str) -{ - if (!str.empty() && str.back() == '\r') { - str.pop_back(); - } -} - +#include "LaserDataLoader.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "VrLog.h" + +// 辅助函数:去除字符串末尾的 \r 字符(处理 Windows 格式的 CR LF 换行符) +static inline void TrimCarriageReturn(std::string& str) +{ + if (!str.empty() && str.back() == '\r') { + str.pop_back(); + } +} + namespace { struct DatValue2DFloat { - double x; - double y; - double value; -}; - -struct Dat3DPoint -{ - double X; - double Y; - double Z; -}; - -struct Dat3DFeaturePoint -{ - int index; - int flag; - DatValue2DFloat stereo; - DatValue2DFloat stereoRight; - Dat3DPoint eye; - Dat3DPoint world; - std::uint64_t next; -}; - -struct DatLaserScan3DLinesHeader -{ - char dataCalibrated; - char adjusted; - int scanLineNum; - int vr1; - int vr2; - double lineV; - int clock; - std::uint32_t maxTimeStamp; - std::uint64_t scanLineList; -}; - -struct DatLineScan3DData -{ - int lineIndex; - std::uint32_t timeStamp; - double lineStepping; -}; - -static_assert(sizeof(DatLaserScan3DLinesHeader) == 40, "Unexpected dat header layout"); -static_assert(sizeof(Dat3DFeaturePoint) == 112, "Unexpected dat feature point layout"); - -template -bool ReadBinary(std::ifstream& inputFile, T& value) -{ - inputFile.read(reinterpret_cast(&value), sizeof(T)); - return static_cast(inputFile); -} - -bool HasFileExtension(const std::string& fileName, const char* extension) -{ - const size_t dotPos = fileName.find_last_of('.'); - if (dotPos == std::string::npos) { - return false; - } - - const size_t slashPos = fileName.find_last_of("/\\"); - if (slashPos != std::string::npos && dotPos < slashPos) { - return false; - } - - std::string fileExt = fileName.substr(dotPos); - std::transform(fileExt.begin(), fileExt.end(), fileExt.begin(), - [](unsigned char c) { return static_cast(std::tolower(c)); }); - return fileExt == extension; -} - -bool LooksLikeTextLaserFile(const std::string& fileName) -{ - std::ifstream inputFile(fileName, std::ios::binary); - if (!inputFile.is_open()) { - return false; - } - - char header[8] = {}; - inputFile.read(header, sizeof(header)); - return inputFile.gcount() == sizeof(header) && std::memcmp(header, "LineNum:", sizeof(header)) == 0; -} - -bool ReadDatLineRecord(std::ifstream& inputFile, int recordSize, DatLineScan3DData& lineData) -{ - char buffer[40] = {}; - if (recordSize <= 0 || recordSize > static_cast(sizeof(buffer))) { - return false; - } - - inputFile.read(buffer, recordSize); - if (!inputFile) { - return false; - } - - std::memcpy(&lineData.lineIndex, buffer, sizeof(lineData.lineIndex)); - std::memcpy(&lineData.timeStamp, buffer + 4, sizeof(lineData.timeStamp)); - std::memcpy(&lineData.lineStepping, buffer + 8, sizeof(lineData.lineStepping)); - return true; -} - -void AppendFormattedLine(std::string& output, const char* format, ...) -{ - char stackBuffer[256]; - - va_list args; - va_start(args, format); - va_list argsCopy; - va_copy(argsCopy, args); - int written = vsnprintf(stackBuffer, sizeof(stackBuffer), format, args); - va_end(args); - - if (written < 0) { - va_end(argsCopy); - return; - } - - if (static_cast(written) < sizeof(stackBuffer)) { - output.append(stackBuffer, static_cast(written)); - va_end(argsCopy); - return; - } - - std::vector dynamicBuffer(static_cast(written) + 1); - vsnprintf(dynamicBuffer.data(), dynamicBuffer.size(), format, argsCopy); - va_end(argsCopy); - output.append(dynamicBuffer.data(), static_cast(written)); -} -} - -LaserDataLoader::LaserDataLoader() -{ - m_lastError.clear(); -} - -LaserDataLoader::~LaserDataLoader() -{ -} - -int LaserDataLoader::LoadLaserScanData(const std::string& fileName, - std::vector>& laserLines, - int& lineNum, - float& scanSpeed, - int& maxTimeStamp, - int& clockPerSecond) -{ - LOG_INFO("Loading from file: %s\n", fileName.c_str()); - - // 清空输出参数 - laserLines.clear(); - lineNum = 0; - scanSpeed = 0.0f; - maxTimeStamp = 0; - clockPerSecond = 0; - - if (HasFileExtension(fileName, ".dat") && !LooksLikeTextLaserFile(fileName)) { - return _LoadLaserScanDataFromDatFile(fileName, laserLines, lineNum, scanSpeed, maxTimeStamp, clockPerSecond); - } - - // 判断文件类型 - std::ifstream inputFile(fileName); - if (!inputFile.is_open()) { - m_lastError = "Cannot open file: " + fileName; - LOG_ERROR("Cannot open file: %s\n", fileName.c_str()); - return ERR_CODE(FILE_ERR_NOEXIST); - } - - std::string line; - int result = SUCCESS; - - // 包含DataType字段,检查数据类型 - EVzResultDataType eDataType = keResultDataType_Invalid; - result = _GetLaserType(fileName, eDataType); - ERR_CODE_RETURN(result); - - LOG_INFO("Laser data type: %d \n", eDataType); - - SVzLaserLineData sLaserData; - memset(&sLaserData, 0, sizeof(SVzLaserLineData)); - - bool bFindLineNum = true; - int nLaserPointIdx = 0; - - while (std::getline(inputFile, line)) { - // 去除行末的 \r 字符(处理 Windows 格式的 CR LF 换行符) - TrimCarriageReturn(line); - - if (line.find("LineNum:") == 0) { - sscanf(line.c_str(), "LineNum:%d", &lineNum); - } else if (line.find("DataType:") == 0) { - - } else if (line.find("Line_") == 0) { - - if(false == bFindLineNum) { - laserLines.push_back(std::make_pair(eDataType, sLaserData)); - } - - int lineIndex; - unsigned int timeStamp; - int ptNum = 0; - sscanf(line.c_str(), "Line_%d_%u_%d", &lineIndex, &timeStamp, &ptNum); - - sLaserData.llFrameIdx = lineIndex; - sLaserData.llTimeStamp = timeStamp; - sLaserData.nPointCount = ptNum; - if (eDataType == keResultDataType_PointXYZRGBA) { - sLaserData.p3DPoint = new SVzNLPointXYZRGBA[ptNum]; - sLaserData.p2DPoint = new SVzNL2DLRPoint[ptNum]; - memset(sLaserData.p3DPoint, 0, sizeof(SVzNLPointXYZRGBA) * ptNum); - memset(sLaserData.p2DPoint, 0, sizeof(SVzNL2DLRPoint) * ptNum); + double x; + double y; + double value; +}; + +struct Dat3DPoint +{ + double X; + double Y; + double Z; +}; + +struct Dat3DFeaturePoint +{ + int index; + int flag; + DatValue2DFloat stereo; + DatValue2DFloat stereoRight; + Dat3DPoint eye; + Dat3DPoint world; + std::uint64_t next; +}; + +struct DatLaserScan3DLinesHeader +{ + char dataCalibrated; + char adjusted; + int scanLineNum; + int vr1; + int vr2; + double lineV; + int clock; + std::uint32_t maxTimeStamp; + std::uint64_t scanLineList; +}; + +struct DatLineScan3DData +{ + int lineIndex; + std::uint32_t timeStamp; + double lineStepping; +}; + +static_assert(sizeof(DatLaserScan3DLinesHeader) == 40, "Unexpected dat header layout"); +static_assert(sizeof(Dat3DFeaturePoint) == 112, "Unexpected dat feature point layout"); + +template +bool ReadBinary(std::ifstream& inputFile, T& value) +{ + inputFile.read(reinterpret_cast(&value), sizeof(T)); + return static_cast(inputFile); +} + +bool HasFileExtension(const std::string& fileName, const char* extension) +{ + const size_t dotPos = fileName.find_last_of('.'); + if (dotPos == std::string::npos) { + return false; + } + + const size_t slashPos = fileName.find_last_of("/\\"); + if (slashPos != std::string::npos && dotPos < slashPos) { + return false; + } + + std::string fileExt = fileName.substr(dotPos); + std::transform(fileExt.begin(), fileExt.end(), fileExt.begin(), + [](unsigned char c) { return static_cast(std::tolower(c)); }); + return fileExt == extension; +} + +bool LooksLikeTextLaserFile(const std::string& fileName) +{ + std::ifstream inputFile(fileName, std::ios::binary); + if (!inputFile.is_open()) { + return false; + } + + char header[8] = {}; + inputFile.read(header, sizeof(header)); + return inputFile.gcount() == sizeof(header) && std::memcmp(header, "LineNum:", sizeof(header)) == 0; +} + +bool ReadDatLineRecord(std::ifstream& inputFile, int recordSize, DatLineScan3DData& lineData) +{ + char buffer[40] = {}; + if (recordSize <= 0 || recordSize > static_cast(sizeof(buffer))) { + return false; + } + + inputFile.read(buffer, recordSize); + if (!inputFile) { + return false; + } + + std::memcpy(&lineData.lineIndex, buffer, sizeof(lineData.lineIndex)); + std::memcpy(&lineData.timeStamp, buffer + 4, sizeof(lineData.timeStamp)); + std::memcpy(&lineData.lineStepping, buffer + 8, sizeof(lineData.lineStepping)); + return true; +} + +void AppendFormattedLine(std::string& output, const char* format, ...) +{ + char stackBuffer[256]; + + va_list args; + va_start(args, format); + va_list argsCopy; + va_copy(argsCopy, args); + int written = vsnprintf(stackBuffer, sizeof(stackBuffer), format, args); + va_end(args); + + if (written < 0) { + va_end(argsCopy); + return; + } + + if (static_cast(written) < sizeof(stackBuffer)) { + output.append(stackBuffer, static_cast(written)); + va_end(argsCopy); + return; + } + + std::vector dynamicBuffer(static_cast(written) + 1); + vsnprintf(dynamicBuffer.data(), dynamicBuffer.size(), format, argsCopy); + va_end(argsCopy); + output.append(dynamicBuffer.data(), static_cast(written)); +} +} + +LaserDataLoader::LaserDataLoader() +{ + m_lastError.clear(); +} + +LaserDataLoader::~LaserDataLoader() +{ +} + +int LaserDataLoader::LoadLaserScanData(const std::string& fileName, + std::vector>& laserLines, + int& lineNum, + float& scanSpeed, + int& maxTimeStamp, + int& clockPerSecond) +{ + LOG_INFO("Loading from file: %s\n", fileName.c_str()); + + // 清空输出参数 + laserLines.clear(); + lineNum = 0; + scanSpeed = 0.0f; + maxTimeStamp = 0; + clockPerSecond = 0; + + if (HasFileExtension(fileName, ".dat") && !LooksLikeTextLaserFile(fileName)) { + return _LoadLaserScanDataFromDatFile(fileName, laserLines, lineNum, scanSpeed, maxTimeStamp, clockPerSecond); + } + + // 判断文件类型 + std::ifstream inputFile(fileName); + if (!inputFile.is_open()) { + m_lastError = "Cannot open file: " + fileName; + LOG_ERROR("Cannot open file: %s\n", fileName.c_str()); + return ERR_CODE(FILE_ERR_NOEXIST); + } + + std::string line; + int result = SUCCESS; + + // 包含DataType字段,检查数据类型 + EVzResultDataType eDataType = keResultDataType_Invalid; + result = _GetLaserType(fileName, eDataType); + ERR_CODE_RETURN(result); + + LOG_INFO("Laser data type: %d \n", eDataType); + + SVzLaserLineData sLaserData; + memset(&sLaserData, 0, sizeof(SVzLaserLineData)); + + bool bFindLineNum = true; + int nLaserPointIdx = 0; + + auto failTextLoad = [&](int code, const std::string& message) -> int { + m_lastError = message; + LOG_ERROR("%s\n", message.c_str()); + if (!bFindLineNum) { + laserLines.push_back(std::make_pair(eDataType, sLaserData)); + bFindLineNum = true; + } + FreeLaserScanData(laserLines); + lineNum = 0; + return ERR_CODE(code); + }; + + auto commitCurrentLine = [&]() -> int { + if (bFindLineNum) return SUCCESS; + // if (nLaserPointIdx != sLaserData.nPointCount) { + // std::ostringstream message; + // message << "Incomplete laser line: expected " << sLaserData.nPointCount + // << " points, got " << nLaserPointIdx; + // return failTextLoad(FILE_ERR_FORMAT, message.str()); + // } + laserLines.push_back(std::make_pair(eDataType, sLaserData)); + bFindLineNum = true; + return SUCCESS; + }; + + while (std::getline(inputFile, line)) { + // 去除行末的 \r 字符(处理 Windows 格式的 CR LF 换行符) + TrimCarriageReturn(line); + + if (line.find("LineNum:") == 0) { + if (1 != sscanf(line.c_str(), "LineNum:%d", &lineNum) || lineNum <= 0) { + return failTextLoad(FILE_ERR_FORMAT, "Invalid LineNum header: " + line); + } + } else if (line.find("DataType:") == 0) { + + } else if (line.find("Line_") == 0) { + + if(false == bFindLineNum) { + result = commitCurrentLine(); + if (SUCCESS != result) return result; + } + + int lineIndex = 0; + unsigned int timeStamp = 0; + int ptNum = 0; + if (3 != sscanf(line.c_str(), "Line_%d_%u_%d", &lineIndex, &timeStamp, &ptNum) || + ptNum < 0) { + return failTextLoad(FILE_ERR_FORMAT, "Invalid laser line header: " + line); + } + + sLaserData.llFrameIdx = lineIndex; + sLaserData.llTimeStamp = timeStamp; + sLaserData.nPointCount = ptNum; + if (eDataType == keResultDataType_PointXYZRGBA) { + sLaserData.p3DPoint = new SVzNLPointXYZRGBA[ptNum]; + sLaserData.p2DPoint = new SVzNL2DLRPoint[ptNum]; + memset(sLaserData.p3DPoint, 0, sizeof(SVzNLPointXYZRGBA) * ptNum); + memset(sLaserData.p2DPoint, 0, sizeof(SVzNL2DLRPoint) * ptNum); } else if(eDataType == keResultDataType_Position) { sLaserData.p3DPoint = new SVzNL3DPosition[ptNum]; sLaserData.p2DPoint = new SVzNL2DPosition[ptNum]; @@ -247,188 +278,203 @@ int LaserDataLoader::LoadLaserScanData(const std::string& fileName, } nLaserPointIdx = 0; bFindLineNum = false; - - } else if (line.find("Poly_") == 0) { - // 跳过 Poly 折线头及后续点坐标数据 - size_t lastUnderscore = line.rfind('_'); - int polyNum = 0; - if (lastUnderscore != std::string::npos && lastUnderscore + 1 < line.size()) { - polyNum = std::atoi(line.c_str() + lastUnderscore + 1); - } - for (int skip = 0; skip < polyNum; ) { - if (!std::getline(inputFile, line)) break; - TrimCarriageReturn(line); - if (!line.empty()) { - ++skip; - } - } - - } else if (line.find("{") == 0) { - // 使用正则表达式判断是XYZ还是RGBD格式 - // XYZ格式: {x,y,z}-{leftX,leftY}-{rightX,rightY} - // RGBD格式: {x,y,z,r,g,b}-{leftX,leftY}-{rightX,rightY} - - // 更精确的正则表达式,匹配完整的行格式 - if(sLaserData.p3DPoint == nullptr || sLaserData.p2DPoint == nullptr) { - LOG_ERROR("sLaserData.p3DPoint == nullptr || sLaserData.p2DPoint == nullptr \n"); - return ERR_CODE(DATA_ERR_INVALID); - } - - // 防止数组越界:跳过超出声明点数的数据行 - if (nLaserPointIdx >= sLaserData.nPointCount) { - LOG_WARN("nLaserPointIdx(%d) >= nPointCount(%d), skip\n", nLaserPointIdx, sLaserData.nPointCount); - continue; - } - - if (eDataType == keResultDataType_PointXYZRGBA) { - SVzNLPointXYZRGBA* pRGBAPoints = static_cast(sLaserData.p3DPoint); - SVzNL2DLRPoint* p2DPoints = static_cast(sLaserData.p2DPoint); - _ParseLaserScanPoint(line, pRGBAPoints[nLaserPointIdx], p2DPoints[nLaserPointIdx]); - nLaserPointIdx++; - } else if (eDataType == keResultDataType_Position) { + + } else if (line.find("Poly_") == 0) { + // 跳过 Poly 折线头及后续点坐标数据 + size_t lastUnderscore = line.rfind('_'); + int polyNum = 0; + if (lastUnderscore != std::string::npos && lastUnderscore + 1 < line.size()) { + polyNum = std::atoi(line.c_str() + lastUnderscore + 1); + } + for (int skip = 0; skip < polyNum; ) { + if (!std::getline(inputFile, line)) { + return failTextLoad(inputFile.bad() ? FILE_ERR_READ : FILE_ERR_FORMAT, + "Incomplete Poly block in laser file: " + fileName); + } + TrimCarriageReturn(line); + if (!line.empty()) { + ++skip; + } + } + + } else if (line.find("{") == 0) { + // 使用正则表达式判断是XYZ还是RGBD格式 + // XYZ格式: {x,y,z}-{leftX,leftY}-{rightX,rightY} + // RGBD格式: {x,y,z,r,g,b}-{leftX,leftY}-{rightX,rightY} + + // 更精确的正则表达式,匹配完整的行格式 + if(sLaserData.p3DPoint == nullptr || sLaserData.p2DPoint == nullptr) { + LOG_ERROR("sLaserData.p3DPoint == nullptr || sLaserData.p2DPoint == nullptr \n"); + return ERR_CODE(DATA_ERR_INVALID); + } + + // 防止数组越界:跳过超出声明点数的数据行 + if (nLaserPointIdx >= sLaserData.nPointCount) { + LOG_WARN("nLaserPointIdx(%d) >= nPointCount(%d), skip\n", nLaserPointIdx, sLaserData.nPointCount); + continue; + } + + if (eDataType == keResultDataType_PointXYZRGBA) { + SVzNLPointXYZRGBA* pRGBAPoints = static_cast(sLaserData.p3DPoint); + SVzNL2DLRPoint* p2DPoints = static_cast(sLaserData.p2DPoint); + result = _ParseLaserScanPoint(line, pRGBAPoints[nLaserPointIdx], p2DPoints[nLaserPointIdx]); + if (SUCCESS != result) { + return failTextLoad(FILE_ERR_FORMAT, "Invalid RGBD point: " + line); + } + nLaserPointIdx++; + } else { SVzNL3DPosition* p3DPoints = static_cast(sLaserData.p3DPoint); SVzNL2DPosition* p2DPoints = static_cast(sLaserData.p2DPoint); result = _ParseLaserScanPoint(line, p3DPoints[nLaserPointIdx], p2DPoints[nLaserPointIdx]); - if (result != SUCCESS) { - m_lastError = "Invalid XYZUV point line: " + line; - LOG_ERROR("%s\n", m_lastError.c_str()); - return result; + if (SUCCESS != result) { + return failTextLoad(FILE_ERR_FORMAT, "Invalid XYZ point: " + line); } p3DPoints[nLaserPointIdx].nPointIdx = nLaserPointIdx; nLaserPointIdx++; } } - } - - // 添加最后一条扫描线数据 - if (!bFindLineNum) { - laserLines.push_back(std::make_pair(eDataType, sLaserData)); - } - - inputFile.close(); - LOG_INFO("Successfully loaded %d laser scan lines from file: %s\n", lineNum, fileName.c_str()); - return SUCCESS; -} - -// Read binary .dat laser scan data. -int LaserDataLoader::_LoadLaserScanDataFromDatFile(const std::string& fileName, - std::vector>& laserLines, - int& lineNum, - float& scanSpeed, - int& maxTimeStamp, - int& clockPerSecond) -{ - std::ifstream inputFile(fileName, std::ios::binary); - if (!inputFile.is_open()) { - m_lastError = "Cannot open file: " + fileName; - LOG_ERROR("Cannot open dat file: %s\n", fileName.c_str()); - return ERR_CODE(FILE_ERR_NOEXIST); - } - - auto fail = [&](int code, const std::string& message) -> int { - m_lastError = message; - LOG_ERROR("%s\n", message.c_str()); - FreeLaserScanData(laserLines); - lineNum = 0; - scanSpeed = 0.0f; - maxTimeStamp = 0; - clockPerSecond = 0; - return ERR_CODE(code); - }; - - inputFile.seekg(0, std::ios::end); - const std::streamoff fileSize = inputFile.tellg(); - inputFile.seekg(0, std::ios::beg); - - DatLaserScan3DLinesHeader header; - std::memset(&header, 0, sizeof(header)); - if (!ReadBinary(inputFile, header)) { - return fail(FILE_ERR_READ, "Cannot read dat header: " + fileName); - } - - if (header.scanLineNum < 0 || fileSize < static_cast(sizeof(header))) { - return fail(FILE_ERR_FORMAT, "Invalid dat header: " + fileName); - } - - const int kFeaturePointSize = static_cast(sizeof(Dat3DFeaturePoint)); - auto validateLayout = [&](int lineRecordSize) -> bool { - std::streamoff pos = static_cast(sizeof(header)); - inputFile.clear(); - inputFile.seekg(pos, std::ios::beg); - - for (int i = 0; i < header.scanLineNum; ++i) { - if (pos + lineRecordSize + static_cast(sizeof(int)) > fileSize) { - return false; - } - - inputFile.seekg(lineRecordSize, std::ios::cur); - pos += lineRecordSize; - - int pointCount = 0; - if (!ReadBinary(inputFile, pointCount)) { - return false; - } - pos += static_cast(sizeof(pointCount)); - - if (pointCount < 0 || pointCount > VZ_LASER_LINE_PT_MAX_NUM) { - return false; - } - - const std::streamoff pointBytes = - static_cast(pointCount) * static_cast(kFeaturePointSize); - if (pointBytes < 0 || pos + pointBytes > fileSize) { - return false; - } - - inputFile.seekg(pointBytes, std::ios::cur); - pos += pointBytes; - } - - return pos == fileSize; - }; - - int lineRecordSize = 40; - if (!validateLayout(lineRecordSize)) { - lineRecordSize = 32; - if (!validateLayout(lineRecordSize)) { - return fail(FILE_ERR_FORMAT, "Invalid dat layout: " + fileName); - } - } - - lineNum = header.scanLineNum; - scanSpeed = static_cast(header.lineV); - maxTimeStamp = static_cast(header.maxTimeStamp); - clockPerSecond = header.clock; - - laserLines.clear(); - laserLines.reserve(static_cast(lineNum)); - - inputFile.clear(); - inputFile.seekg(static_cast(sizeof(header)), std::ios::beg); - - for (int i = 0; i < lineNum; ++i) { - DatLineScan3DData datLine; - std::memset(&datLine, 0, sizeof(datLine)); - if (!ReadDatLineRecord(inputFile, lineRecordSize, datLine)) { - return fail(FILE_ERR_READ, "Cannot read dat line header: " + fileName); - } - - int pointCount = 0; - if (!ReadBinary(inputFile, pointCount)) { - return fail(FILE_ERR_READ, "Cannot read dat point count: " + fileName); - } - if (pointCount < 0 || pointCount > VZ_LASER_LINE_PT_MAX_NUM) { - return fail(FILE_ERR_FORMAT, "Invalid dat point count: " + fileName); - } - - SVzLaserLineData lineData; - std::memset(&lineData, 0, sizeof(lineData)); - lineData.nPointCount = pointCount; - lineData.dTotleOffset = datLine.lineStepping; - lineData.dStep = datLine.lineStepping; - lineData.llFrameIdx = static_cast(datLine.lineIndex); - lineData.llTimeStamp = static_cast(datLine.timeStamp); - + } + + if (inputFile.bad()) { + return failTextLoad(FILE_ERR_READ, "Failed while reading laser file: " + fileName); + } + + // 添加最后一条扫描线数据 + if (!bFindLineNum) { + result = commitCurrentLine(); + if (SUCCESS != result) return result; + } + if (lineNum <= 0 || laserLines.size() != static_cast(lineNum)) { + std::ostringstream message; + message << "Laser line count mismatch: expected " << lineNum + << ", got " << laserLines.size(); + return failTextLoad(FILE_ERR_FORMAT, message.str()); + } + + inputFile.close(); + LOG_INFO("Successfully loaded %d laser scan lines from file: %s\n", lineNum, fileName.c_str()); + return SUCCESS; +} + +// Read binary .dat laser scan data. +int LaserDataLoader::_LoadLaserScanDataFromDatFile(const std::string& fileName, + std::vector>& laserLines, + int& lineNum, + float& scanSpeed, + int& maxTimeStamp, + int& clockPerSecond) +{ + std::ifstream inputFile(fileName, std::ios::binary); + if (!inputFile.is_open()) { + m_lastError = "Cannot open file: " + fileName; + LOG_ERROR("Cannot open dat file: %s\n", fileName.c_str()); + return ERR_CODE(FILE_ERR_NOEXIST); + } + + auto fail = [&](int code, const std::string& message) -> int { + m_lastError = message; + LOG_ERROR("%s\n", message.c_str()); + FreeLaserScanData(laserLines); + lineNum = 0; + scanSpeed = 0.0f; + maxTimeStamp = 0; + clockPerSecond = 0; + return ERR_CODE(code); + }; + + inputFile.seekg(0, std::ios::end); + const std::streamoff fileSize = inputFile.tellg(); + inputFile.seekg(0, std::ios::beg); + + DatLaserScan3DLinesHeader header; + std::memset(&header, 0, sizeof(header)); + if (!ReadBinary(inputFile, header)) { + return fail(FILE_ERR_READ, "Cannot read dat header: " + fileName); + } + + if (header.scanLineNum < 0 || fileSize < static_cast(sizeof(header))) { + return fail(FILE_ERR_FORMAT, "Invalid dat header: " + fileName); + } + + const int kFeaturePointSize = static_cast(sizeof(Dat3DFeaturePoint)); + auto validateLayout = [&](int lineRecordSize) -> bool { + std::streamoff pos = static_cast(sizeof(header)); + inputFile.clear(); + inputFile.seekg(pos, std::ios::beg); + + for (int i = 0; i < header.scanLineNum; ++i) { + if (pos + lineRecordSize + static_cast(sizeof(int)) > fileSize) { + return false; + } + + inputFile.seekg(lineRecordSize, std::ios::cur); + pos += lineRecordSize; + + int pointCount = 0; + if (!ReadBinary(inputFile, pointCount)) { + return false; + } + pos += static_cast(sizeof(pointCount)); + + if (pointCount < 0 || pointCount > VZ_LASER_LINE_PT_MAX_NUM) { + return false; + } + + const std::streamoff pointBytes = + static_cast(pointCount) * static_cast(kFeaturePointSize); + if (pointBytes < 0 || pos + pointBytes > fileSize) { + return false; + } + + inputFile.seekg(pointBytes, std::ios::cur); + pos += pointBytes; + } + + return pos == fileSize; + }; + + int lineRecordSize = 40; + if (!validateLayout(lineRecordSize)) { + lineRecordSize = 32; + if (!validateLayout(lineRecordSize)) { + return fail(FILE_ERR_FORMAT, "Invalid dat layout: " + fileName); + } + } + + lineNum = header.scanLineNum; + scanSpeed = static_cast(header.lineV); + maxTimeStamp = static_cast(header.maxTimeStamp); + clockPerSecond = header.clock; + + laserLines.clear(); + laserLines.reserve(static_cast(lineNum)); + + inputFile.clear(); + inputFile.seekg(static_cast(sizeof(header)), std::ios::beg); + + for (int i = 0; i < lineNum; ++i) { + DatLineScan3DData datLine; + std::memset(&datLine, 0, sizeof(datLine)); + if (!ReadDatLineRecord(inputFile, lineRecordSize, datLine)) { + return fail(FILE_ERR_READ, "Cannot read dat line header: " + fileName); + } + + int pointCount = 0; + if (!ReadBinary(inputFile, pointCount)) { + return fail(FILE_ERR_READ, "Cannot read dat point count: " + fileName); + } + if (pointCount < 0 || pointCount > VZ_LASER_LINE_PT_MAX_NUM) { + return fail(FILE_ERR_FORMAT, "Invalid dat point count: " + fileName); + } + + SVzLaserLineData lineData; + std::memset(&lineData, 0, sizeof(lineData)); + lineData.nPointCount = pointCount; + lineData.dTotleOffset = datLine.lineStepping; + lineData.dStep = datLine.lineStepping; + lineData.llFrameIdx = static_cast(datLine.lineIndex); + lineData.llTimeStamp = static_cast(datLine.timeStamp); + std::unique_ptr points3D; std::unique_ptr points2D; @@ -441,98 +487,98 @@ int LaserDataLoader::_LoadLaserScanDataFromDatFile(const std::string& fileName, std::memset(points3D.get(), 0, sizeof(SVzNL3DPosition) * static_cast(pointCount)); std::memset(points2D.get(), 0, sizeof(SVzNL2DPosition) * static_cast(pointCount)); - - for (int j = 0; j < pointCount; ++j) { - Dat3DFeaturePoint featurePoint; - std::memset(&featurePoint, 0, sizeof(featurePoint)); - if (!ReadBinary(inputFile, featurePoint)) { - return fail(FILE_ERR_READ, "Cannot read dat feature point: " + fileName); - } - - points3D[j].nPointIdx = j; - points3D[j].pt3D.x = featurePoint.eye.X; - points3D[j].pt3D.y = featurePoint.eye.Y; - points3D[j].pt3D.z = featurePoint.eye.Z; - + + for (int j = 0; j < pointCount; ++j) { + Dat3DFeaturePoint featurePoint; + std::memset(&featurePoint, 0, sizeof(featurePoint)); + if (!ReadBinary(inputFile, featurePoint)) { + return fail(FILE_ERR_READ, "Cannot read dat feature point: " + fileName); + } + + points3D[j].nPointIdx = j; + points3D[j].pt3D.x = featurePoint.eye.X; + points3D[j].pt3D.y = featurePoint.eye.Y; + points3D[j].pt3D.z = featurePoint.eye.Z; + points2D[j].nPointIdx = j; points2D[j].ptLeft2D.x = static_cast(featurePoint.stereo.x); points2D[j].ptLeft2D.y = static_cast(featurePoint.stereo.y); points2D[j].ptRight2D.x = static_cast(featurePoint.stereoRight.x); points2D[j].ptRight2D.y = static_cast(featurePoint.stereoRight.y); - } - - lineData.p3DPoint = points3D.get(); - lineData.p2DPoint = points2D.get(); - } - + } + + lineData.p3DPoint = points3D.get(); + lineData.p2DPoint = points2D.get(); + } + laserLines.push_back(std::make_pair(keResultDataType_Position, lineData)); - points3D.release(); - points2D.release(); - } - - LOG_INFO("Successfully loaded %d dat laser scan lines from file: %s\n", lineNum, fileName.c_str()); - return SUCCESS; -} - -// 保存激光扫描数据到文件 - 统一接口,支持两种类型的数据 -int LaserDataLoader::SaveLaserScanData(const std::string& fileName, - const std::vector>& laserLines, - int lineNum, - float scanSpeed, - int maxTimeStamp, - int clockPerSecond) -{ - LOG_INFO("Saving to file: %s\n", fileName.c_str()); - - if (laserLines.empty() || lineNum <= 0) { - m_lastError = "Invalid input parameters for saving unified data"; - LOG_ERROR("Invalid parameters for saving unified laser data\n"); - return ERR_CODE(DEV_ARG_INVAILD); - } - - try { - std::ofstream sw(fileName); - if (!sw.is_open()) { - m_lastError = "Cannot open file for writing: " + fileName; - LOG_ERROR("Cannot open file for writing: %s\n", fileName.c_str()); - return ERR_CODE(FILE_ERR_WRITE); - } - + points3D.release(); + points2D.release(); + } + + LOG_INFO("Successfully loaded %d dat laser scan lines from file: %s\n", lineNum, fileName.c_str()); + return SUCCESS; +} + +// 保存激光扫描数据到文件 - 统一接口,支持两种类型的数据 +int LaserDataLoader::SaveLaserScanData(const std::string& fileName, + const std::vector>& laserLines, + int lineNum, + float scanSpeed, + int maxTimeStamp, + int clockPerSecond) +{ + LOG_INFO("Saving to file: %s\n", fileName.c_str()); + + if (laserLines.empty() || lineNum <= 0) { + m_lastError = "Invalid input parameters for saving unified data"; + LOG_ERROR("Invalid parameters for saving unified laser data\n"); + return ERR_CODE(DEV_ARG_INVAILD); + } + + try { + std::ofstream sw(fileName); + if (!sw.is_open()) { + m_lastError = "Cannot open file for writing: " + fileName; + LOG_ERROR("Cannot open file for writing: %s\n", fileName.c_str()); + return ERR_CODE(FILE_ERR_WRITE); + } + // 写入文件头 sw << "LineNum:" << lineNum << '\n'; - sw << "DataType: 0" << '\n'; + sw << "DataType:" << static_cast(fileDataType) << '\n'; sw << "ScanSpeed:" << scanSpeed << '\n'; sw << "PointAdjust: 1" << '\n'; sw << "MaxTimeStamp:" << maxTimeStamp << "_" << clockPerSecond << '\n'; - - // 写入每条扫描线数据 - for (const auto& linePair : laserLines) { - - EVzResultDataType dataType = linePair.first; - const SVzLaserLineData& lineData = linePair.second; - - sw << "Line_" << lineData.llFrameIdx << "_" << lineData.llTimeStamp << "_" << lineData.nPointCount << '\n'; - - // 根据数据类型写入点云数据 + + // 写入每条扫描线数据 + for (const auto& linePair : laserLines) { + + EVzResultDataType dataType = linePair.first; + const SVzLaserLineData& lineData = linePair.second; + + sw << "Line_" << lineData.llFrameIdx << "_" << lineData.llTimeStamp << "_" << lineData.nPointCount << '\n'; + + // 根据数据类型写入点云数据 if (dataType == keResultDataType_Position && lineData.p3DPoint) { // 写入XYZ格式数据 const SVzNL3DPosition* points = static_cast(lineData.p3DPoint); const SVzNL2DPosition* points2D = static_cast(lineData.p2DPoint); - constexpr int BATCH = 256; - constexpr int PER_POINT = 128; - for (int base = 0; base < lineData.nPointCount; base += BATCH) { - int end = (base + BATCH < lineData.nPointCount) ? (base + BATCH) : lineData.nPointCount; - std::string buffer; - buffer.reserve(BATCH * PER_POINT); - for (int i = base; i < end; ++i) { - float x = static_cast(points[i].pt3D.x); - float y = static_cast(points[i].pt3D.y); - float z = static_cast(points[i].pt3D.z); - AppendFormattedLine(buffer, "{ %.6f,%.6f,%.6f }-{ %d,%d }-{ %d,%d }\n", - x, y, z, - points2D[i].ptLeft2D.x, points2D[i].ptLeft2D.y, - points2D[i].ptRight2D.x, points2D[i].ptRight2D.y); - } + constexpr int BATCH = 256; + constexpr int PER_POINT = 128; + for (int base = 0; base < lineData.nPointCount; base += BATCH) { + int end = (base + BATCH < lineData.nPointCount) ? (base + BATCH) : lineData.nPointCount; + std::string buffer; + buffer.reserve(BATCH * PER_POINT); + for (int i = base; i < end; ++i) { + float x = static_cast(points[i].pt3D.x); + float y = static_cast(points[i].pt3D.y); + float z = static_cast(points[i].pt3D.z); + AppendFormattedLine(buffer, "{ %.6f,%.6f,%.6f }-{ %d,%d }-{ %d,%d }\n", + x, y, z, + points2D[i].ptLeft2D.x, points2D[i].ptLeft2D.y, + points2D[i].ptRight2D.x, points2D[i].ptRight2D.y); + } sw.write(buffer.data(), static_cast(buffer.size())); } } else if (dataType == keResultDataType_PositionF && lineData.p3DPoint) { @@ -556,290 +602,317 @@ int LaserDataLoader::SaveLaserScanData(const std::string& fileName, sw.write(buffer.data(), static_cast(buffer.size())); } } else if (dataType == keResultDataType_PointXYZI && lineData.p3DPoint) { - // LiDAR 点云:snprintf 批量写入,每 256 点 flush - // 坐标 mm 范围可达 ±200000 → "%.6f" 最长 14 字符 → 整行 ≤73 字符 - const SVzNLPointXYZI* points = static_cast(lineData.p3DPoint); - const int n = lineData.nPointCount; - constexpr int BATCH = 256; - constexpr int PER_POINT = 80; - char buf[BATCH * PER_POINT]; - for (int base = 0; base < n; base += BATCH) - { - int end = (base + BATCH < n) ? (base + BATCH) : n; - char* p = buf; - for (int i = base; i < end; ++i) - { - p += snprintf(p, PER_POINT, "{ %.6f, %.6f, %.6f } - { 0, 0 } - { 0, 0 }\n", - points[i].fData[0], points[i].fData[1], points[i].fData[2]); - } - sw.write(buf, p - buf); - } - } else if (dataType == keResultDataType_PointXYZRGBA && lineData.p3DPoint) { - // 写入RGBD格式数据 - const SVzNLPointXYZRGBA* points = static_cast(lineData.p3DPoint); - const SVzNL2DLRPoint* points2D = static_cast(lineData.p2DPoint); - constexpr int BATCH = 256; - constexpr int PER_POINT = 160; - for (int base = 0; base < lineData.nPointCount; base += BATCH) { - int end = (base + BATCH < lineData.nPointCount) ? (base + BATCH) : lineData.nPointCount; - std::string buffer; - buffer.reserve(BATCH * PER_POINT); - for (int i = base; i < end; ++i) { - float x = static_cast(points[i].x); - float y = static_cast(points[i].y); - float z = static_cast(points[i].z); - int r = (points[i].nRGB >> 16) & 0xFF; - int g = (points[i].nRGB >> 8) & 0xFF; - int b = points[i].nRGB & 0xFF; - - AppendFormattedLine(buffer, "{%.6f,%.6f,%.6f,%.6f,%.6f,%.6f}-{ %d,%d }-{ %d,%d }\n", - x, y, z, - b * 1.0f / 255, g * 1.0f / 255, r * 1.0f / 255, - points2D[i].sLeft.x, points2D[i].sLeft.y, - points2D[i].sRight.x, points2D[i].sRight.y); - } - sw.write(buffer.data(), static_cast(buffer.size())); - } - } - } - - sw.close(); - return SUCCESS; - - } catch (const std::exception& e) { - m_lastError = "Error saving unified file: " + std::string(e.what()); - LOG_ERROR("Error saving unified laser data to file: %s\n", e.what()); - return ERR_CODE(FILE_ERR_WRITE); - } -} - -int LaserDataLoader::DebugSaveLaser(std::string fileName, std::vector > xyzData) -{ - return DebugSaveLaser(std::move(fileName), std::move(xyzData), nullptr); -} - -int LaserDataLoader::DebugSaveLaser(std::string fileName, std::vector> xyzData, - ISaveProgressCallback* callback) -{ - LOG_INFO("Saving unified laser scan data to file: %s\n", fileName.c_str()); - - if (xyzData.empty()) { - m_lastError = "Invalid input parameters for saving unified data"; - LOG_ERROR("Invalid parameters for saving unified laser data\n"); - return ERR_CODE(DEV_ARG_INVAILD); - } - - try { - std::ofstream sw(fileName); - if (!sw.is_open()) { - m_lastError = "Cannot open file for writing: " + fileName; - LOG_ERROR("Cannot open file for writing: %s\n", fileName.c_str()); - return ERR_CODE(FILE_ERR_WRITE); - } - - // 写入文件头 - sw << "LineNum:" << xyzData.size() << '\n'; - sw << "DataType: 0" << '\n'; - sw << "ScanSpeed:" << 0 << '\n'; - sw << "PointAdjust: 1" << '\n'; - sw << "MaxTimeStamp:" << 0 << "_" << 0 << '\n'; - - const size_t totalLines = xyzData.size(); - int index = 0; - // 写入每条扫描线数据 - for (const auto& linePair : xyzData) { - if (callback && callback->isSaveCancelled()) { - sw.close(); - m_lastError = "Save cancelled by user"; - LOG_INFO("Save cancelled by user at line %d/%zu\n", index, totalLines); - return ERR_CODE(DEV_ARG_INVAILD); - } - - sw << "Line_" << index << "_" << 0 << "_" << linePair.size() << '\n'; - // 根据数据类型写入点云数据 - constexpr int BATCH = 256; - constexpr int PER_POINT = 96; - for (size_t base = 0; base < linePair.size(); base += BATCH) { - size_t end = std::min(base + static_cast(BATCH), linePair.size()); - std::string buffer; - buffer.reserve(BATCH * PER_POINT); - for (size_t i = base; i < end; ++i) { - const auto& point = linePair[i]; - - // 写入XYZ格式数据 - float x = static_cast(point.pt3D.x); - float y = static_cast(point.pt3D.y); - float z = static_cast(point.pt3D.z); - AppendFormattedLine(buffer, "{ %.6f, %.6f, %.6f } - { 0, 0 } - { 0, 0 }\n", - x, y, z); - } - sw.write(buffer.data(), static_cast(buffer.size())); - } - - ++index; - if (callback) { - callback->onSaveProgress(static_cast(index) / static_cast(totalLines)); - } - } - - sw.close(); - return SUCCESS; - - } catch (const std::exception& e) { - m_lastError = "Error saving unified file: " + std::string(e.what()); - LOG_ERROR("Error saving unified laser data to file: %s\n", e.what()); - return ERR_CODE(FILE_ERR_WRITE); - } -} - -int LaserDataLoader::LoadPolySegments(const std::string& fileName, - std::vector>& polyLines) -{ - LOG_INFO("Loading poly segments from file: %s\n", fileName.c_str()); - - polyLines.clear(); - - std::ifstream inputFile(fileName); - if (!inputFile.is_open()) { - m_lastError = "Cannot open file: " + fileName; - LOG_ERROR("Cannot open file: %s\n", fileName.c_str()); - return ERR_CODE(FILE_ERR_NOEXIST); - } - - std::string line; - - while (std::getline(inputFile, line)) { - TrimCarriageReturn(line); - - // 跳过空行和注释 - if (line.empty() || line[0] == '#') { - continue; - } - - // 查找 Poly_index_num 头 - if (line.find("Poly_") != 0) { - continue; - } - - // 解析点数量(最后一个下划线后的数字) - size_t lastUnderscore = line.rfind('_'); - if (lastUnderscore == std::string::npos || lastUnderscore + 1 >= line.size()) { - LOG_WARN("[LaserDataLoader] Invalid Poly header: %s\n", line.c_str()); - continue; - } - - int num = std::atoi(line.c_str() + lastUnderscore + 1); - if (num < 2) { - LOG_WARN("[LaserDataLoader] Invalid point count in Poly header: %s\n", line.c_str()); - continue; - } - - // 读取 num 个点坐标 - std::vector points; - for (int i = 0; i < num; ) { - if (!std::getline(inputFile, line)) break; - TrimCarriageReturn(line); - if (line.empty()) { - continue; // 跳过空行,不计数 - } - - SVzNLPointXYZRGBA pt; - memset(&pt, 0, sizeof(pt)); - pt.nRGB = 0x00FFFFFF; // 默认白色 - bool parsed = false; - - // 尝试花括号格式 - if (line.find('{') != std::string::npos) { - float fx, fy, fz; - int R = 255, G = 255, B = 255, A = 255; - - // 新RGBD格式: {x,y,z}-{lx,ly}-{rx,ry}-{R,G,B,A} - int cnt = sscanf(line.c_str(), - " { %f , %f , %f } - { %*f , %*f } - { %*f , %*f } - { %d , %d , %d , %d }", - &fx, &fy, &fz, &R, &G, &B, &A); - if (cnt >= 3) { - pt.x = fx; - pt.y = fy; - pt.z = fz; - if (cnt >= 7) { - // 解析到了颜色 R,G,B,A(0-255),Alpha 存储在高 8 位 - unsigned int nRGB = static_cast(A) << 24; - nRGB |= static_cast(B) << 16; - nRGB |= static_cast(G) << 8; - nRGB |= static_cast(R); - pt.nRGB = nRGB; - } - parsed = true; - } - - // 旧RGBD格式: {x,y,z,r,g,b}-{lx,ly}-{rx,ry} - if (!parsed) { - float r, g, b; - if (sscanf(line.c_str(), " { %f , %f , %f , %f , %f , %f }", - &fx, &fy, &fz, &r, &g, &b) == 6) { - pt.x = fx; - pt.y = fy; - pt.z = fz; - int nRGB = (int)(b * 255); nRGB <<= 8; - nRGB += (int)(g * 255); nRGB <<= 8; - nRGB += (int)(r * 255); - pt.nRGB = nRGB; - parsed = true; - } - } - - // XYZ格式: {x,y,z}-{...}-{...} - if (!parsed) { - if (sscanf(line.c_str(), " { %f , %f , %f }", &fx, &fy, &fz) == 3) { - pt.x = fx; - pt.y = fy; - pt.z = fz; - parsed = true; - } - } - } - - // 尝试纯数字格式: x y z(空格/Tab/逗号分隔) - if (!parsed) { - float fx, fy, fz; - std::string lineCopy = line; - for (char& c : lineCopy) { - if (c == ',' || c == '\t') c = ' '; - } - if (sscanf(lineCopy.c_str(), "%f %f %f", &fx, &fy, &fz) == 3) { - pt.x = fx; - pt.y = fy; - pt.z = fz; - parsed = true; - } - } - - if (parsed) { - points.push_back(pt); - } - ++i; - } - - if (points.size() >= 2) { - polyLines.push_back(std::move(points)); - } - } - - inputFile.close(); - LOG_INFO("Loaded %zu poly lines from file: %s\n", polyLines.size(), fileName.c_str()); - return SUCCESS; -} - -void LaserDataLoader::FreeLaserScanData(std::vector>& laserLines) -{ - LOG_DEBUG("Freeing unified laser scan data, line count: %zu\n", laserLines.size()); - - if (!laserLines.empty()) { - for (auto& linePair : laserLines) { - EVzResultDataType dataType = linePair.first; - SVzLaserLineData& lineData = linePair.second; - - if (lineData.p3DPoint) { + // LiDAR 点云:snprintf 批量写入,每 256 点 flush + // 坐标 mm 范围可达 ±200000 → "%.6f" 最长 14 字符 → 整行 ≤73 字符 + const SVzNLPointXYZI* points = static_cast(lineData.p3DPoint); + const int n = lineData.nPointCount; + constexpr int BATCH = 256; + constexpr int PER_POINT = 80; + char buf[BATCH * PER_POINT]; + for (int base = 0; base < n; base += BATCH) + { + int end = (base + BATCH < n) ? (base + BATCH) : n; + char* p = buf; + for (int i = base; i < end; ++i) + { + p += snprintf(p, PER_POINT, "{ %.6f, %.6f, %.6f } - { 0, 0 } - { 0, 0 }\n", + points[i].fData[0], points[i].fData[1], points[i].fData[2]); + } + sw.write(buf, p - buf); + } + } else if (dataType == keResultDataType_PointXYZRGBA && lineData.p3DPoint) { + // 写入RGBD格式数据 + const SVzNLPointXYZRGBA* points = static_cast(lineData.p3DPoint); + const SVzNL2DLRPoint* points2D = static_cast(lineData.p2DPoint); + constexpr int BATCH = 256; + constexpr int PER_POINT = 160; + for (int base = 0; base < lineData.nPointCount; base += BATCH) { + int end = (base + BATCH < lineData.nPointCount) ? (base + BATCH) : lineData.nPointCount; + std::string buffer; + buffer.reserve(BATCH * PER_POINT); + for (int i = base; i < end; ++i) { + float x = static_cast(points[i].x); + float y = static_cast(points[i].y); + float z = static_cast(points[i].z); + int r = (points[i].nRGB >> 16) & 0xFF; + int g = (points[i].nRGB >> 8) & 0xFF; + int b = points[i].nRGB & 0xFF; + + AppendFormattedLine(buffer, "{%.6f,%.6f,%.6f,%.6f,%.6f,%.6f}-{ %d,%d }-{ %d,%d }\n", + x, y, z, + b * 1.0f / 255, g * 1.0f / 255, r * 1.0f / 255, + points2D[i].sLeft.x, points2D[i].sLeft.y, + points2D[i].sRight.x, points2D[i].sRight.y); + } + sw.write(buffer.data(), static_cast(buffer.size())); + } + } + } + + sw.flush(); + if (!sw.good()) { + m_lastError = "Failed while writing file: " + fileName; + LOG_ERROR("Failed while writing file: %s\n", fileName.c_str()); + return ERR_CODE(FILE_ERR_WRITE); + } + sw.close(); + return SUCCESS; + + } catch (const std::exception& e) { + m_lastError = "Error saving unified file: " + std::string(e.what()); + LOG_ERROR("Error saving unified laser data to file: %s\n", e.what()); + return ERR_CODE(FILE_ERR_WRITE); + } +} + +int LaserDataLoader::DebugSaveLaser(std::string fileName, std::vector > xyzData) +{ + return DebugSaveLaser(std::move(fileName), std::move(xyzData), nullptr); +} + +int LaserDataLoader::DebugSaveLaser(std::string fileName, std::vector> xyzData, + ISaveProgressCallback* callback) +{ + LOG_INFO("Saving unified laser scan data to file: %s\n", fileName.c_str()); + + if (xyzData.empty()) { + m_lastError = "Invalid input parameters for saving unified data"; + LOG_ERROR("Invalid parameters for saving unified laser data\n"); + return ERR_CODE(DEV_ARG_INVAILD); + } + + try { + std::ofstream sw(fileName); + if (!sw.is_open()) { + m_lastError = "Cannot open file for writing: " + fileName; + LOG_ERROR("Cannot open file for writing: %s\n", fileName.c_str()); + return ERR_CODE(FILE_ERR_WRITE); + } + + // 写入文件头 + sw << "LineNum:" << xyzData.size() << '\n'; + sw << "DataType: 0" << '\n'; + sw << "ScanSpeed:" << 0 << '\n'; + sw << "PointAdjust: 1" << '\n'; + sw << "MaxTimeStamp:" << 0 << "_" << 0 << '\n'; + + const size_t totalLines = xyzData.size(); + int index = 0; + // 写入每条扫描线数据 + for (const auto& linePair : xyzData) { + if (callback && callback->isSaveCancelled()) { + sw.close(); + m_lastError = "Save cancelled by user"; + LOG_INFO("Save cancelled by user at line %d/%zu\n", index, totalLines); + return ERR_CODE(DEV_ARG_INVAILD); + } + + sw << "Line_" << index << "_" << 0 << "_" << linePair.size() << '\n'; + // 根据数据类型写入点云数据 + constexpr int BATCH = 256; + constexpr int PER_POINT = 96; + for (size_t base = 0; base < linePair.size(); base += BATCH) { + size_t end = std::min(base + static_cast(BATCH), linePair.size()); + std::string buffer; + buffer.reserve(BATCH * PER_POINT); + for (size_t i = base; i < end; ++i) { + const auto& point = linePair[i]; + + // 写入XYZ格式数据 + float x = static_cast(point.pt3D.x); + float y = static_cast(point.pt3D.y); + float z = static_cast(point.pt3D.z); + AppendFormattedLine(buffer, "{ %.6f, %.6f, %.6f } - { 0, 0 } - { 0, 0 }\n", + x, y, z); + } + sw.write(buffer.data(), static_cast(buffer.size())); + } + + ++index; + if (callback) { + callback->onSaveProgress(static_cast(index) / static_cast(totalLines)); + } + } + + sw.flush(); + if (!sw.good()) { + m_lastError = "Failed while writing file: " + fileName; + LOG_ERROR("Failed while writing file: %s\n", fileName.c_str()); + return ERR_CODE(FILE_ERR_WRITE); + } + sw.close(); + return SUCCESS; + + } catch (const std::exception& e) { + m_lastError = "Error saving unified file: " + std::string(e.what()); + LOG_ERROR("Error saving unified laser data to file: %s\n", e.what()); + return ERR_CODE(FILE_ERR_WRITE); + } +} + +int LaserDataLoader::LoadPolySegments(const std::string& fileName, + std::vector>& polyLines) +{ + LOG_INFO("Loading poly segments from file: %s\n", fileName.c_str()); + + polyLines.clear(); + + std::ifstream inputFile(fileName); + if (!inputFile.is_open()) { + m_lastError = "Cannot open file: " + fileName; + LOG_ERROR("Cannot open file: %s\n", fileName.c_str()); + return ERR_CODE(FILE_ERR_NOEXIST); + } + + std::string line; + + while (std::getline(inputFile, line)) { + TrimCarriageReturn(line); + + // 跳过空行和注释 + if (line.empty() || line[0] == '#') { + continue; + } + + // 查找 Poly_index_num 头 + if (line.find("Poly_") != 0) { + continue; + } + + // 解析点数量(最后一个下划线后的数字) + size_t lastUnderscore = line.rfind('_'); + if (lastUnderscore == std::string::npos || lastUnderscore + 1 >= line.size()) { + LOG_WARN("[LaserDataLoader] Invalid Poly header: %s\n", line.c_str()); + continue; + } + + int num = std::atoi(line.c_str() + lastUnderscore + 1); + if (num < 2) { + LOG_WARN("[LaserDataLoader] Invalid point count in Poly header: %s\n", line.c_str()); + continue; + } + + // 读取 num 个点坐标 + std::vector points; + bool incompleteBlock = false; + for (int i = 0; i < num; ) { + if (!std::getline(inputFile, line)) { + incompleteBlock = true; + break; + } + TrimCarriageReturn(line); + if (line.empty()) { + continue; // 跳过空行,不计数 + } + + SVzNLPointXYZRGBA pt; + memset(&pt, 0, sizeof(pt)); + pt.nRGB = 0x00FFFFFF; // 默认白色 + bool parsed = false; + + // 尝试花括号格式 + if (line.find('{') != std::string::npos) { + float fx, fy, fz; + int R = 255, G = 255, B = 255, A = 255; + + // 新RGBD格式: {x,y,z}-{lx,ly}-{rx,ry}-{R,G,B,A} + int cnt = sscanf(line.c_str(), + " { %f , %f , %f } - { %*f , %*f } - { %*f , %*f } - { %d , %d , %d , %d }", + &fx, &fy, &fz, &R, &G, &B, &A); + if (cnt >= 3) { + pt.x = fx; + pt.y = fy; + pt.z = fz; + if (cnt >= 7) { + // 解析到了颜色 R,G,B,A(0-255),Alpha 存储在高 8 位 + unsigned int nRGB = static_cast(A) << 24; + nRGB |= static_cast(B) << 16; + nRGB |= static_cast(G) << 8; + nRGB |= static_cast(R); + pt.nRGB = nRGB; + } + parsed = true; + } + + // 旧RGBD格式: {x,y,z,r,g,b}-{lx,ly}-{rx,ry} + if (!parsed) { + float r, g, b; + if (sscanf(line.c_str(), " { %f , %f , %f , %f , %f , %f }", + &fx, &fy, &fz, &r, &g, &b) == 6) { + pt.x = fx; + pt.y = fy; + pt.z = fz; + int nRGB = (int)(b * 255); nRGB <<= 8; + nRGB += (int)(g * 255); nRGB <<= 8; + nRGB += (int)(r * 255); + pt.nRGB = nRGB; + parsed = true; + } + } + + // XYZ格式: {x,y,z}-{...}-{...} + if (!parsed) { + if (sscanf(line.c_str(), " { %f , %f , %f }", &fx, &fy, &fz) == 3) { + pt.x = fx; + pt.y = fy; + pt.z = fz; + parsed = true; + } + } + } + + // 尝试纯数字格式: x y z(空格/Tab/逗号分隔) + if (!parsed) { + float fx, fy, fz; + std::string lineCopy = line; + for (char& c : lineCopy) { + if (c == ',' || c == '\t') c = ' '; + } + if (sscanf(lineCopy.c_str(), "%f %f %f", &fx, &fy, &fz) == 3) { + pt.x = fx; + pt.y = fy; + pt.z = fz; + parsed = true; + } + } + + if (parsed) { + points.push_back(pt); + } + ++i; + } + + if (incompleteBlock || points.size() != static_cast(num)) { + m_lastError = "Incomplete or invalid Poly block in file: " + fileName; + LOG_ERROR("%s\n", m_lastError.c_str()); + polyLines.clear(); + return ERR_CODE(inputFile.bad() ? FILE_ERR_READ : FILE_ERR_FORMAT); + } + polyLines.push_back(std::move(points)); + } + + if (inputFile.bad()) { + m_lastError = "Failed while reading Poly file: " + fileName; + LOG_ERROR("%s\n", m_lastError.c_str()); + polyLines.clear(); + return ERR_CODE(FILE_ERR_READ); + } + + inputFile.close(); + LOG_INFO("Loaded %zu poly lines from file: %s\n", polyLines.size(), fileName.c_str()); + return SUCCESS; +} + +void LaserDataLoader::FreeLaserScanData(std::vector>& laserLines) +{ + LOG_DEBUG("Freeing unified laser scan data, line count: %zu\n", laserLines.size()); + + if (!laserLines.empty()) { + for (auto& linePair : laserLines) { + EVzResultDataType dataType = linePair.first; + SVzLaserLineData& lineData = linePair.second; + + if (lineData.p3DPoint) { switch (dataType) { case keResultDataType_Position: delete[] static_cast(lineData.p3DPoint); @@ -847,98 +920,98 @@ void LaserDataLoader::FreeLaserScanData(std::vector(lineData.p3DPoint); break; - case keResultDataType_PointF: - delete[] static_cast(lineData.p3DPoint); - break; - case keResultDataType_PointXYZ: - delete[] static_cast(lineData.p3DPoint); - break; - case keResultDataType_PointXYZRGBA: - delete[] static_cast(lineData.p3DPoint); - break; - case keResultDataType_PointGray: - delete[] static_cast(lineData.p3DPoint); - break; - case keResultDataType_PointXYZI: - delete[] static_cast(lineData.p3DPoint); - break; - case keResultDataType_PointRGBA_D: - delete[] static_cast(lineData.p3DPoint); - break; - default: - LOG_WARN("Unknown laser data type when freeing p3DPoint: %d\n", dataType); - break; - } - lineData.p3DPoint = nullptr; - } - - if (lineData.p2DPoint) { - switch (dataType) { - case keResultDataType_Position: - delete[] static_cast(lineData.p2DPoint); - break; - case keResultDataType_PositionF: - delete[] static_cast(lineData.p2DPoint); - break; - case keResultDataType_PointF: - case keResultDataType_PointXYZ: - case keResultDataType_PointXYZRGBA: - case keResultDataType_PointGray: - case keResultDataType_PointXYZI: - case keResultDataType_PointRGBA_D: - delete[] static_cast(lineData.p2DPoint); - break; - default: - LOG_WARN("Unknown laser data type when freeing p2DPoint: %d\n", dataType); - break; - } - lineData.p2DPoint = nullptr; - } - } - laserLines.clear(); - } - - LOG_DEBUG("Unified laser scan data freed successfully\n"); -} - -// 转换统一格式数据为std::vector>格式 -int LaserDataLoader::ConvertToSVzNL3DPosition(const std::vector>& unifiedData, - std::vector>& scanLines, - size_t* validPointCount) -{ - LOG_DEBUG("Converting unified data to scan lines format\n"); - - scanLines.clear(); - - // 初始化有效点计数 - size_t validCount = 0; - - // 浮点数零值判断阈值 - const double EPSILON = 1e-6; - - for (const auto& linePair : unifiedData) { - EVzResultDataType dataType = linePair.first; - const SVzLaserLineData& lineData = linePair.second; - + case keResultDataType_PointF: + delete[] static_cast(lineData.p3DPoint); + break; + case keResultDataType_PointXYZ: + delete[] static_cast(lineData.p3DPoint); + break; + case keResultDataType_PointXYZRGBA: + delete[] static_cast(lineData.p3DPoint); + break; + case keResultDataType_PointGray: + delete[] static_cast(lineData.p3DPoint); + break; + case keResultDataType_PointXYZI: + delete[] static_cast(lineData.p3DPoint); + break; + case keResultDataType_PointRGBA_D: + delete[] static_cast(lineData.p3DPoint); + break; + default: + LOG_WARN("Unknown laser data type when freeing p3DPoint: %d\n", dataType); + break; + } + lineData.p3DPoint = nullptr; + } + + if (lineData.p2DPoint) { + switch (dataType) { + case keResultDataType_Position: + delete[] static_cast(lineData.p2DPoint); + break; + case keResultDataType_PositionF: + delete[] static_cast(lineData.p2DPoint); + break; + case keResultDataType_PointF: + case keResultDataType_PointXYZ: + case keResultDataType_PointXYZRGBA: + case keResultDataType_PointGray: + case keResultDataType_PointXYZI: + case keResultDataType_PointRGBA_D: + delete[] static_cast(lineData.p2DPoint); + break; + default: + LOG_WARN("Unknown laser data type when freeing p2DPoint: %d\n", dataType); + break; + } + lineData.p2DPoint = nullptr; + } + } + laserLines.clear(); + } + + LOG_DEBUG("Unified laser scan data freed successfully\n"); +} + +// 转换统一格式数据为std::vector>格式 +int LaserDataLoader::ConvertToSVzNL3DPosition(const std::vector>& unifiedData, + std::vector>& scanLines, + size_t* validPointCount) +{ + LOG_DEBUG("Converting unified data to scan lines format\n"); + + scanLines.clear(); + + // 初始化有效点计数 + size_t validCount = 0; + + // 浮点数零值判断阈值 + const double EPSILON = 1e-6; + + for (const auto& linePair : unifiedData) { + EVzResultDataType dataType = linePair.first; + const SVzLaserLineData& lineData = linePair.second; + // 只处理Position类型的数据 if (dataType == keResultDataType_Position && lineData.p3DPoint) { - std::vector scanLine; - - // 为当前扫描线分配空间 - scanLine.reserve(lineData.nPointCount); - - // 复制点数据并统计有效点 - const SVzNL3DPosition* points = static_cast(lineData.p3DPoint); - for (int i = 0; i < lineData.nPointCount; i++) { - scanLine.push_back(points[i]); - - // 统计有效点(xyz全非0,使用epsilon阈值判断) - if (std::fabs(points[i].pt3D.x) > EPSILON && - std::fabs(points[i].pt3D.y) > EPSILON && - std::fabs(points[i].pt3D.z) > EPSILON) { - validCount++; - } - } + std::vector scanLine; + + // 为当前扫描线分配空间 + scanLine.reserve(lineData.nPointCount); + + // 复制点数据并统计有效点 + const SVzNL3DPosition* points = static_cast(lineData.p3DPoint); + for (int i = 0; i < lineData.nPointCount; i++) { + scanLine.push_back(points[i]); + + // 统计有效点(xyz全非0,使用epsilon阈值判断) + if (std::fabs(points[i].pt3D.x) > EPSILON && + std::fabs(points[i].pt3D.y) > EPSILON && + std::fabs(points[i].pt3D.z) > EPSILON) { + validCount++; + } + } scanLines.push_back(scanLine); } else if (dataType == keResultDataType_PositionF && lineData.p3DPoint) { @@ -959,45 +1032,45 @@ int LaserDataLoader::ConvertToSVzNL3DPosition(const std::vector>& unifiedData, std::vector>& scanLines) { LOG_DEBUG("Converting unified data to SVzNLPositionD scan lines format\n"); - - scanLines.clear(); - - for (const auto& linePair : unifiedData) { + + scanLines.clear(); + + for (const auto& linePair : unifiedData) { if (linePair.first != keResultDataType_Position && linePair.first != keResultDataType_PositionF) { continue; } - - const SVzLaserLineData& lineData = linePair.second; - if (lineData.nPointCount < 0) { - continue; - } - - if (lineData.nPointCount > 0 && (!lineData.p3DPoint || !lineData.p2DPoint)) { - m_lastError = "Missing 3D or 2D point buffer in Position laser data"; - LOG_ERROR("%s, pointCount=%d, p3D=%p, p2D=%p\n", - m_lastError.c_str(), - lineData.nPointCount, - lineData.p3DPoint, - lineData.p2DPoint); - continue; - } - + + const SVzLaserLineData& lineData = linePair.second; + if (lineData.nPointCount < 0) { + continue; + } + + if (lineData.nPointCount > 0 && (!lineData.p3DPoint || !lineData.p2DPoint)) { + m_lastError = "Missing 3D or 2D point buffer in Position laser data"; + LOG_ERROR("%s, pointCount=%d, p3D=%p, p2D=%p\n", + m_lastError.c_str(), + lineData.nPointCount, + lineData.p3DPoint, + lineData.p2DPoint); + continue; + } + std::vector scanLine; scanLine.reserve(static_cast(lineData.nPointCount)); if (linePair.first == keResultDataType_Position) { @@ -1033,132 +1106,132 @@ int LaserDataLoader::ConvertToSVzNLPositionD( if (!scanLine.empty()) { scanLines.push_back(scanLine); } - } - - if (scanLines.empty()) { - m_lastError = "No Position laser data to convert"; - LOG_ERROR("%s\n", m_lastError.c_str()); - return ERR_CODE(DATA_ERR_INVALID); - } - - LOG_DEBUG("Converted %zu lines to SVzNLPositionD scan lines format\n", scanLines.size()); - return SUCCESS; -} - -// 转换统一格式数据为SVzNL3DLaserLine格式 -int LaserDataLoader::ConvertToSVzNL3DLaserLine(const std::vector>& unifiedData, - std::vector& xyzData) -{ - xyzData.clear(); - - for (const auto& linePair : unifiedData) { - EVzResultDataType dataType = linePair.first; - const SVzLaserLineData& lineData = linePair.second; - + } + + if (scanLines.empty()) { + m_lastError = "No Position laser data to convert"; + LOG_ERROR("%s\n", m_lastError.c_str()); + return ERR_CODE(DATA_ERR_INVALID); + } + + LOG_DEBUG("Converted %zu lines to SVzNLPositionD scan lines format\n", scanLines.size()); + return SUCCESS; +} + +// 转换统一格式数据为SVzNL3DLaserLine格式 +int LaserDataLoader::ConvertToSVzNL3DLaserLine(const std::vector>& unifiedData, + std::vector& xyzData) +{ + xyzData.clear(); + + for (const auto& linePair : unifiedData) { + EVzResultDataType dataType = linePair.first; + const SVzLaserLineData& lineData = linePair.second; + // PositionF 的 3D buffer 也是 SVzNL3DPosition;仅 2D UV 为浮点。 if ((dataType == keResultDataType_Position || dataType == keResultDataType_PositionF) && lineData.p3DPoint) { SVzNL3DLaserLine xyzLine; xyzLine.nTimeStamp = lineData.llTimeStamp; xyzLine.nPositionCnt = lineData.nPointCount; - - if (lineData.nPointCount > 0) { - xyzLine.p3DPosition = new SVzNL3DPosition[lineData.nPointCount]; - if (xyzLine.p3DPosition) { - memcpy(xyzLine.p3DPosition, lineData.p3DPoint, sizeof(SVzNL3DPosition) * lineData.nPointCount); - } else { - m_lastError = "Memory allocation failed for SVzNL3DPosition"; - LOG_ERROR("Memory allocation failed for SVzNL3DPosition\n"); - return ERR_CODE(DATA_ERR_INVALID); - } - } else { - xyzLine.p3DPosition = nullptr; - } - - xyzData.push_back(xyzLine); - } - } - - LOG_DEBUG("Converted %zu lines to SVzNL3DLaserLine format\n", xyzData.size()); - return SUCCESS; -} - -// 转换统一格式数据为SVzNLXYZRGBDLaserLine格式 -int LaserDataLoader::ConvertToSVzNLXYZRGBDLaserLine(const std::vector>& unifiedData, - std::vector& rgbdData) -{ - LOG_DEBUG("Converting unified data to SVzNLXYZRGBDLaserLine format\n"); - - rgbdData.clear(); - - for (const auto& linePair : unifiedData) { - EVzResultDataType dataType = linePair.first; - const SVzLaserLineData& lineData = linePair.second; - - // 只处理PointXYZRGBA类型的数据 - if (dataType == keResultDataType_PointXYZRGBA && lineData.p3DPoint) { - SVzNLXYZRGBDLaserLine rgbdLine; - rgbdLine.nTimeStamp = lineData.llTimeStamp; - rgbdLine.nPointCnt = lineData.nPointCount; - - if (lineData.nPointCount > 0) { - rgbdLine.p3DPoint = new SVzNLPointXYZRGBA[lineData.nPointCount]; - if (rgbdLine.p3DPoint) { - memcpy(rgbdLine.p3DPoint, lineData.p3DPoint, sizeof(SVzNLPointXYZRGBA) * lineData.nPointCount); - } else { - m_lastError = "Memory allocation failed for SVzNLPointXYZRGBA"; - LOG_ERROR("Memory allocation failed for SVzNLPointXYZRGBA\n"); - return ERR_CODE(DATA_ERR_INVALID); - } - } else { - rgbdLine.p3DPoint = nullptr; - } - - rgbdData.push_back(rgbdLine); - } - } - - LOG_DEBUG("Converted %zu lines to SVzNLXYZRGBDLaserLine format\n", rgbdData.size()); - return SUCCESS; -} - -// 释放转换后的SVzNL3DLaserLine数据内存 -void LaserDataLoader::FreeConvertedData(std::vector& xyzData) -{ - LOG_DEBUG("Freeing converted SVzNL3DLaserLine data, line count: %zu\n", xyzData.size()); - - if (!xyzData.empty()) { - for (auto& scanLine : xyzData) { - if (scanLine.p3DPosition) { - delete[] scanLine.p3DPosition; - scanLine.p3DPosition = nullptr; - } - } - xyzData.clear(); - } - - LOG_DEBUG("Converted SVzNL3DLaserLine data freed successfully\n"); -} - -// 释放转换后的SVzNLXYZRGBDLaserLine数据内存 -void LaserDataLoader::FreeConvertedData(std::vector& rgbdData) -{ - LOG_DEBUG("Freeing converted SVzNLXYZRGBDLaserLine data, line count: %zu\n", rgbdData.size()); - - if (!rgbdData.empty()) { - for (auto& scanLine : rgbdData) { - if (scanLine.p3DPoint) { - delete[] scanLine.p3DPoint; - scanLine.p3DPoint = nullptr; - } - } - rgbdData.clear(); - } - - LOG_DEBUG("Converted SVzNLXYZRGBDLaserLine data freed successfully\n"); -} - - + + if (lineData.nPointCount > 0) { + xyzLine.p3DPosition = new SVzNL3DPosition[lineData.nPointCount]; + if (xyzLine.p3DPosition) { + memcpy(xyzLine.p3DPosition, lineData.p3DPoint, sizeof(SVzNL3DPosition) * lineData.nPointCount); + } else { + m_lastError = "Memory allocation failed for SVzNL3DPosition"; + LOG_ERROR("Memory allocation failed for SVzNL3DPosition\n"); + return ERR_CODE(DATA_ERR_INVALID); + } + } else { + xyzLine.p3DPosition = nullptr; + } + + xyzData.push_back(xyzLine); + } + } + + LOG_DEBUG("Converted %zu lines to SVzNL3DLaserLine format\n", xyzData.size()); + return SUCCESS; +} + +// 转换统一格式数据为SVzNLXYZRGBDLaserLine格式 +int LaserDataLoader::ConvertToSVzNLXYZRGBDLaserLine(const std::vector>& unifiedData, + std::vector& rgbdData) +{ + LOG_DEBUG("Converting unified data to SVzNLXYZRGBDLaserLine format\n"); + + rgbdData.clear(); + + for (const auto& linePair : unifiedData) { + EVzResultDataType dataType = linePair.first; + const SVzLaserLineData& lineData = linePair.second; + + // 只处理PointXYZRGBA类型的数据 + if (dataType == keResultDataType_PointXYZRGBA && lineData.p3DPoint) { + SVzNLXYZRGBDLaserLine rgbdLine; + rgbdLine.nTimeStamp = lineData.llTimeStamp; + rgbdLine.nPointCnt = lineData.nPointCount; + + if (lineData.nPointCount > 0) { + rgbdLine.p3DPoint = new SVzNLPointXYZRGBA[lineData.nPointCount]; + if (rgbdLine.p3DPoint) { + memcpy(rgbdLine.p3DPoint, lineData.p3DPoint, sizeof(SVzNLPointXYZRGBA) * lineData.nPointCount); + } else { + m_lastError = "Memory allocation failed for SVzNLPointXYZRGBA"; + LOG_ERROR("Memory allocation failed for SVzNLPointXYZRGBA\n"); + return ERR_CODE(DATA_ERR_INVALID); + } + } else { + rgbdLine.p3DPoint = nullptr; + } + + rgbdData.push_back(rgbdLine); + } + } + + LOG_DEBUG("Converted %zu lines to SVzNLXYZRGBDLaserLine format\n", rgbdData.size()); + return SUCCESS; +} + +// 释放转换后的SVzNL3DLaserLine数据内存 +void LaserDataLoader::FreeConvertedData(std::vector& xyzData) +{ + LOG_DEBUG("Freeing converted SVzNL3DLaserLine data, line count: %zu\n", xyzData.size()); + + if (!xyzData.empty()) { + for (auto& scanLine : xyzData) { + if (scanLine.p3DPosition) { + delete[] scanLine.p3DPosition; + scanLine.p3DPosition = nullptr; + } + } + xyzData.clear(); + } + + LOG_DEBUG("Converted SVzNL3DLaserLine data freed successfully\n"); +} + +// 释放转换后的SVzNLXYZRGBDLaserLine数据内存 +void LaserDataLoader::FreeConvertedData(std::vector& rgbdData) +{ + LOG_DEBUG("Freeing converted SVzNLXYZRGBDLaserLine data, line count: %zu\n", rgbdData.size()); + + if (!rgbdData.empty()) { + for (auto& scanLine : rgbdData) { + if (scanLine.p3DPoint) { + delete[] scanLine.p3DPoint; + scanLine.p3DPoint = nullptr; + } + } + rgbdData.clear(); + } + + LOG_DEBUG("Converted SVzNLXYZRGBDLaserLine data freed successfully\n"); +} + + int LaserDataLoader::_ParseLaserScanPoint(const std::string& data, SVzNL3DPosition& sData, SVzNL2DPosition& s2DData) { float X = 0.0f, Y = 0.0f, Z = 0.0f; @@ -1173,10 +1246,10 @@ int LaserDataLoader::_ParseLaserScanPoint(const std::string& data, SVzNL3DPositi sData.pt3D.x = X; sData.pt3D.y = Y; sData.pt3D.z = Z; - s2DData.ptLeft2D.x = leftX; - s2DData.ptLeft2D.y = leftY; - s2DData.ptRight2D.x = rightX; - s2DData.ptRight2D.y = rightY; + s2DData.ptLeft2D.x = leftX; + s2DData.ptLeft2D.y = leftY; + s2DData.ptRight2D.x = rightX; + s2DData.ptRight2D.y = rightY; return SUCCESS; } @@ -1202,71 +1275,73 @@ int LaserDataLoader::_ParseLaserScanPoint(const std::string& data, SVzNL3DPositi } int LaserDataLoader::_ParseLaserScanPoint(const std::string& data, SVzNLPointXYZRGBA& sData, SVzNL2DLRPoint& s2DData) -{ - float X, Y, Z; - float leftX, leftY; - float rightX, rightY; - - // 尝试新格式: {x,y,z}-{lx,ly}-{rx,ry}-{R,G,B,A} - int R, G, B, A; - int parsed = sscanf(data.c_str(), - " { %f , %f , %f } - { %f , %f } - { %f , %f } - { %d , %d , %d , %d }", - &X, &Y, &Z, &leftX, &leftY, &rightX, &rightY, &R, &G, &B, &A); - if (parsed >= 11) { - sData.x = X; - sData.y = Y; - sData.z = Z; - - // R,G,B,A 是 0-255 整数值,Alpha 存储在高 8 位 - unsigned int nRGB = static_cast(A) << 24; - nRGB |= static_cast(B) << 16; - nRGB |= static_cast(G) << 8; - nRGB |= static_cast(R); - sData.nRGB = nRGB; - - s2DData.sLeft.x = leftX; - s2DData.sLeft.y = leftY; - s2DData.sRight.x = rightX; - s2DData.sRight.y = rightY; - return SUCCESS; - } - - // 旧格式: {x,y,z,r,g,b}-{lx,ly}-{rx,ry} r,g,b 为 0~1 浮点值 - float r, g, b; - sscanf(data.c_str(), - " { %f , %f , %f , %f , %f , %f } - { %f , %f } - { %f , %f }", - &X, &Y, &Z, &r, &g, &b, &leftX, &leftY, &rightX, &rightY); - sData.x = X; - sData.y = Y; - sData.z = Z; - - int nr = (int)(r * 255); - int ng = (int)(g * 255); - int nb = (int)(b * 255); - nb <<= 8; - nb += ng; - nb <<= 8; - nb += nr; - sData.nRGB = nb; - - s2DData.sLeft.x = leftX; - s2DData.sLeft.y = leftY; - s2DData.sRight.x = rightX; - s2DData.sRight.y = rightY; - return SUCCESS; -} - -// 获取激光数据类型 +{ + float X = 0.0f, Y = 0.0f, Z = 0.0f; + float leftX = 0.0f, leftY = 0.0f; + float rightX = 0.0f, rightY = 0.0f; + + // 尝试新格式: {x,y,z}-{lx,ly}-{rx,ry}-{R,G,B,A} + int R, G, B, A; + int parsed = sscanf(data.c_str(), + " { %f , %f , %f } - { %f , %f } - { %f , %f } - { %d , %d , %d , %d }", + &X, &Y, &Z, &leftX, &leftY, &rightX, &rightY, &R, &G, &B, &A); + if (parsed >= 11) { + sData.x = X; + sData.y = Y; + sData.z = Z; + + // R,G,B,A 是 0-255 整数值,Alpha 存储在高 8 位 + unsigned int nRGB = static_cast(A) << 24; + nRGB |= static_cast(B) << 16; + nRGB |= static_cast(G) << 8; + nRGB |= static_cast(R); + sData.nRGB = nRGB; + + s2DData.sLeft.x = leftX; + s2DData.sLeft.y = leftY; + s2DData.sRight.x = rightX; + s2DData.sRight.y = rightY; + return SUCCESS; + } + + // 旧格式: {x,y,z,r,g,b}-{lx,ly}-{rx,ry} r,g,b 为 0~1 浮点值 + float r = 0.0f, g = 0.0f, b = 0.0f; + if (10 != sscanf(data.c_str(), + " { %f , %f , %f , %f , %f , %f } - { %f , %f } - { %f , %f }", + &X, &Y, &Z, &r, &g, &b, &leftX, &leftY, &rightX, &rightY)) { + return ERR_CODE(FILE_ERR_FORMAT); + } + sData.x = X; + sData.y = Y; + sData.z = Z; + + int nr = (int)(r * 255); + int ng = (int)(g * 255); + int nb = (int)(b * 255); + nb <<= 8; + nb += ng; + nb <<= 8; + nb += nr; + sData.nRGB = nb; + + s2DData.sLeft.x = leftX; + s2DData.sLeft.y = leftY; + s2DData.sRight.x = rightX; + s2DData.sRight.y = rightY; + return SUCCESS; +} + +// 获取激光数据类型 int LaserDataLoader::_GetLaserType(const std::string& fileName, EVzResultDataType& eDataType) { std::ifstream inputFile(fileName); std::string linedata; - - if (!inputFile.is_open()) { - m_lastError = "Cannot open file: " + fileName; - return ERR_CODE(FILE_ERR_NOEXIST); - } - + + if (!inputFile.is_open()) { + m_lastError = "Cannot open file: " + fileName; + return ERR_CODE(FILE_ERR_NOEXIST); + } + bool bFind = false; while (std::getline(inputFile, linedata)) { TrimCarriageReturn(linedata); @@ -1275,21 +1350,21 @@ int LaserDataLoader::_GetLaserType(const std::string& fileName, EVzResultDataTyp // 统计 { 的数量判断格式 int braceCount = 0; for (char c : linedata) { - if (c == '{') braceCount++; - } - - if (braceCount == 4) { - // 新RGBD格式: {x,y,z}-{lx,ly}-{rx,ry}-{R,G,B,A} - eDataType = keResultDataType_PointXYZRGBA; - bFind = true; - } else if (braceCount == 3) { - // 检查第一组的逗号数量区分XYZ和旧RGBD - size_t firstClose = linedata.find('}'); - if (firstClose != std::string::npos) { - int commaCount = 0; - for (size_t i = 0; i < firstClose; ++i) { - if (linedata[i] == ',') commaCount++; - } + if (c == '{') braceCount++; + } + + if (braceCount == 4) { + // 新RGBD格式: {x,y,z}-{lx,ly}-{rx,ry}-{R,G,B,A} + eDataType = keResultDataType_PointXYZRGBA; + bFind = true; + } else if (braceCount == 3) { + // 检查第一组的逗号数量区分XYZ和旧RGBD + size_t firstClose = linedata.find('}'); + if (firstClose != std::string::npos) { + int commaCount = 0; + for (size_t i = 0; i < firstClose; ++i) { + if (linedata[i] == ',') commaCount++; + } if (commaCount >= 5) { // 旧RGBD格式: {x,y,z,r,g,b}-{lx,ly}-{rx,ry} eDataType = keResultDataType_PointXYZRGBA; @@ -1300,11 +1375,11 @@ int LaserDataLoader::_GetLaserType(const std::string& fileName, EVzResultDataTyp bFind = true; } } - } - break; - } - } - - inputFile.close(); - return bFind ? SUCCESS : ERR_CODE(FILE_ERR_FORMAT); -} + } + break; + } + } + + inputFile.close(); + return bFind ? SUCCESS : ERR_CODE(FILE_ERR_FORMAT); +} diff --git a/CloudView/CloudView.pro b/CloudView/CloudView.pro index adcf38a..e38b636 100644 --- a/CloudView/CloudView.pro +++ b/CloudView/CloudView.pro @@ -15,9 +15,9 @@ TEMPLATE = app # 椤圭洰鐩綍 INCLUDEPATH += $$PWD/Inc +INCLUDEPATH += $$PWD/../../Device/SDK/VzNLSDK/Inc INCLUDEPATH += $$PWD/../CloudView3D/Inc INCLUDEPATH += $$PWD/../CloudUtils/Inc -INCLUDEPATH += $$PWD/../../Device/SDK/VzNLSDK/Inc INCLUDEPATH += $$PWD/../VrUtils/Inc INCLUDEPATH += $$PWD/../VrCommon/Inc @@ -35,23 +35,28 @@ SOURCES += \ # 璧勬簮鏂囦欢 RESOURCES += resource/resource.qrc -RESOURCES += $$PWD/../../AppUtils/UICommon/common_resources.qrc +COMMON_RESOURCES = $$PWD/../../AppUtils/UICommon/common_resources.qrc +exists($$COMMON_RESOURCES) { + RESOURCES += $$COMMON_RESOURCES +} else { + message("CloudView: optional common_resources.qrc not found; using text view buttons") +} # Windows 搴旂敤鍥炬爣 RC_ICONS = resource/logo.ico # Windows骞冲彴搴撻摼鎺? win32:CONFIG(release, debug|release): { - LIBS += -L../CloudView3D/release -lCloudView3D - LIBS += -L../CloudUtils/release -lCloudUtils - LIBS += -L../VrUtils/release -lVrUtils - LIBS += -L../VrCommon/release -lVrCommon + LIBS += -L$$PWD/../CloudView3D/release -lCloudView3D + LIBS += -L$$PWD/../CloudUtils/release -lCloudUtils + LIBS += -L$$PWD/../VrUtils/release -lVrUtils + LIBS += -L$$PWD/../VrCommon/release -lVrCommon } else:win32:CONFIG(debug, debug|release): { - LIBS += -L../CloudView3D/debug -lCloudView3D - LIBS += -L../CloudUtils/debug -lCloudUtils - LIBS += -L../VrUtils/debug -lVrUtils - LIBS += -L../VrCommon/debug -lVrCommon + LIBS += -L$$PWD/../CloudView3D/debug -lCloudView3D + LIBS += -L$$PWD/../CloudUtils/debug -lCloudUtils + LIBS += -L$$PWD/../VrUtils/debug -lVrUtils + LIBS += -L$$PWD/../VrCommon/debug -lVrCommon } # Windows绯荤粺搴? diff --git a/CloudView/Inc/CloudViewMainWindow.h b/CloudView/Inc/CloudViewMainWindow.h index eabe549..73cda7e 100644 --- a/CloudView/Inc/CloudViewMainWindow.h +++ b/CloudView/Inc/CloudViewMainWindow.h @@ -17,17 +17,17 @@ #include #include #include -#include -#include -#include -#include +#include +#include +#include +#include -#include "CloudView3D.h" -#include "PointCloudConverter.h" +#include "CloudView3D.h" +#include "PointCloudConverter.h" -class QTextEdit; -class QTableWidget; -class QCloseEvent; +class QTextEdit; +class QTableWidget; +class QCloseEvent; /** * @brief 点云查看器主窗口 @@ -37,55 +37,55 @@ class CloudViewMainWindow : public QMainWindow { Q_OBJECT -public: - struct StartupFiles - { - QStringList displayFiles; - QStringList bboxFiles; - QStringList circleFiles; - QStringList rectangleFiles; - QStringList surfaceFiles; - QStringList triangleFiles; - bool sequential = false; - }; - - explicit CloudViewMainWindow(QWidget* parent = nullptr); - ~CloudViewMainWindow() override; - - bool loadStartupFiles(const StartupFiles& files); - bool loadDisplayFile(const QString& fileName); - bool loadCircleShapeFile(const QString& fileName); - bool loadRectangleShapeFile(const QString& fileName); - bool loadSurfaceShapeFile(const QString& fileName); - bool loadTriangleShapeFile(const QString& fileName); - void fitViewToContent(); +public: + struct StartupFiles + { + QStringList displayFiles; + QStringList bboxFiles; + QStringList circleFiles; + QStringList rectangleFiles; + QStringList surfaceFiles; + QStringList triangleFiles; + bool sequential = false; + }; -protected: - bool eventFilter(QObject* obj, QEvent* event) override; - void closeEvent(QCloseEvent* event) override; - -private: - enum class StartupFileType - { - Display, - BBox, - Circle, - Rectangle, - Surface, - Triangle - }; - - struct StartupItem - { - StartupFileType type; - QString fileName; - }; - - void buildStartupSequence(const StartupFiles& files); - bool loadStartupItem(const StartupItem& item); - bool loadCurrentStartupSequenceItem(); - -private slots: + explicit CloudViewMainWindow(QWidget* parent = nullptr); + ~CloudViewMainWindow() override; + + bool loadStartupFiles(const StartupFiles& files); + bool loadDisplayFile(const QString& fileName); + bool loadCircleShapeFile(const QString& fileName); + bool loadRectangleShapeFile(const QString& fileName); + bool loadSurfaceShapeFile(const QString& fileName); + bool loadTriangleShapeFile(const QString& fileName); + void fitViewToContent(); + +protected: + bool eventFilter(QObject* obj, QEvent* event) override; + void closeEvent(QCloseEvent* event) override; + +private: + enum class StartupFileType + { + Display, + BBox, + Circle, + Rectangle, + Surface, + Triangle + }; + + struct StartupItem + { + StartupFileType type; + QString fileName; + }; + + void buildStartupSequence(const StartupFiles& files); + bool loadStartupItem(const StartupItem& item); + bool loadCurrentStartupSequenceItem(); + +private slots: /** * @brief 打开文件 */ @@ -146,11 +146,6 @@ private slots: */ void onPoint2CoordChanged(); - /** - * @brief 处理视角旋转角度改变事件 - */ - void onViewAnglesChanged(float rotX, float rotY, float rotZ); - /** * @brief 通过输入线号选择线 */ @@ -211,11 +206,11 @@ private slots: void onSavePointCloud(); // 保存 txt 点云 void onSavePlyPcdCloud(); // 保存 ply/pcd 点云 void onBatchConvertTxtToPly(); // 批量转换 txt -> ply - void onConvertEulerMatrix(); - void onShowCircleShape(); - void onShowRectangleShape(); - void onShowSurfaceShape(); - void onShowTriangleShape(); + void onConvertEulerMatrix(); + void onShowCircleShape(); + void onShowRectangleShape(); + void onShowSurfaceShape(); + void onShowTriangleShape(); /** * @brief 姿态补偿计算 @@ -243,11 +238,6 @@ private: */ QGroupBox* createFileGroup(); - /** - * @brief 创建视图方向工具栏(纵向排列,位于3D视图左侧) - */ - QWidget* createViewToolbar(); - /** * @brief 创建选点测距组 */ @@ -276,14 +266,14 @@ private: /** * @brief 更新选点信息显示 */ - void updateSelectedPointsDisplay(); - void clearMeasureResultDisplay(); - void updateMeasureResultDisplay(const SelectedPointInfo& p1, - const SelectedPointInfo& p2, - float distance, - const QString& statusPrefix = QString()); - - /** + void updateSelectedPointsDisplay(); + void clearMeasureResultDisplay(); + void updateMeasureResultDisplay(const SelectedPointInfo& p1, + const SelectedPointInfo& p2, + float distance, + const QString& statusPrefix = QString()); + + /** * @brief 从原始数据获取线上点(包含0,0,0) */ QVector getOriginalLinePoints(const SelectedLineInfo& lineInfo); @@ -310,12 +300,12 @@ private: * @return 是否成功加载到包围盒数据 */ bool loadBoundingBoxFile(const QString& fileName); - void applyTransformToAllClouds(const QMatrix4x4& matrix); - void addGeneratedCloud(const PointCloudXYZ& cloud, const QString& name); - void showBasicShape(const QString& shapeName, - const QVector& segments, - const QVector& points = QVector()); - void showBasicSurface(const QString& shapeName, const QVector& surfaces, const QVector& outlines); + void applyTransformToAllClouds(const QMatrix4x4& matrix); + void addGeneratedCloud(const PointCloudXYZ& cloud, const QString& name); + void showBasicShape(const QString& shapeName, + const QVector& segments, + const QVector& points = QVector()); + void showBasicSurface(const QString& shapeName, const QVector& surfaces, const QVector& outlines); // 点云显示控件 PointCloudGLWidget* m_glWidget; @@ -328,18 +318,13 @@ private: QPushButton* m_btnOpenBBox; QPushButton* m_btnClearAll; - // 视图角度输入控件 - QLineEdit* m_editRotX; - QLineEdit* m_editRotY; - QLineEdit* m_editRotZ; - // 选点测距控件 QCheckBox* m_cbMeasureDistance; QPushButton* m_btnClearPoints; - QLabel* m_lblPoint1; - QLabel* m_lblPoint2; - QLabel* m_lblDistance; - QLabel* m_lblDeltaXYZ; + QLabel* m_lblPoint1; + QLabel* m_lblPoint2; + QLabel* m_lblDistance; + QLabel* m_lblDeltaXYZ; // 点1坐标编辑控件 QLineEdit* m_editPoint1X; @@ -407,14 +392,10 @@ private: QTableWidget* m_linePointsTable; QVector m_currentLinePoints; // 当前线的原始点坐标 - // 悬浮缩放按钮 - QToolButton* m_btnZoomIn; - QToolButton* m_btnZoomOut; - - // 启动参数序列显示 - QVector m_startupSequence; - int m_startupSequenceIndex; - bool m_startupSequentialMode; -}; + // 启动参数序列显示 + QVector m_startupSequence; + int m_startupSequenceIndex; + bool m_startupSequentialMode; +}; #endif // CLOUD_VIEW_MAIN_WINDOW_H diff --git a/CloudView/Src/CloudViewMainWindow.cpp b/CloudView/Src/CloudViewMainWindow.cpp index 9dd45d8..39d02ab 100644 --- a/CloudView/Src/CloudViewMainWindow.cpp +++ b/CloudView/Src/CloudViewMainWindow.cpp @@ -25,12 +25,12 @@ #include #include #include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include +#include +#include #include #include #include @@ -40,22 +40,22 @@ namespace { -constexpr double kPi = 3.14159265358979323846; -constexpr double kDegToRad = kPi / 180.0; -constexpr float kCircleR = 1.0f; -constexpr float kCircleG = 0.85f; -constexpr float kCircleB = 0.0f; -constexpr float kRectangleR = 1.0f; -constexpr float kRectangleG = 0.25f; -constexpr float kRectangleB = 0.9f; -constexpr float kSurfaceR = 0.0f; -constexpr float kSurfaceG = 0.8f; -constexpr float kSurfaceB = 1.0f; -constexpr float kSurfaceA = 0.5f; -constexpr float kShapeCenterR = 1.0f; -constexpr float kShapeCenterG = 0.1f; -constexpr float kShapeCenterB = 0.1f; -constexpr float kShapeCenterPointSize = 9.0f; +constexpr double kPi = 3.14159265358979323846; +constexpr double kDegToRad = kPi / 180.0; +constexpr float kCircleR = 1.0f; +constexpr float kCircleG = 0.85f; +constexpr float kCircleB = 0.0f; +constexpr float kRectangleR = 1.0f; +constexpr float kRectangleG = 0.25f; +constexpr float kRectangleB = 0.9f; +constexpr float kSurfaceR = 0.0f; +constexpr float kSurfaceG = 0.8f; +constexpr float kSurfaceB = 1.0f; +constexpr float kSurfaceA = 0.5f; +constexpr float kShapeCenterR = 1.0f; +constexpr float kShapeCenterG = 0.1f; +constexpr float kShapeCenterB = 0.1f; +constexpr float kShapeCenterPointSize = 9.0f; bool parseMatrixText(const QString& text, QMatrix4x4& matrix, QString* errorMessage = nullptr) { @@ -412,36 +412,36 @@ void getShapeFrame(const PointCloudXYZ& cloud, QVector3D& center, QVector3D& siz size = maxBound - minBound; } -QDoubleSpinBox* createShapeSpinBox(QWidget* parent, double value, double minValue = -1000000000.0, double maxValue = 1000000000.0) -{ - QDoubleSpinBox* spin = new QDoubleSpinBox(parent); - spin->setDecimals(6); +QDoubleSpinBox* createShapeSpinBox(QWidget* parent, double value, double minValue = -1000000000.0, double maxValue = 1000000000.0) +{ + QDoubleSpinBox* spin = new QDoubleSpinBox(parent); + spin->setDecimals(6); spin->setRange(minValue, maxValue); - spin->setValue(value); - spin->setSingleStep(1.0); - spin->setAlignment(Qt::AlignRight); - spin->setButtonSymbols(QAbstractSpinBox::NoButtons); - return spin; -} - -EulerRotationOrder currentShapeEulerOrder(const QComboBox* combo) -{ - if (combo && combo->currentIndex() >= 0) { - return static_cast(combo->currentData().toInt()); - } - return EulerRotationOrder::ZYX; -} - -QVector3D transformShapePoint(const QVector3D& center, const QMatrix4x4& rotation, const QVector3D& localPoint) -{ - return center + rotation.map(localPoint); -} - -QVector makeClosedShapeSegments(const QVector& points, float r, float g, float b) -{ - QVector segments; - if (points.size() < 2) { - return segments; + spin->setValue(value); + spin->setSingleStep(1.0); + spin->setAlignment(Qt::AlignRight); + spin->setButtonSymbols(QAbstractSpinBox::NoButtons); + return spin; +} + +EulerRotationOrder currentShapeEulerOrder(const QComboBox* combo) +{ + if (combo && combo->currentIndex() >= 0) { + return static_cast(combo->currentData().toInt()); + } + return EulerRotationOrder::ZYX; +} + +QVector3D transformShapePoint(const QVector3D& center, const QMatrix4x4& rotation, const QVector3D& localPoint) +{ + return center + rotation.map(localPoint); +} + +QVector makeClosedShapeSegments(const QVector& points, float r, float g, float b) +{ + QVector segments; + if (points.size() < 2) { + return segments; } segments.reserve(points.size()); @@ -451,702 +451,690 @@ QVector makeClosedShapeSegments(const QVector& points, f segments.append(LineSegment(p1.x(), p1.y(), p1.z(), p2.x(), p2.y(), p2.z(), r, g, b, 3.0f)); - } - return segments; -} - -QVector makeShapeCenterPoint(const QVector3D& center) -{ - QVector points; - points.append(BasicShapePoint(center.x(), - center.y(), - center.z(), - kShapeCenterR, - kShapeCenterG, - kShapeCenterB, - kShapeCenterPointSize)); - return points; -} - -QVector makeCircleSegments(const QVector3D& center, - double rxDeg, - double ryDeg, - double rzDeg, - float radius, - EulerRotationOrder order) -{ - QVector segments; - if (radius <= 0.0f) { - return segments; - } - - const QMatrix4x4 rotation = buildEulerRotationMatrix(order, rxDeg, ryDeg, rzDeg); - const int segmentCount = 96; - segments.reserve(segmentCount); - for (int i = 0; i < segmentCount; ++i) { - const float a1 = static_cast(2.0 * kPi * i / segmentCount); - const float a2 = static_cast(2.0 * kPi * (i + 1) / segmentCount); - const QVector3D p1 = transformShapePoint( - center, rotation, QVector3D(radius * std::cos(a1), radius * std::sin(a1), 0.0f)); - const QVector3D p2 = transformShapePoint( - center, rotation, QVector3D(radius * std::cos(a2), radius * std::sin(a2), 0.0f)); - segments.append(LineSegment( - p1.x(), p1.y(), p1.z(), - p2.x(), p2.y(), p2.z(), - kCircleR, kCircleG, kCircleB, 3.0f)); - } - return segments; -} - -QVector makeRectangleSegments(const QVector3D& center, - double rxDeg, - double ryDeg, - double rzDeg, - float width, - float height, - EulerRotationOrder order) -{ - if (width <= 0.0f || height <= 0.0f) { - return {}; - } - - QVector points; - points.reserve(4); - const QMatrix4x4 rotation = buildEulerRotationMatrix(order, rxDeg, ryDeg, rzDeg); - const float halfWidth = width * 0.5f; - const float halfHeight = height * 0.5f; - - points.append(transformShapePoint(center, rotation, QVector3D(-halfWidth, -halfHeight, 0.0f))); - points.append(transformShapePoint(center, rotation, QVector3D( halfWidth, -halfHeight, 0.0f))); - points.append(transformShapePoint(center, rotation, QVector3D( halfWidth, halfHeight, 0.0f))); - points.append(transformShapePoint(center, rotation, QVector3D(-halfWidth, halfHeight, 0.0f))); - return makeClosedShapeSegments(points, kRectangleR, kRectangleG, kRectangleB); -} - -QVector makeSurfacePoints(const QVector3D& center, - double rxDeg, - double ryDeg, - double rzDeg, - float length, - float width, - EulerRotationOrder order) -{ - QVector points; - if (length <= 0.0f || width <= 0.0f) { - return points; - } - - const QMatrix4x4 rotation = buildEulerRotationMatrix(order, rxDeg, ryDeg, rzDeg); - const float halfLength = length * 0.5f; - const float halfWidth = width * 0.5f; - - points.reserve(4); - points.append(transformShapePoint(center, rotation, QVector3D(-halfLength, -halfWidth, 0.0f))); - points.append(transformShapePoint(center, rotation, QVector3D( halfLength, -halfWidth, 0.0f))); - points.append(transformShapePoint(center, rotation, QVector3D( halfLength, halfWidth, 0.0f))); - points.append(transformShapePoint(center, rotation, QVector3D(-halfLength, halfWidth, 0.0f))); - return points; -} - -QVector makeSurfaceQuads(const QVector3D& center, - double rxDeg, - double ryDeg, - double rzDeg, - float length, - float width, - EulerRotationOrder order) -{ - QVector surfaces; - const QVector points = makeSurfacePoints(center, rxDeg, ryDeg, rzDeg, length, width, order); - if (points.size() != 4) { - return surfaces; - } - - surfaces.append(SurfaceQuad(points[0], points[1], points[2], points[3], kSurfaceR, kSurfaceG, kSurfaceB, kSurfaceA)); - return surfaces; -} - -QVector makeSurfaceOutlineSegments(const QVector3D& center, - double rxDeg, - double ryDeg, - double rzDeg, - float length, - float width, - EulerRotationOrder order) -{ - const QVector points = makeSurfacePoints(center, rxDeg, ryDeg, rzDeg, length, width, order); - return makeClosedShapeSegments(points, kSurfaceR, kSurfaceG, kSurfaceB); -} - -void appendLineSegments(QVector& target, const QVector& source) -{ - for (const LineSegment& segment : source) { - target.append(segment); - } -} - -void appendBasicShapePoints(QVector& target, const QVector& source) -{ - for (const BasicShapePoint& point : source) { - target.append(point); - } -} - -void appendSurfaceQuads(QVector& target, const QVector& source) -{ - for (const SurfaceQuad& surface : source) { - target.append(surface); - } -} - -struct NumericRow -{ - int lineNumber; - QVector values; -}; - -QVector parseNumericValuesFromLine(QString line) -{ - const int hashIndex = line.indexOf('#'); - if (hashIndex >= 0) { - line = line.left(hashIndex); - } - const int slashIndex = line.indexOf("//"); - if (slashIndex >= 0) { - line = line.left(slashIndex); - } - - line.replace(QRegExp("[,;:\\{\\}\\[\\]\\(\\)\"]"), " "); - - QVector values; - const QStringList parts = line.split(QRegExp("\\s+"), QString::SkipEmptyParts); - for (const QString& part : parts) { - bool ok = false; - const double value = part.toDouble(&ok); - if (ok) { - values.append(value); - } - } - return values; -} - -bool loadNumericRowsFromFile(const QString& fileName, QVector& rows, QString& errorMessage) -{ - rows.clear(); - - QFile file(fileName); - if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { - errorMessage = QString("无法打开文件: %1").arg(file.errorString()); - return false; - } - - QTextStream in(&file); - int lineNumber = 0; - while (!in.atEnd()) { - ++lineNumber; - const QVector values = parseNumericValuesFromLine(in.readLine()); - if (values.isEmpty()) { - continue; - } - rows.append(NumericRow{lineNumber, values}); - } - - if (rows.isEmpty()) { - errorMessage = "文件中没有可读取的数值"; - return false; - } - - return true; -} - -bool makeCircleSegmentsFromRows(const QVector& rows, - EulerRotationOrder order, - QVector& segments, - QVector& centerPoints, - int& shapeCount, - QString& errorMessage) -{ - constexpr int kRecordSize = 7; - segments.clear(); - centerPoints.clear(); - shapeCount = 0; - - if (rows.isEmpty()) { - errorMessage = "圆数据格式应为每行一个 {x,y,z}-{r,p,y}-{r}"; - return false; - } - - for (const NumericRow& row : rows) { - const QVector& values = row.values; - if (values.size() != kRecordSize) { - errorMessage = QString("第 %1 行圆数据格式应为 {x,y,z}-{r,p,y}-{r},共 7 个数值").arg(row.lineNumber); - return false; - } - if (values[6] <= 0.0) { - errorMessage = QString("第 %1 行圆半径必须大于 0").arg(row.lineNumber); - return false; - } - - const QVector3D center(static_cast(values[0]), - static_cast(values[1]), - static_cast(values[2])); - appendLineSegments(segments, makeCircleSegments(center, - values[3], - values[4], - values[5], - static_cast(values[6]), - order)); - appendBasicShapePoints(centerPoints, makeShapeCenterPoint(center)); - ++shapeCount; - } - - return shapeCount > 0; -} - -bool makeRectangleSegmentsFromRows(const QVector& rows, - EulerRotationOrder order, - QVector& segments, - QVector& centerPoints, - int& shapeCount, - QString& errorMessage) -{ - constexpr int kRecordSize = 8; - segments.clear(); - centerPoints.clear(); - shapeCount = 0; - - if (rows.isEmpty()) { - errorMessage = "矩形数据格式应为每行一个 {x,y,z}-{r,p,y}-{w,h}"; - return false; - } - - for (const NumericRow& row : rows) { - const QVector& values = row.values; - if (values.size() != kRecordSize) { - errorMessage = QString("第 %1 行矩形数据格式应为 {x,y,z}-{r,p,y}-{w,h},共 8 个数值").arg(row.lineNumber); - return false; - } - if (values[6] <= 0.0 || values[7] <= 0.0) { - errorMessage = QString("第 %1 行矩形宽高必须大于 0").arg(row.lineNumber); - return false; - } - - const QVector3D center(static_cast(values[0]), - static_cast(values[1]), - static_cast(values[2])); - appendLineSegments(segments, makeRectangleSegments(center, - values[3], - values[4], - values[5], - static_cast(values[6]), - static_cast(values[7]), - order)); - appendBasicShapePoints(centerPoints, makeShapeCenterPoint(center)); - ++shapeCount; - } - - return shapeCount > 0; -} - -bool makeSurfaceDataFromRows(const QVector& rows, - EulerRotationOrder order, - QVector& surfaces, - QVector& outlines, - int& shapeCount, - QString& errorMessage) -{ - surfaces.clear(); - outlines.clear(); - shapeCount = 0; - - if (rows.isEmpty()) { - errorMessage = "面数据格式应为每行一个 {x,y,z}-{l,w} 或 {x,y,z}-{r,p,y}-{l,w}"; - return false; - } - - for (const NumericRow& row : rows) { - const QVector& values = row.values; - if (values.size() != 5 && values.size() != 8) { - errorMessage = QString("第 %1 行面数据格式应为 {x,y,z}-{l,w} 或 {x,y,z}-{r,p,y}-{l,w}").arg(row.lineNumber); - return false; - } - - const bool hasPose = values.size() == 8; - const int sizeIndex = hasPose ? 6 : 3; - if (values[sizeIndex] <= 0.0 || values[sizeIndex + 1] <= 0.0) { - errorMessage = QString("第 %1 行面长宽必须大于 0").arg(row.lineNumber); - return false; - } - - const QVector3D center(static_cast(values[0]), - static_cast(values[1]), - static_cast(values[2])); - const double rx = hasPose ? values[3] : 0.0; - const double ry = hasPose ? values[4] : 0.0; - const double rz = hasPose ? values[5] : 0.0; - const float length = static_cast(values[sizeIndex]); - const float width = static_cast(values[sizeIndex + 1]); - - appendSurfaceQuads(surfaces, makeSurfaceQuads(center, rx, ry, rz, length, width, order)); - appendLineSegments(outlines, makeSurfaceOutlineSegments(center, rx, ry, rz, length, width, order)); - ++shapeCount; - } - - return shapeCount > 0; -} - -bool makeTriangleSegmentsFromRows(const QVector& rows, - QVector& segments, - int& shapeCount, - QString& errorMessage) -{ - constexpr int kRecordSize = 9; - segments.clear(); - shapeCount = 0; - - if (rows.isEmpty()) { - errorMessage = "三角形数据格式应为每行一个 {x,y,z}-{x,y,z}-{x,y,z}"; - return false; - } - - for (const NumericRow& row : rows) { - const QVector& values = row.values; - if (values.size() != kRecordSize) { - errorMessage = QString("第 %1 行三角形数据格式应为 {x,y,z}-{x,y,z}-{x,y,z},共 9 个数值").arg(row.lineNumber); - return false; - } - - QVector points; - points.reserve(3); - for (int i = 0; i < 3; ++i) { - points.append(QVector3D(static_cast(values[i * 3]), - static_cast(values[i * 3 + 1]), - static_cast(values[i * 3 + 2]))); - } - appendLineSegments(segments, makeClosedShapeSegments(points, 0.2f, 1.0f, 0.2f)); - ++shapeCount; - } - - return shapeCount > 0; -} -} // namespace + } + return segments; +} + +QVector makeShapeCenterPoint(const QVector3D& center) +{ + QVector points; + points.append(BasicShapePoint(center.x(), + center.y(), + center.z(), + kShapeCenterR, + kShapeCenterG, + kShapeCenterB, + kShapeCenterPointSize)); + return points; +} + +QVector makeCircleSegments(const QVector3D& center, + double rxDeg, + double ryDeg, + double rzDeg, + float radius, + EulerRotationOrder order) +{ + QVector segments; + if (radius <= 0.0f) { + return segments; + } + + const QMatrix4x4 rotation = buildEulerRotationMatrix(order, rxDeg, ryDeg, rzDeg); + const int segmentCount = 96; + segments.reserve(segmentCount); + for (int i = 0; i < segmentCount; ++i) { + const float a1 = static_cast(2.0 * kPi * i / segmentCount); + const float a2 = static_cast(2.0 * kPi * (i + 1) / segmentCount); + const QVector3D p1 = transformShapePoint( + center, rotation, QVector3D(radius * std::cos(a1), radius * std::sin(a1), 0.0f)); + const QVector3D p2 = transformShapePoint( + center, rotation, QVector3D(radius * std::cos(a2), radius * std::sin(a2), 0.0f)); + segments.append(LineSegment( + p1.x(), p1.y(), p1.z(), + p2.x(), p2.y(), p2.z(), + kCircleR, kCircleG, kCircleB, 3.0f)); + } + return segments; +} + +QVector makeRectangleSegments(const QVector3D& center, + double rxDeg, + double ryDeg, + double rzDeg, + float width, + float height, + EulerRotationOrder order) +{ + if (width <= 0.0f || height <= 0.0f) { + return {}; + } + + QVector points; + points.reserve(4); + const QMatrix4x4 rotation = buildEulerRotationMatrix(order, rxDeg, ryDeg, rzDeg); + const float halfWidth = width * 0.5f; + const float halfHeight = height * 0.5f; + + points.append(transformShapePoint(center, rotation, QVector3D(-halfWidth, -halfHeight, 0.0f))); + points.append(transformShapePoint(center, rotation, QVector3D( halfWidth, -halfHeight, 0.0f))); + points.append(transformShapePoint(center, rotation, QVector3D( halfWidth, halfHeight, 0.0f))); + points.append(transformShapePoint(center, rotation, QVector3D(-halfWidth, halfHeight, 0.0f))); + return makeClosedShapeSegments(points, kRectangleR, kRectangleG, kRectangleB); +} + +QVector makeSurfacePoints(const QVector3D& center, + double rxDeg, + double ryDeg, + double rzDeg, + float length, + float width, + EulerRotationOrder order) +{ + QVector points; + if (length <= 0.0f || width <= 0.0f) { + return points; + } + + const QMatrix4x4 rotation = buildEulerRotationMatrix(order, rxDeg, ryDeg, rzDeg); + const float halfLength = length * 0.5f; + const float halfWidth = width * 0.5f; + + points.reserve(4); + points.append(transformShapePoint(center, rotation, QVector3D(-halfLength, -halfWidth, 0.0f))); + points.append(transformShapePoint(center, rotation, QVector3D( halfLength, -halfWidth, 0.0f))); + points.append(transformShapePoint(center, rotation, QVector3D( halfLength, halfWidth, 0.0f))); + points.append(transformShapePoint(center, rotation, QVector3D(-halfLength, halfWidth, 0.0f))); + return points; +} + +QVector makeSurfaceQuads(const QVector3D& center, + double rxDeg, + double ryDeg, + double rzDeg, + float length, + float width, + EulerRotationOrder order) +{ + QVector surfaces; + const QVector points = makeSurfacePoints(center, rxDeg, ryDeg, rzDeg, length, width, order); + if (points.size() != 4) { + return surfaces; + } + + surfaces.append(SurfaceQuad(points[0], points[1], points[2], points[3], kSurfaceR, kSurfaceG, kSurfaceB, kSurfaceA)); + return surfaces; +} + +QVector makeSurfaceOutlineSegments(const QVector3D& center, + double rxDeg, + double ryDeg, + double rzDeg, + float length, + float width, + EulerRotationOrder order) +{ + const QVector points = makeSurfacePoints(center, rxDeg, ryDeg, rzDeg, length, width, order); + return makeClosedShapeSegments(points, kSurfaceR, kSurfaceG, kSurfaceB); +} + +void appendLineSegments(QVector& target, const QVector& source) +{ + for (const LineSegment& segment : source) { + target.append(segment); + } +} + +void appendBasicShapePoints(QVector& target, const QVector& source) +{ + for (const BasicShapePoint& point : source) { + target.append(point); + } +} + +void appendSurfaceQuads(QVector& target, const QVector& source) +{ + for (const SurfaceQuad& surface : source) { + target.append(surface); + } +} + +struct NumericRow +{ + int lineNumber; + QVector values; +}; + +QVector parseNumericValuesFromLine(QString line) +{ + const int hashIndex = line.indexOf('#'); + if (hashIndex >= 0) { + line = line.left(hashIndex); + } + const int slashIndex = line.indexOf("//"); + if (slashIndex >= 0) { + line = line.left(slashIndex); + } + + line.replace(QRegExp("[,;:\\{\\}\\[\\]\\(\\)\"]"), " "); + + QVector values; + const QStringList parts = line.split(QRegExp("\\s+"), QString::SkipEmptyParts); + for (const QString& part : parts) { + bool ok = false; + const double value = part.toDouble(&ok); + if (ok) { + values.append(value); + } + } + return values; +} + +bool loadNumericRowsFromFile(const QString& fileName, QVector& rows, QString& errorMessage) +{ + rows.clear(); + + QFile file(fileName); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + errorMessage = QString("无法打开文件: %1").arg(file.errorString()); + return false; + } + + QTextStream in(&file); + int lineNumber = 0; + while (!in.atEnd()) { + ++lineNumber; + const QVector values = parseNumericValuesFromLine(in.readLine()); + if (values.isEmpty()) { + continue; + } + rows.append(NumericRow{lineNumber, values}); + } + + if (rows.isEmpty()) { + errorMessage = "文件中没有可读取的数值"; + return false; + } + + return true; +} + +bool makeCircleSegmentsFromRows(const QVector& rows, + EulerRotationOrder order, + QVector& segments, + QVector& centerPoints, + int& shapeCount, + QString& errorMessage) +{ + constexpr int kRecordSize = 7; + segments.clear(); + centerPoints.clear(); + shapeCount = 0; + + if (rows.isEmpty()) { + errorMessage = "圆数据格式应为每行一个 {x,y,z}-{r,p,y}-{r}"; + return false; + } + + for (const NumericRow& row : rows) { + const QVector& values = row.values; + if (values.size() != kRecordSize) { + errorMessage = QString("第 %1 行圆数据格式应为 {x,y,z}-{r,p,y}-{r},共 7 个数值").arg(row.lineNumber); + return false; + } + if (values[6] <= 0.0) { + errorMessage = QString("第 %1 行圆半径必须大于 0").arg(row.lineNumber); + return false; + } + + const QVector3D center(static_cast(values[0]), + static_cast(values[1]), + static_cast(values[2])); + appendLineSegments(segments, makeCircleSegments(center, + values[3], + values[4], + values[5], + static_cast(values[6]), + order)); + appendBasicShapePoints(centerPoints, makeShapeCenterPoint(center)); + ++shapeCount; + } + + return shapeCount > 0; +} + +bool makeRectangleSegmentsFromRows(const QVector& rows, + EulerRotationOrder order, + QVector& segments, + QVector& centerPoints, + int& shapeCount, + QString& errorMessage) +{ + constexpr int kRecordSize = 8; + segments.clear(); + centerPoints.clear(); + shapeCount = 0; + + if (rows.isEmpty()) { + errorMessage = "矩形数据格式应为每行一个 {x,y,z}-{r,p,y}-{w,h}"; + return false; + } + + for (const NumericRow& row : rows) { + const QVector& values = row.values; + if (values.size() != kRecordSize) { + errorMessage = QString("第 %1 行矩形数据格式应为 {x,y,z}-{r,p,y}-{w,h},共 8 个数值").arg(row.lineNumber); + return false; + } + if (values[6] <= 0.0 || values[7] <= 0.0) { + errorMessage = QString("第 %1 行矩形宽高必须大于 0").arg(row.lineNumber); + return false; + } + + const QVector3D center(static_cast(values[0]), + static_cast(values[1]), + static_cast(values[2])); + appendLineSegments(segments, makeRectangleSegments(center, + values[3], + values[4], + values[5], + static_cast(values[6]), + static_cast(values[7]), + order)); + appendBasicShapePoints(centerPoints, makeShapeCenterPoint(center)); + ++shapeCount; + } + + return shapeCount > 0; +} + +bool makeSurfaceDataFromRows(const QVector& rows, + EulerRotationOrder order, + QVector& surfaces, + QVector& outlines, + int& shapeCount, + QString& errorMessage) +{ + surfaces.clear(); + outlines.clear(); + shapeCount = 0; + + if (rows.isEmpty()) { + errorMessage = "面数据格式应为每行一个 {x,y,z}-{l,w} 或 {x,y,z}-{r,p,y}-{l,w}"; + return false; + } + + for (const NumericRow& row : rows) { + const QVector& values = row.values; + if (values.size() != 5 && values.size() != 8) { + errorMessage = QString("第 %1 行面数据格式应为 {x,y,z}-{l,w} 或 {x,y,z}-{r,p,y}-{l,w}").arg(row.lineNumber); + return false; + } + + const bool hasPose = values.size() == 8; + const int sizeIndex = hasPose ? 6 : 3; + if (values[sizeIndex] <= 0.0 || values[sizeIndex + 1] <= 0.0) { + errorMessage = QString("第 %1 行面长宽必须大于 0").arg(row.lineNumber); + return false; + } + + const QVector3D center(static_cast(values[0]), + static_cast(values[1]), + static_cast(values[2])); + const double rx = hasPose ? values[3] : 0.0; + const double ry = hasPose ? values[4] : 0.0; + const double rz = hasPose ? values[5] : 0.0; + const float length = static_cast(values[sizeIndex]); + const float width = static_cast(values[sizeIndex + 1]); + + appendSurfaceQuads(surfaces, makeSurfaceQuads(center, rx, ry, rz, length, width, order)); + appendLineSegments(outlines, makeSurfaceOutlineSegments(center, rx, ry, rz, length, width, order)); + ++shapeCount; + } + + return shapeCount > 0; +} + +bool makeTriangleSegmentsFromRows(const QVector& rows, + QVector& segments, + int& shapeCount, + QString& errorMessage) +{ + constexpr int kRecordSize = 9; + segments.clear(); + shapeCount = 0; + + if (rows.isEmpty()) { + errorMessage = "三角形数据格式应为每行一个 {x,y,z}-{x,y,z}-{x,y,z}"; + return false; + } + + for (const NumericRow& row : rows) { + const QVector& values = row.values; + if (values.size() != kRecordSize) { + errorMessage = QString("第 %1 行三角形数据格式应为 {x,y,z}-{x,y,z}-{x,y,z},共 9 个数值").arg(row.lineNumber); + return false; + } + + QVector points; + points.reserve(3); + for (int i = 0; i < 3; ++i) { + points.append(QVector3D(static_cast(values[i * 3]), + static_cast(values[i * 3 + 1]), + static_cast(values[i * 3 + 2]))); + } + appendLineSegments(segments, makeClosedShapeSegments(points, 0.2f, 1.0f, 0.2f)); + ++shapeCount; + } + + return shapeCount > 0; +} +} // namespace CloudViewMainWindow::CloudViewMainWindow(QWidget* parent) - : QMainWindow(parent) - , m_glWidget(nullptr) - , m_converter(std::make_unique()) - , m_lblDeltaXYZ(nullptr) - , m_cloudCount(0) + : QMainWindow(parent) + , m_glWidget(nullptr) + , m_converter(std::make_unique()) + , m_lblDeltaXYZ(nullptr) + , m_cloudCount(0) , m_currentLineNum(0) - , m_currentLinePtNum(0) - , m_linePointsDialog(nullptr) - , m_linePointsTable(nullptr) - , m_startupSequenceIndex(-1) - , m_startupSequentialMode(false) -{ + , m_currentLinePtNum(0) + , m_linePointsDialog(nullptr) + , m_linePointsTable(nullptr) + , m_startupSequenceIndex(-1) + , m_startupSequentialMode(false) +{ setupUI(); LOG_INFO("CloudViewMainWindow initialized\n"); } -CloudViewMainWindow::~CloudViewMainWindow() -{ -} - -void CloudViewMainWindow::buildStartupSequence(const StartupFiles& files) -{ - m_startupSequence.clear(); - m_startupSequenceIndex = -1; - m_startupSequentialMode = false; - - auto appendFiles = [this](StartupFileType type, const QStringList& fileNames) { - for (const QString& fileName : fileNames) { - m_startupSequence.append(StartupItem{type, fileName}); - } - }; - - appendFiles(StartupFileType::Display, files.displayFiles); - appendFiles(StartupFileType::BBox, files.bboxFiles); - appendFiles(StartupFileType::Circle, files.circleFiles); - appendFiles(StartupFileType::Rectangle, files.rectangleFiles); - appendFiles(StartupFileType::Surface, files.surfaceFiles); - appendFiles(StartupFileType::Triangle, files.triangleFiles); -} - -bool CloudViewMainWindow::loadStartupItem(const StartupItem& item) -{ - switch (item.type) { - case StartupFileType::Display: - return loadDisplayFile(item.fileName); - case StartupFileType::BBox: - return loadBoundingBoxFile(item.fileName); - case StartupFileType::Circle: - return loadCircleShapeFile(item.fileName); - case StartupFileType::Rectangle: - return loadRectangleShapeFile(item.fileName); - case StartupFileType::Surface: - return loadSurfaceShapeFile(item.fileName); - case StartupFileType::Triangle: - return loadTriangleShapeFile(item.fileName); - } - - return false; -} - -bool CloudViewMainWindow::loadCurrentStartupSequenceItem() -{ - if (m_startupSequenceIndex < 0 || m_startupSequenceIndex >= m_startupSequence.size()) { - return false; - } - - const StartupItem& item = m_startupSequence[m_startupSequenceIndex]; - const bool ok = loadStartupItem(item); - if (ok) { - fitViewToContent(); - } else { - LOG_WARN("[CloudView] Startup sequence load failed: %s\n", item.fileName.toStdString().c_str()); - } - - statusBar()->showMessage(QString("序列显示 %1/%2:%3%4") - .arg(m_startupSequenceIndex + 1) - .arg(m_startupSequence.size()) - .arg(QFileInfo(item.fileName).fileName()) - .arg(ok ? "" : " 加载失败")); - return ok; -} - -bool CloudViewMainWindow::loadStartupFiles(const StartupFiles& files) -{ - if (files.sequential) { - buildStartupSequence(files); - if (m_startupSequence.isEmpty()) { - return true; - } - - m_startupSequentialMode = true; - m_startupSequenceIndex = 0; - return loadCurrentStartupSequenceItem(); - } - - const int requestedCount = - files.displayFiles.size() + - files.bboxFiles.size() + - files.circleFiles.size() + - files.rectangleFiles.size() + - files.surfaceFiles.size() + - files.triangleFiles.size(); - - if (requestedCount == 0) { - return true; - } - - int loadedCount = 0; - QStringList failedFiles; - - auto recordLoadResult = [&](const QString& fileName, bool ok) { - if (ok) { - ++loadedCount; - } else { - failedFiles.append(fileName); - } - }; - - for (const QString& fileName : files.displayFiles) { - recordLoadResult(fileName, loadDisplayFile(fileName)); - } - for (const QString& fileName : files.bboxFiles) { - recordLoadResult(fileName, loadBoundingBoxFile(fileName)); - } - for (const QString& fileName : files.circleFiles) { - recordLoadResult(fileName, loadCircleShapeFile(fileName)); - } - for (const QString& fileName : files.rectangleFiles) { - recordLoadResult(fileName, loadRectangleShapeFile(fileName)); - } - for (const QString& fileName : files.surfaceFiles) { - recordLoadResult(fileName, loadSurfaceShapeFile(fileName)); - } - for (const QString& fileName : files.triangleFiles) { - recordLoadResult(fileName, loadTriangleShapeFile(fileName)); - } - - if (loadedCount > 0) { - fitViewToContent(); - } - - if (!failedFiles.isEmpty()) { - for (const QString& fileName : failedFiles) { - LOG_WARN("[CloudView] Startup load failed: %s\n", fileName.toStdString().c_str()); - } - statusBar()->showMessage(QString("启动参数加载完成:%1/%2 成功,%3 失败") - .arg(loadedCount) - .arg(requestedCount) - .arg(failedFiles.size())); - return false; - } - - statusBar()->showMessage(QString("启动参数加载完成:%1 个文件").arg(loadedCount)); - return true; -} - -bool CloudViewMainWindow::loadDisplayFile(const QString& fileName) -{ - // 同一文件中可能同时包含点云和线段,两者都尝试加载。 - const bool cloudOk = loadPointCloudFile(fileName); - const bool segmentOk = loadSegmentFile(fileName); - - if (!cloudOk && !segmentOk) { - statusBar()->showMessage(QString("加载失败:%1").arg(QFileInfo(fileName).fileName())); - LOG_WARN("[CloudView] Failed to load display file: %s\n", fileName.toStdString().c_str()); - return false; - } - - return true; -} - -bool CloudViewMainWindow::loadCircleShapeFile(const QString& fileName) -{ - QVector rows; - QString errorMessage; - if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { - statusBar()->showMessage(QString("加载圆数据失败:%1").arg(errorMessage)); - LOG_WARN("[CloudView] Load circle file failed: %s, %s\n", - fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); - return false; - } - - QVector segments; - QVector centerPoints; - int shapeCount = 0; - if (!makeCircleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, centerPoints, shapeCount, errorMessage)) { - statusBar()->showMessage(QString("加载圆数据失败:%1").arg(errorMessage)); - LOG_WARN("[CloudView] Parse circle file failed: %s, %s\n", - fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); - return false; - } - - showBasicShape(QString("%1 个圆").arg(shapeCount), segments, centerPoints); - m_cloudList->addItem(QString("Circle (%1) - %2 个").arg(QFileInfo(fileName).fileName()).arg(shapeCount)); - return true; -} - -bool CloudViewMainWindow::loadRectangleShapeFile(const QString& fileName) -{ - QVector rows; - QString errorMessage; - if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { - statusBar()->showMessage(QString("加载矩形数据失败:%1").arg(errorMessage)); - LOG_WARN("[CloudView] Load rectangle file failed: %s, %s\n", - fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); - return false; - } - - QVector segments; - QVector centerPoints; - int shapeCount = 0; - if (!makeRectangleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, centerPoints, shapeCount, errorMessage)) { - statusBar()->showMessage(QString("加载矩形数据失败:%1").arg(errorMessage)); - LOG_WARN("[CloudView] Parse rectangle file failed: %s, %s\n", - fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); - return false; - } - - showBasicShape(QString("%1 个矩形").arg(shapeCount), segments, centerPoints); - m_cloudList->addItem(QString("Rectangle (%1) - %2 个").arg(QFileInfo(fileName).fileName()).arg(shapeCount)); - return true; -} - -bool CloudViewMainWindow::loadSurfaceShapeFile(const QString& fileName) -{ - QVector rows; - QString errorMessage; - if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { - statusBar()->showMessage(QString("加载面数据失败:%1").arg(errorMessage)); - LOG_WARN("[CloudView] Load surface file failed: %s, %s\n", - fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); - return false; - } - - QVector surfaces; - QVector outlines; - int shapeCount = 0; - if (!makeSurfaceDataFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), surfaces, outlines, shapeCount, errorMessage)) { - statusBar()->showMessage(QString("加载面数据失败:%1").arg(errorMessage)); - LOG_WARN("[CloudView] Parse surface file failed: %s, %s\n", - fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); - return false; - } - - showBasicSurface(QString("%1 个面").arg(shapeCount), surfaces, outlines); - m_cloudList->addItem(QString("Surface (%1) - %2 个").arg(QFileInfo(fileName).fileName()).arg(shapeCount)); - return true; -} - -bool CloudViewMainWindow::loadTriangleShapeFile(const QString& fileName) -{ - QVector rows; - QString errorMessage; - if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { - statusBar()->showMessage(QString("加载三角形数据失败:%1").arg(errorMessage)); - LOG_WARN("[CloudView] Load triangle file failed: %s, %s\n", - fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); - return false; - } - - QVector segments; - int shapeCount = 0; - if (!makeTriangleSegmentsFromRows(rows, segments, shapeCount, errorMessage)) { - statusBar()->showMessage(QString("加载三角形数据失败:%1").arg(errorMessage)); - LOG_WARN("[CloudView] Parse triangle file failed: %s, %s\n", - fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); - return false; - } - - showBasicShape(QString("%1 个三角形").arg(shapeCount), segments); - m_cloudList->addItem(QString("Triangle (%1) - %2 个").arg(QFileInfo(fileName).fileName()).arg(shapeCount)); - return true; -} - -void CloudViewMainWindow::fitViewToContent() -{ - if (m_glWidget) { - m_glWidget->fitToContent(); - } -} - -bool CloudViewMainWindow::eventFilter(QObject* obj, QEvent* event) -{ - if (obj == m_glWidget && event->type() == QEvent::Resize) { - auto* resizeEvt = static_cast(event); - int w = resizeEvt->size().width(); - int h = resizeEvt->size().height(); - int btnW = m_btnZoomIn->width(); - int btnH = m_btnZoomIn->height(); - int margin = 8; - int gap = 4; - int x = w - btnW - margin; - int y = (h - btnH * 2 - gap) / 2; - m_btnZoomIn->move(x, y); - m_btnZoomOut->move(x, y + btnH + gap); - } - return QMainWindow::eventFilter(obj, event); -} - -void CloudViewMainWindow::closeEvent(QCloseEvent* event) -{ - if (m_startupSequentialMode && m_startupSequenceIndex + 1 < m_startupSequence.size()) { - event->ignore(); - ++m_startupSequenceIndex; - onClearAll(); - loadCurrentStartupSequenceItem(); - return; - } - - m_startupSequentialMode = false; - QMainWindow::closeEvent(event); -} - -void CloudViewMainWindow::setupUI() -{ +CloudViewMainWindow::~CloudViewMainWindow() +{ +} + +void CloudViewMainWindow::buildStartupSequence(const StartupFiles& files) +{ + m_startupSequence.clear(); + m_startupSequenceIndex = -1; + m_startupSequentialMode = false; + + auto appendFiles = [this](StartupFileType type, const QStringList& fileNames) { + for (const QString& fileName : fileNames) { + m_startupSequence.append(StartupItem{type, fileName}); + } + }; + + appendFiles(StartupFileType::Display, files.displayFiles); + appendFiles(StartupFileType::BBox, files.bboxFiles); + appendFiles(StartupFileType::Circle, files.circleFiles); + appendFiles(StartupFileType::Rectangle, files.rectangleFiles); + appendFiles(StartupFileType::Surface, files.surfaceFiles); + appendFiles(StartupFileType::Triangle, files.triangleFiles); +} + +bool CloudViewMainWindow::loadStartupItem(const StartupItem& item) +{ + switch (item.type) { + case StartupFileType::Display: + return loadDisplayFile(item.fileName); + case StartupFileType::BBox: + return loadBoundingBoxFile(item.fileName); + case StartupFileType::Circle: + return loadCircleShapeFile(item.fileName); + case StartupFileType::Rectangle: + return loadRectangleShapeFile(item.fileName); + case StartupFileType::Surface: + return loadSurfaceShapeFile(item.fileName); + case StartupFileType::Triangle: + return loadTriangleShapeFile(item.fileName); + } + + return false; +} + +bool CloudViewMainWindow::loadCurrentStartupSequenceItem() +{ + if (m_startupSequenceIndex < 0 || m_startupSequenceIndex >= m_startupSequence.size()) { + return false; + } + + const StartupItem& item = m_startupSequence[m_startupSequenceIndex]; + const bool ok = loadStartupItem(item); + if (ok) { + fitViewToContent(); + } else { + LOG_WARN("[CloudView] Startup sequence load failed: %s\n", item.fileName.toStdString().c_str()); + } + + statusBar()->showMessage(QString("序列显示 %1/%2:%3%4") + .arg(m_startupSequenceIndex + 1) + .arg(m_startupSequence.size()) + .arg(QFileInfo(item.fileName).fileName()) + .arg(ok ? "" : " 加载失败")); + return ok; +} + +bool CloudViewMainWindow::loadStartupFiles(const StartupFiles& files) +{ + if (files.sequential) { + buildStartupSequence(files); + if (m_startupSequence.isEmpty()) { + return true; + } + + m_startupSequentialMode = true; + m_startupSequenceIndex = 0; + return loadCurrentStartupSequenceItem(); + } + + const int requestedCount = + files.displayFiles.size() + + files.bboxFiles.size() + + files.circleFiles.size() + + files.rectangleFiles.size() + + files.surfaceFiles.size() + + files.triangleFiles.size(); + + if (requestedCount == 0) { + return true; + } + + int loadedCount = 0; + QStringList failedFiles; + + auto recordLoadResult = [&](const QString& fileName, bool ok) { + if (ok) { + ++loadedCount; + } else { + failedFiles.append(fileName); + } + }; + + for (const QString& fileName : files.displayFiles) { + recordLoadResult(fileName, loadDisplayFile(fileName)); + } + for (const QString& fileName : files.bboxFiles) { + recordLoadResult(fileName, loadBoundingBoxFile(fileName)); + } + for (const QString& fileName : files.circleFiles) { + recordLoadResult(fileName, loadCircleShapeFile(fileName)); + } + for (const QString& fileName : files.rectangleFiles) { + recordLoadResult(fileName, loadRectangleShapeFile(fileName)); + } + for (const QString& fileName : files.surfaceFiles) { + recordLoadResult(fileName, loadSurfaceShapeFile(fileName)); + } + for (const QString& fileName : files.triangleFiles) { + recordLoadResult(fileName, loadTriangleShapeFile(fileName)); + } + + if (loadedCount > 0) { + fitViewToContent(); + } + + if (!failedFiles.isEmpty()) { + for (const QString& fileName : failedFiles) { + LOG_WARN("[CloudView] Startup load failed: %s\n", fileName.toStdString().c_str()); + } + statusBar()->showMessage(QString("启动参数加载完成:%1/%2 成功,%3 失败") + .arg(loadedCount) + .arg(requestedCount) + .arg(failedFiles.size())); + return false; + } + + statusBar()->showMessage(QString("启动参数加载完成:%1 个文件").arg(loadedCount)); + return true; +} + +bool CloudViewMainWindow::loadDisplayFile(const QString& fileName) +{ + // 同一文件中可能同时包含点云和线段,两者都尝试加载。 + const bool cloudOk = loadPointCloudFile(fileName); + const bool segmentOk = loadSegmentFile(fileName); + + if (!cloudOk && !segmentOk) { + statusBar()->showMessage(QString("加载失败:%1").arg(QFileInfo(fileName).fileName())); + LOG_WARN("[CloudView] Failed to load display file: %s\n", fileName.toStdString().c_str()); + return false; + } + + return true; +} + +bool CloudViewMainWindow::loadCircleShapeFile(const QString& fileName) +{ + QVector rows; + QString errorMessage; + if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { + statusBar()->showMessage(QString("加载圆数据失败:%1").arg(errorMessage)); + LOG_WARN("[CloudView] Load circle file failed: %s, %s\n", + fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); + return false; + } + + QVector segments; + QVector centerPoints; + int shapeCount = 0; + if (!makeCircleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, centerPoints, shapeCount, errorMessage)) { + statusBar()->showMessage(QString("加载圆数据失败:%1").arg(errorMessage)); + LOG_WARN("[CloudView] Parse circle file failed: %s, %s\n", + fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); + return false; + } + + showBasicShape(QString("%1 个圆").arg(shapeCount), segments, centerPoints); + m_cloudList->addItem(QString("Circle (%1) - %2 个").arg(QFileInfo(fileName).fileName()).arg(shapeCount)); + return true; +} + +bool CloudViewMainWindow::loadRectangleShapeFile(const QString& fileName) +{ + QVector rows; + QString errorMessage; + if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { + statusBar()->showMessage(QString("加载矩形数据失败:%1").arg(errorMessage)); + LOG_WARN("[CloudView] Load rectangle file failed: %s, %s\n", + fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); + return false; + } + + QVector segments; + QVector centerPoints; + int shapeCount = 0; + if (!makeRectangleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, centerPoints, shapeCount, errorMessage)) { + statusBar()->showMessage(QString("加载矩形数据失败:%1").arg(errorMessage)); + LOG_WARN("[CloudView] Parse rectangle file failed: %s, %s\n", + fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); + return false; + } + + showBasicShape(QString("%1 个矩形").arg(shapeCount), segments, centerPoints); + m_cloudList->addItem(QString("Rectangle (%1) - %2 个").arg(QFileInfo(fileName).fileName()).arg(shapeCount)); + return true; +} + +bool CloudViewMainWindow::loadSurfaceShapeFile(const QString& fileName) +{ + QVector rows; + QString errorMessage; + if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { + statusBar()->showMessage(QString("加载面数据失败:%1").arg(errorMessage)); + LOG_WARN("[CloudView] Load surface file failed: %s, %s\n", + fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); + return false; + } + + QVector surfaces; + QVector outlines; + int shapeCount = 0; + if (!makeSurfaceDataFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), surfaces, outlines, shapeCount, errorMessage)) { + statusBar()->showMessage(QString("加载面数据失败:%1").arg(errorMessage)); + LOG_WARN("[CloudView] Parse surface file failed: %s, %s\n", + fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); + return false; + } + + showBasicSurface(QString("%1 个面").arg(shapeCount), surfaces, outlines); + m_cloudList->addItem(QString("Surface (%1) - %2 个").arg(QFileInfo(fileName).fileName()).arg(shapeCount)); + return true; +} + +bool CloudViewMainWindow::loadTriangleShapeFile(const QString& fileName) +{ + QVector rows; + QString errorMessage; + if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { + statusBar()->showMessage(QString("加载三角形数据失败:%1").arg(errorMessage)); + LOG_WARN("[CloudView] Load triangle file failed: %s, %s\n", + fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); + return false; + } + + QVector segments; + int shapeCount = 0; + if (!makeTriangleSegmentsFromRows(rows, segments, shapeCount, errorMessage)) { + statusBar()->showMessage(QString("加载三角形数据失败:%1").arg(errorMessage)); + LOG_WARN("[CloudView] Parse triangle file failed: %s, %s\n", + fileName.toStdString().c_str(), errorMessage.toStdString().c_str()); + return false; + } + + showBasicShape(QString("%1 个三角形").arg(shapeCount), segments); + m_cloudList->addItem(QString("Triangle (%1) - %2 个").arg(QFileInfo(fileName).fileName()).arg(shapeCount)); + return true; +} + +void CloudViewMainWindow::fitViewToContent() +{ + if (m_glWidget) { + m_glWidget->fitToContent(); + } +} + +bool CloudViewMainWindow::eventFilter(QObject* obj, QEvent* event) +{ + // 缩放按钮已移至 CloudView3D,事件过滤器不再需要处理缩放按钮定位 + return QMainWindow::eventFilter(obj, event); +} + +void CloudViewMainWindow::closeEvent(QCloseEvent* event) +{ + if (m_startupSequentialMode && m_startupSequenceIndex + 1 < m_startupSequence.size()) { + event->ignore(); + ++m_startupSequenceIndex; + onClearAll(); + loadCurrentStartupSequenceItem(); + return; + } + + m_startupSequentialMode = false; + QMainWindow::closeEvent(event); +} + +void CloudViewMainWindow::setupUI() +{ // ── 文件菜单 ── QMenu* fileMenu = menuBar()->addMenu("文件"); QAction* actOpenFile = fileMenu->addAction("打开...\tCtrl+O"); @@ -1176,18 +1164,18 @@ void CloudViewMainWindow::setupUI() QAction* actPoseComp = toolMenu->addAction("姿态补偿计算..."); connect(actPoseComp, &QAction::triggered, this, &CloudViewMainWindow::onPoseCompensation); - QMenu* imageToolMenu = menuBar()->addMenu("图形工具"); + QMenu* imageToolMenu = menuBar()->addMenu("图形工具"); QAction* actOpenPosePoint = imageToolMenu->addAction("点(带姿态)"); connect(actOpenPosePoint, &QAction::triggered, this, &CloudViewMainWindow::onOpenPoseFile); imageToolMenu->addSeparator(); QAction* actCircleShape = imageToolMenu->addAction("圆"); connect(actCircleShape, &QAction::triggered, this, &CloudViewMainWindow::onShowCircleShape); - QAction* actRectangleShape = imageToolMenu->addAction("矩形"); - connect(actRectangleShape, &QAction::triggered, this, &CloudViewMainWindow::onShowRectangleShape); - QAction* actSurfaceShape = imageToolMenu->addAction("面"); - connect(actSurfaceShape, &QAction::triggered, this, &CloudViewMainWindow::onShowSurfaceShape); - QAction* actTriangleShape = imageToolMenu->addAction("三角形"); - connect(actTriangleShape, &QAction::triggered, this, &CloudViewMainWindow::onShowTriangleShape); + QAction* actRectangleShape = imageToolMenu->addAction("矩形"); + connect(actRectangleShape, &QAction::triggered, this, &CloudViewMainWindow::onShowRectangleShape); + QAction* actSurfaceShape = imageToolMenu->addAction("面"); + connect(actSurfaceShape, &QAction::triggered, this, &CloudViewMainWindow::onShowSurfaceShape); + QAction* actTriangleShape = imageToolMenu->addAction("三角形"); + connect(actTriangleShape, &QAction::triggered, this, &CloudViewMainWindow::onShowTriangleShape); QMenu* transformMenu = menuBar()->addMenu("矩阵变换"); QAction* actEyeToHand = transformMenu->addAction("眼在手外..."); @@ -1206,16 +1194,12 @@ void CloudViewMainWindow::setupUI() // 创建分割器 QSplitter* splitter = new QSplitter(Qt::Horizontal, centralWidget); - // 左侧:视图工具栏 + 点云显示区域 + // 左侧:点云显示区域(视图工具栏已移至 CloudView3D) QWidget* viewerContainer = new QWidget(splitter); QHBoxLayout* viewerLayout = new QHBoxLayout(viewerContainer); viewerLayout->setContentsMargins(0, 0, 0, 0); viewerLayout->setSpacing(2); - // 视图方向工具栏(纵向排列,在3D视图左侧) - QWidget* viewToolbar = createViewToolbar(); - viewerLayout->addWidget(viewToolbar); - // 点云显示区域 QWidget* viewerArea = createViewerArea(); viewerLayout->addWidget(viewerArea); @@ -1246,50 +1230,12 @@ QWidget* CloudViewMainWindow::createViewerArea() m_glWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); layout->addWidget(m_glWidget); - // 悬浮缩放按钮(父控件为 m_glWidget,悬浮在右侧中间) - int zoomBtnSize = 28; - QString zoomBtnStyle = - "QToolButton {" - " background-color: rgba(50, 50, 50, 180);" - " color: white;" - " border: 1px solid rgba(120, 120, 120, 150);" - " border-radius: 4px;" - " font-size: 32px;" - " font-weight: bold;" - "}" - "QToolButton:hover {" - " background-color: rgba(80, 80, 80, 220);" - "}" - "QToolButton:pressed {" - " background-color: rgba(30, 30, 30, 240);" - "}"; - - m_btnZoomIn = new QToolButton(m_glWidget); - m_btnZoomIn->setText("+"); - m_btnZoomIn->setFixedSize(zoomBtnSize, zoomBtnSize); - m_btnZoomIn->setStyleSheet(zoomBtnStyle); - m_btnZoomIn->setAutoRepeat(true); - m_btnZoomIn->setAutoRepeatDelay(300); - m_btnZoomIn->setAutoRepeatInterval(50); - connect(m_btnZoomIn, &QToolButton::clicked, m_glWidget, &PointCloudGLWidget::zoomIn); - - m_btnZoomOut = new QToolButton(m_glWidget); - m_btnZoomOut->setText("-"); - m_btnZoomOut->setFixedSize(zoomBtnSize, zoomBtnSize); - m_btnZoomOut->setStyleSheet(zoomBtnStyle); - m_btnZoomOut->setAutoRepeat(true); - m_btnZoomOut->setAutoRepeatDelay(300); - m_btnZoomOut->setAutoRepeatInterval(50); - connect(m_btnZoomOut, &QToolButton::clicked, m_glWidget, &PointCloudGLWidget::zoomOut); - - // 安装事件过滤器,在 GL 控件 resize 时更新按钮位置 - m_glWidget->installEventFilter(this); + // 缩放按钮 / 旋转角度显示已移至 CloudView3D,此处不再创建 // 连接信号 connect(m_glWidget, &PointCloudGLWidget::pointSelected, this, &CloudViewMainWindow::onPointSelected); connect(m_glWidget, &PointCloudGLWidget::twoPointsSelected, this, &CloudViewMainWindow::onTwoPointsSelected); connect(m_glWidget, &PointCloudGLWidget::lineSelected, this, &CloudViewMainWindow::onLineSelected); - connect(m_glWidget, &PointCloudGLWidget::viewAnglesChanged, this, &CloudViewMainWindow::onViewAnglesChanged); return widget; } @@ -1349,105 +1295,6 @@ QGroupBox* CloudViewMainWindow::createFileGroup() return group; } -QWidget* CloudViewMainWindow::createViewToolbar() -{ - QWidget* widget = new QWidget(this); - QVBoxLayout* layout = new QVBoxLayout(widget); - layout->setContentsMargins(2, 2, 2, 2); - layout->setSpacing(3); - - // 视图预设:图标、提示、旋转角度 - struct ViewPreset { - const char* iconPath; - const char* tooltip; - float rotX; - float rotY; - float rotZ; - }; - - // 坐标系定义:X向右,Y向下,Z朝后 - ViewPreset presets[] = { - {":/common/resource/view_front.png", "正视 (XY面)", 180.0f, 0.0f, 0.0f}, - {":/common/resource/view_back.png", "后视", 180.0f, 180.0f, 0.0f}, - {":/common/resource/view_left.png", "左视 (YZ面)", 180.0f, 90.0f, 0.0f}, - {":/common/resource/view_right.png", "右视", 180.0f, -90.0f, 0.0f}, - {":/common/resource/view_top.png", "俯视 (XZ面)", 90.0f, 0.0f, 0.0f}, - {":/common/resource/view_bottom.png", "仰视", -90.0f, 0.0f, 0.0f}, - {":/common/resource/view_robot.png", "机械臂", -90.0f, 90.0f, 0.0f}, - }; - - int btnSize = 32; - int iconSize = 24; - - for (const auto& preset : presets) { - QToolButton* btn = new QToolButton(widget); - btn->setIcon(QIcon(preset.iconPath)); - btn->setIconSize(QSize(iconSize, iconSize)); - btn->setFixedSize(btnSize, btnSize); - btn->setToolTip(preset.tooltip); - btn->setAutoRaise(true); - - float rx = preset.rotX; - float ry = preset.rotY; - float rz = preset.rotZ; - connect(btn, &QToolButton::clicked, this, [this, rx, ry, rz]() { - m_glWidget->setViewAngles(rx, ry, rz); - m_editRotX->setText(QString::number(rx, 'f', 1)); - m_editRotY->setText(QString::number(ry, 'f', 1)); - m_editRotZ->setText(QString::number(rz, 'f', 1)); - }); - layout->addWidget(btn); - } - - layout->addStretch(); - - // 旋转角度输入(纵向紧凑布局) - m_editRotX = new QLineEdit("180.0", widget); - m_editRotY = new QLineEdit("0.0", widget); - m_editRotZ = new QLineEdit("0.0", widget); - - auto makeAngleRow = [&](const QString& label, QLineEdit* edit) { - QWidget* row = new QWidget(widget); - QVBoxLayout* rowLayout = new QVBoxLayout(row); - rowLayout->setContentsMargins(0, 0, 0, 0); - rowLayout->setSpacing(0); - QLabel* lbl = new QLabel(label, row); - lbl->setStyleSheet("font-size: 10px; color: gray;"); - rowLayout->addWidget(lbl); - edit->setFixedWidth(btnSize); - edit->setMaximumHeight(20); - edit->setStyleSheet("font-size: 10px;"); - rowLayout->addWidget(edit); - return row; - }; - - layout->addWidget(makeAngleRow("RX", m_editRotX)); - layout->addWidget(makeAngleRow("RY", m_editRotY)); - layout->addWidget(makeAngleRow("RZ", m_editRotZ)); - - // 应用按钮 - QPushButton* btnApply = new QPushButton("GO", widget); - btnApply->setFixedSize(btnSize, 20); - btnApply->setStyleSheet("font-size: 10px;"); - auto applyAngles = [this]() { - bool okX, okY, okZ; - float rotX = m_editRotX->text().toFloat(&okX); - float rotY = m_editRotY->text().toFloat(&okY); - float rotZ = m_editRotZ->text().toFloat(&okZ); - if (okX && okY && okZ) { - m_glWidget->setViewAngles(rotX, rotY, rotZ); - } - }; - connect(btnApply, &QPushButton::clicked, this, applyAngles); - connect(m_editRotX, &QLineEdit::returnPressed, this, applyAngles); - connect(m_editRotY, &QLineEdit::returnPressed, this, applyAngles); - connect(m_editRotZ, &QLineEdit::returnPressed, this, applyAngles); - layout->addWidget(btnApply); - - widget->setFixedWidth(btnSize + 8); - return widget; -} - QWidget* CloudViewMainWindow::createMeasurePage() { QWidget* page = new QWidget(this); @@ -1565,7 +1412,7 @@ QGroupBox* CloudViewMainWindow::createMeasureGroup() m_glWidget->clearSelectedPoints(); m_lblPoint1->setText("点1: --"); m_lblPoint2->setText("点2: --"); - clearMeasureResultDisplay(); + clearMeasureResultDisplay(); m_editPoint1X->setText("--"); m_editPoint1Y->setText("--"); m_editPoint1Z->setText("--"); @@ -1767,21 +1614,21 @@ QGroupBox* CloudViewMainWindow::createMeasureGroup() lblDistTitle->setStyleSheet("font-size: 10px;"); m_lblDistance = new QLabel("--", group); m_lblDistance->setStyleSheet("font-weight: bold; color: green; font-size: 20px;"); - distLayout->addWidget(lblDistTitle); - distLayout->addWidget(m_lblDistance, 1); - layout->addLayout(distLayout); - - QHBoxLayout* deltaLayout = new QHBoxLayout(); - QLabel* lblDeltaTitle = new QLabel("差值:", group); - lblDeltaTitle->setStyleSheet("font-size: 10px;"); - m_lblDeltaXYZ = new QLabel("--", group); - m_lblDeltaXYZ->setStyleSheet("font-weight: bold; color: #1f6feb; font-size: 13px;"); - deltaLayout->addWidget(lblDeltaTitle); - deltaLayout->addWidget(m_lblDeltaXYZ, 1); - layout->addLayout(deltaLayout); - - return group; -} + distLayout->addWidget(lblDistTitle); + distLayout->addWidget(m_lblDistance, 1); + layout->addLayout(distLayout); + + QHBoxLayout* deltaLayout = new QHBoxLayout(); + QLabel* lblDeltaTitle = new QLabel("差值:", group); + lblDeltaTitle->setStyleSheet("font-size: 10px;"); + m_lblDeltaXYZ = new QLabel("--", group); + m_lblDeltaXYZ->setStyleSheet("font-weight: bold; color: #1f6feb; font-size: 13px;"); + deltaLayout->addWidget(lblDeltaTitle); + deltaLayout->addWidget(m_lblDeltaXYZ, 1); + layout->addLayout(deltaLayout); + + return group; +} QGroupBox* CloudViewMainWindow::createLineGroup() { @@ -1900,11 +1747,11 @@ void CloudViewMainWindow::onOpenFile() return; } - if (!loadDisplayFile(fileName)) { - QMessageBox::critical(this, "错误", "无法从文件中加载点云或线段数据"); - statusBar()->showMessage("加载失败"); - } -} + if (!loadDisplayFile(fileName)) { + QMessageBox::critical(this, "错误", "无法从文件中加载点云或线段数据"); + statusBar()->showMessage("加载失败"); + } +} bool CloudViewMainWindow::loadPointCloudFile(const QString& fileName) { @@ -2330,7 +2177,7 @@ void CloudViewMainWindow::applyTransformToAllClouds(const QMatrix4x4& matrix) m_lblPoint1->setText("点1: --"); m_lblPoint2->setText("点2: --"); - clearMeasureResultDisplay(); + clearMeasureResultDisplay(); m_editPoint1X->setText("--"); m_editPoint1Y->setText("--"); m_editPoint1Z->setText("--"); @@ -2365,55 +2212,57 @@ void CloudViewMainWindow::addGeneratedCloud(const PointCloudXYZ& cloud, const QS cloudName.toStdString().c_str(), cloud.size()); } -void CloudViewMainWindow::showBasicShape(const QString& shapeName, - const QVector& segments, - const QVector& points) -{ - if (segments.isEmpty() && points.isEmpty()) { - return; - } - - if (!segments.isEmpty()) { - m_glWidget->appendBasicShapeSegments(segments); - } - if (!points.isEmpty()) { - m_glWidget->appendBasicShapePoints(points); - } - statusBar()->showMessage(QString("已添加 %1").arg(shapeName)); - LOG_INFO("[CloudView] Add basic shape: %s, segments=%d, points=%d\n", - shapeName.toStdString().c_str(), segments.size(), points.size()); -} - -void CloudViewMainWindow::showBasicSurface(const QString& shapeName, const QVector& surfaces, const QVector& outlines) -{ - if (surfaces.isEmpty()) { - return; - } - - m_glWidget->appendBasicShapeSurfaces(surfaces); - m_glWidget->appendBasicShapeSegments(outlines); - statusBar()->showMessage(QString("已添加 %1").arg(shapeName)); - LOG_INFO("[CloudView] Add basic surface: %s, surfaces=%d, outlines=%d\n", - shapeName.toStdString().c_str(), surfaces.size(), outlines.size()); -} - -void CloudViewMainWindow::onShowCircleShape() -{ - QVector3D center; - QVector3D size; - getShapeFrame(m_originalCloud, center, size); +void CloudViewMainWindow::showBasicShape(const QString& shapeName, + const QVector& segments, + const QVector& points) +{ + if (segments.isEmpty() && points.isEmpty()) { + return; + } + + if (!segments.isEmpty()) { + m_glWidget->appendBasicShapeSegments(segments); + } + if (!points.isEmpty()) { + m_glWidget->appendBasicShapePoints(points); + } + m_glWidget->fitToContent(); + statusBar()->showMessage(QString("已添加 %1").arg(shapeName)); + LOG_INFO("[CloudView] Add basic shape: %s, segments=%d, points=%d\n", + shapeName.toStdString().c_str(), segments.size(), points.size()); +} + +void CloudViewMainWindow::showBasicSurface(const QString& shapeName, const QVector& surfaces, const QVector& outlines) +{ + if (surfaces.isEmpty()) { + return; + } + + m_glWidget->appendBasicShapeSurfaces(surfaces); + m_glWidget->appendBasicShapeSegments(outlines); + m_glWidget->fitToContent(); + statusBar()->showMessage(QString("已添加 %1").arg(shapeName)); + LOG_INFO("[CloudView] Add basic surface: %s, surfaces=%d, outlines=%d\n", + shapeName.toStdString().c_str(), surfaces.size(), outlines.size()); +} + +void CloudViewMainWindow::onShowCircleShape() +{ + QVector3D center; + QVector3D size; + getShapeFrame(m_originalCloud, center, size); const double defaultRadius = std::max(1.0f, std::max(size.x(), size.y()) * 0.25f); QDialog dialog(this); dialog.setWindowTitle("圆"); dialog.setModal(true); - - QVBoxLayout* layout = new QVBoxLayout(&dialog); - - QLabel* tip = new QLabel("输入圆心、姿态和半径后点击添加;文件每行一个:{x,y,z}-{r,p,y}-{r}。", &dialog); - tip->setWordWrap(true); - tip->setStyleSheet("color: gray;"); - layout->addWidget(tip); + + QVBoxLayout* layout = new QVBoxLayout(&dialog); + + QLabel* tip = new QLabel("输入圆心、姿态和半径后点击添加;文件每行一个:{x,y,z}-{r,p,y}-{r}。", &dialog); + tip->setWordWrap(true); + tip->setStyleSheet("color: gray;"); + layout->addWidget(tip); QGroupBox* centerGroup = new QGroupBox("圆心", &dialog); QFormLayout* centerLayout = new QFormLayout(centerGroup); @@ -2422,97 +2271,97 @@ void CloudViewMainWindow::onShowCircleShape() QDoubleSpinBox* centerZ = createShapeSpinBox(centerGroup, center.z()); centerLayout->addRow("X:", centerX); centerLayout->addRow("Y:", centerY); - centerLayout->addRow("Z:", centerZ); - layout->addWidget(centerGroup); - - QGroupBox* poseGroup = new QGroupBox("姿态", &dialog); - QFormLayout* poseLayout = new QFormLayout(poseGroup); - QDoubleSpinBox* poseR = createShapeSpinBox(poseGroup, 0.0); - QDoubleSpinBox* poseP = createShapeSpinBox(poseGroup, 0.0); - QDoubleSpinBox* poseY = createShapeSpinBox(poseGroup, 0.0); - poseLayout->addRow("R/RX:", poseR); - poseLayout->addRow("P/RY:", poseP); - poseLayout->addRow("Y/RZ:", poseY); - layout->addWidget(poseGroup); - - QGroupBox* radiusGroup = new QGroupBox("尺寸", &dialog); - QFormLayout* radiusLayout = new QFormLayout(radiusGroup); - QDoubleSpinBox* radius = createShapeSpinBox(radiusGroup, defaultRadius, 0.000001); - radiusLayout->addRow("半径:", radius); - layout->addWidget(radiusGroup); - - auto addCurrentCircle = [this, centerX, centerY, centerZ, poseR, poseP, poseY, radius]() { - const QVector3D inputCenter(static_cast(centerX->value()), - static_cast(centerY->value()), - static_cast(centerZ->value())); - showBasicShape("圆", - makeCircleSegments(inputCenter, - poseR->value(), - poseP->value(), - poseY->value(), - static_cast(radius->value()), - currentShapeEulerOrder(m_comboEulerOrder)), - makeShapeCenterPoint(inputCenter)); - }; - - QPushButton* loadButton = new QPushButton("加载文件", &dialog); - connect(loadButton, &QPushButton::clicked, this, [this, centerX, centerY, centerZ, poseR, poseP, poseY, radius]() { - const QString fileName = QFileDialog::getOpenFileName( - this, "加载圆数据", QString(), - "数据文件 (*.txt *.json *.dat);;所有文件 (*.*)"); - if (fileName.isEmpty()) { - return; - } - - QVector rows; - QString errorMessage; - if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { - QMessageBox::warning(this, "错误", errorMessage); - return; - } - - QVector segments; - QVector centerPoints; - int shapeCount = 0; - if (!makeCircleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, centerPoints, shapeCount, errorMessage)) { - QMessageBox::warning(this, "错误", errorMessage); - return; - } - - if (shapeCount == 1) { - const QVector& values = rows.first().values; - centerX->setValue(values[0]); - centerY->setValue(values[1]); - centerZ->setValue(values[2]); - poseR->setValue(values[3]); - poseP->setValue(values[4]); - poseY->setValue(values[5]); - radius->setValue(values[6]); - } - - showBasicShape(QString("%1 个圆").arg(shapeCount), segments, centerPoints); - }); - - QPushButton* addButton = new QPushButton("添加", &dialog); - connect(addButton, &QPushButton::clicked, this, addCurrentCircle); - - QHBoxLayout* actionLayout = new QHBoxLayout(); - actionLayout->addWidget(loadButton); - actionLayout->addWidget(addButton); - actionLayout->addStretch(); - layout->addLayout(actionLayout); - - QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); - connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); - layout->addWidget(buttonBox); - - dialog.exec(); -} + centerLayout->addRow("Z:", centerZ); + layout->addWidget(centerGroup); -void CloudViewMainWindow::onShowRectangleShape() -{ - QVector3D center; - QVector3D size; + QGroupBox* poseGroup = new QGroupBox("姿态", &dialog); + QFormLayout* poseLayout = new QFormLayout(poseGroup); + QDoubleSpinBox* poseR = createShapeSpinBox(poseGroup, 0.0); + QDoubleSpinBox* poseP = createShapeSpinBox(poseGroup, 0.0); + QDoubleSpinBox* poseY = createShapeSpinBox(poseGroup, 0.0); + poseLayout->addRow("R/RX:", poseR); + poseLayout->addRow("P/RY:", poseP); + poseLayout->addRow("Y/RZ:", poseY); + layout->addWidget(poseGroup); + + QGroupBox* radiusGroup = new QGroupBox("尺寸", &dialog); + QFormLayout* radiusLayout = new QFormLayout(radiusGroup); + QDoubleSpinBox* radius = createShapeSpinBox(radiusGroup, defaultRadius, 0.000001); + radiusLayout->addRow("半径:", radius); + layout->addWidget(radiusGroup); + + auto addCurrentCircle = [this, centerX, centerY, centerZ, poseR, poseP, poseY, radius]() { + const QVector3D inputCenter(static_cast(centerX->value()), + static_cast(centerY->value()), + static_cast(centerZ->value())); + showBasicShape("圆", + makeCircleSegments(inputCenter, + poseR->value(), + poseP->value(), + poseY->value(), + static_cast(radius->value()), + currentShapeEulerOrder(m_comboEulerOrder)), + makeShapeCenterPoint(inputCenter)); + }; + + QPushButton* loadButton = new QPushButton("加载文件", &dialog); + connect(loadButton, &QPushButton::clicked, this, [this, centerX, centerY, centerZ, poseR, poseP, poseY, radius]() { + const QString fileName = QFileDialog::getOpenFileName( + this, "加载圆数据", QString(), + "数据文件 (*.txt *.json *.dat);;所有文件 (*.*)"); + if (fileName.isEmpty()) { + return; + } + + QVector rows; + QString errorMessage; + if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { + QMessageBox::warning(this, "错误", errorMessage); + return; + } + + QVector segments; + QVector centerPoints; + int shapeCount = 0; + if (!makeCircleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, centerPoints, shapeCount, errorMessage)) { + QMessageBox::warning(this, "错误", errorMessage); + return; + } + + if (shapeCount == 1) { + const QVector& values = rows.first().values; + centerX->setValue(values[0]); + centerY->setValue(values[1]); + centerZ->setValue(values[2]); + poseR->setValue(values[3]); + poseP->setValue(values[4]); + poseY->setValue(values[5]); + radius->setValue(values[6]); + } + + showBasicShape(QString("%1 个圆").arg(shapeCount), segments, centerPoints); + }); + + QPushButton* addButton = new QPushButton("添加", &dialog); + connect(addButton, &QPushButton::clicked, this, addCurrentCircle); + + QHBoxLayout* actionLayout = new QHBoxLayout(); + actionLayout->addWidget(loadButton); + actionLayout->addWidget(addButton); + actionLayout->addStretch(); + layout->addLayout(actionLayout); + + QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); + connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + layout->addWidget(buttonBox); + + dialog.exec(); +} + +void CloudViewMainWindow::onShowRectangleShape() +{ + QVector3D center; + QVector3D size; getShapeFrame(m_originalCloud, center, size); const double defaultWidth = std::max(1.0f, size.x() * 0.5f); const double defaultHeight = std::max(1.0f, size.y() * 0.5f); @@ -2523,10 +2372,10 @@ void CloudViewMainWindow::onShowRectangleShape() QVBoxLayout* layout = new QVBoxLayout(&dialog); - QLabel* tip = new QLabel("输入矩形中心、姿态和宽高后点击添加;文件每行一个:{x,y,z}-{r,p,y}-{w,h}。", &dialog); - tip->setWordWrap(true); - tip->setStyleSheet("color: gray;"); - layout->addWidget(tip); + QLabel* tip = new QLabel("输入矩形中心、姿态和宽高后点击添加;文件每行一个:{x,y,z}-{r,p,y}-{w,h}。", &dialog); + tip->setWordWrap(true); + tip->setStyleSheet("color: gray;"); + layout->addWidget(tip); QGroupBox* centerGroup = new QGroupBox("中心", &dialog); QFormLayout* centerLayout = new QFormLayout(centerGroup); @@ -2535,225 +2384,225 @@ void CloudViewMainWindow::onShowRectangleShape() QDoubleSpinBox* centerZ = createShapeSpinBox(centerGroup, center.z()); centerLayout->addRow("X:", centerX); centerLayout->addRow("Y:", centerY); - centerLayout->addRow("Z:", centerZ); - layout->addWidget(centerGroup); - - QGroupBox* poseGroup = new QGroupBox("姿态", &dialog); - QFormLayout* poseLayout = new QFormLayout(poseGroup); - QDoubleSpinBox* poseR = createShapeSpinBox(poseGroup, 0.0); - QDoubleSpinBox* poseP = createShapeSpinBox(poseGroup, 0.0); - QDoubleSpinBox* poseY = createShapeSpinBox(poseGroup, 0.0); - poseLayout->addRow("R/RX:", poseR); - poseLayout->addRow("P/RY:", poseP); - poseLayout->addRow("Y/RZ:", poseY); - layout->addWidget(poseGroup); - - QGroupBox* sizeGroup = new QGroupBox("尺寸", &dialog); - QFormLayout* sizeLayout = new QFormLayout(sizeGroup); - QDoubleSpinBox* width = createShapeSpinBox(sizeGroup, defaultWidth, 0.000001); - QDoubleSpinBox* height = createShapeSpinBox(sizeGroup, defaultHeight, 0.000001); - sizeLayout->addRow("宽:", width); - sizeLayout->addRow("高:", height); - layout->addWidget(sizeGroup); - - auto addCurrentRectangle = [this, centerX, centerY, centerZ, poseR, poseP, poseY, width, height]() { - const QVector3D inputCenter(static_cast(centerX->value()), - static_cast(centerY->value()), - static_cast(centerZ->value())); - showBasicShape("矩形", - makeRectangleSegments(inputCenter, - poseR->value(), - poseP->value(), - poseY->value(), - static_cast(width->value()), - static_cast(height->value()), - currentShapeEulerOrder(m_comboEulerOrder)), - makeShapeCenterPoint(inputCenter)); - }; - - QPushButton* loadButton = new QPushButton("加载文件", &dialog); - connect(loadButton, &QPushButton::clicked, this, [this, centerX, centerY, centerZ, poseR, poseP, poseY, width, height]() { - const QString fileName = QFileDialog::getOpenFileName( - this, "加载矩形数据", QString(), - "数据文件 (*.txt *.json *.dat);;所有文件 (*.*)"); + centerLayout->addRow("Z:", centerZ); + layout->addWidget(centerGroup); + + QGroupBox* poseGroup = new QGroupBox("姿态", &dialog); + QFormLayout* poseLayout = new QFormLayout(poseGroup); + QDoubleSpinBox* poseR = createShapeSpinBox(poseGroup, 0.0); + QDoubleSpinBox* poseP = createShapeSpinBox(poseGroup, 0.0); + QDoubleSpinBox* poseY = createShapeSpinBox(poseGroup, 0.0); + poseLayout->addRow("R/RX:", poseR); + poseLayout->addRow("P/RY:", poseP); + poseLayout->addRow("Y/RZ:", poseY); + layout->addWidget(poseGroup); + + QGroupBox* sizeGroup = new QGroupBox("尺寸", &dialog); + QFormLayout* sizeLayout = new QFormLayout(sizeGroup); + QDoubleSpinBox* width = createShapeSpinBox(sizeGroup, defaultWidth, 0.000001); + QDoubleSpinBox* height = createShapeSpinBox(sizeGroup, defaultHeight, 0.000001); + sizeLayout->addRow("宽:", width); + sizeLayout->addRow("高:", height); + layout->addWidget(sizeGroup); + + auto addCurrentRectangle = [this, centerX, centerY, centerZ, poseR, poseP, poseY, width, height]() { + const QVector3D inputCenter(static_cast(centerX->value()), + static_cast(centerY->value()), + static_cast(centerZ->value())); + showBasicShape("矩形", + makeRectangleSegments(inputCenter, + poseR->value(), + poseP->value(), + poseY->value(), + static_cast(width->value()), + static_cast(height->value()), + currentShapeEulerOrder(m_comboEulerOrder)), + makeShapeCenterPoint(inputCenter)); + }; + + QPushButton* loadButton = new QPushButton("加载文件", &dialog); + connect(loadButton, &QPushButton::clicked, this, [this, centerX, centerY, centerZ, poseR, poseP, poseY, width, height]() { + const QString fileName = QFileDialog::getOpenFileName( + this, "加载矩形数据", QString(), + "数据文件 (*.txt *.json *.dat);;所有文件 (*.*)"); if (fileName.isEmpty()) { - return; - } - - QVector rows; - QString errorMessage; - if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { - QMessageBox::warning(this, "错误", errorMessage); - return; - } - - QVector segments; - QVector centerPoints; - int shapeCount = 0; - if (!makeRectangleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, centerPoints, shapeCount, errorMessage)) { - QMessageBox::warning(this, "错误", errorMessage); - return; - } - - if (shapeCount == 1) { - const QVector& values = rows.first().values; - centerX->setValue(values[0]); - centerY->setValue(values[1]); - centerZ->setValue(values[2]); - poseR->setValue(values[3]); - poseP->setValue(values[4]); - poseY->setValue(values[5]); - width->setValue(values[6]); - height->setValue(values[7]); - } - - showBasicShape(QString("%1 个矩形").arg(shapeCount), segments, centerPoints); - }); - - QPushButton* addButton = new QPushButton("添加", &dialog); - connect(addButton, &QPushButton::clicked, this, addCurrentRectangle); - - QHBoxLayout* actionLayout = new QHBoxLayout(); - actionLayout->addWidget(loadButton); - actionLayout->addWidget(addButton); - actionLayout->addStretch(); - layout->addLayout(actionLayout); - - QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); - connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); - layout->addWidget(buttonBox); - - dialog.exec(); -} - -void CloudViewMainWindow::onShowSurfaceShape() -{ - QVector3D center; - QVector3D size; - getShapeFrame(m_originalCloud, center, size); - const double defaultLength = std::max(1.0f, size.x() * 0.5f); - const double defaultWidth = std::max(1.0f, size.y() * 0.5f); - - QDialog dialog(this); - dialog.setWindowTitle("面"); - dialog.setModal(true); - - QVBoxLayout* layout = new QVBoxLayout(&dialog); - - QLabel* tip = new QLabel("输入中心、姿态和长宽后点击添加;文件每行一个:{x,y,z}-{l,w} 或 {x,y,z}-{r,p,y}-{l,w}。", &dialog); - tip->setWordWrap(true); - tip->setStyleSheet("color: gray;"); - layout->addWidget(tip); - - QGroupBox* centerGroup = new QGroupBox("中心", &dialog); - QFormLayout* centerLayout = new QFormLayout(centerGroup); - QDoubleSpinBox* centerX = createShapeSpinBox(centerGroup, center.x()); - QDoubleSpinBox* centerY = createShapeSpinBox(centerGroup, center.y()); - QDoubleSpinBox* centerZ = createShapeSpinBox(centerGroup, center.z()); - centerLayout->addRow("X:", centerX); - centerLayout->addRow("Y:", centerY); - centerLayout->addRow("Z:", centerZ); - layout->addWidget(centerGroup); - - QGroupBox* poseGroup = new QGroupBox("姿态", &dialog); - QFormLayout* poseLayout = new QFormLayout(poseGroup); - QDoubleSpinBox* poseR = createShapeSpinBox(poseGroup, 0.0); - QDoubleSpinBox* poseP = createShapeSpinBox(poseGroup, 0.0); - QDoubleSpinBox* poseY = createShapeSpinBox(poseGroup, 0.0); - poseLayout->addRow("R/RX:", poseR); - poseLayout->addRow("P/RY:", poseP); - poseLayout->addRow("Y/RZ:", poseY); - layout->addWidget(poseGroup); - - QGroupBox* sizeGroup = new QGroupBox("尺寸", &dialog); - QFormLayout* sizeLayout = new QFormLayout(sizeGroup); - QDoubleSpinBox* length = createShapeSpinBox(sizeGroup, defaultLength, 0.000001); - QDoubleSpinBox* width = createShapeSpinBox(sizeGroup, defaultWidth, 0.000001); - sizeLayout->addRow("长:", length); - sizeLayout->addRow("宽:", width); - layout->addWidget(sizeGroup); - - auto addCurrentSurface = [this, centerX, centerY, centerZ, poseR, poseP, poseY, length, width]() { - const QVector3D inputCenter(static_cast(centerX->value()), - static_cast(centerY->value()), - static_cast(centerZ->value())); - const EulerRotationOrder order = currentShapeEulerOrder(m_comboEulerOrder); - const QVector surfaces = makeSurfaceQuads(inputCenter, - poseR->value(), - poseP->value(), - poseY->value(), - static_cast(length->value()), - static_cast(width->value()), - order); - const QVector outlines = makeSurfaceOutlineSegments(inputCenter, - poseR->value(), - poseP->value(), - poseY->value(), - static_cast(length->value()), - static_cast(width->value()), - order); - showBasicSurface("面", surfaces, outlines); - }; - - QPushButton* loadButton = new QPushButton("加载文件", &dialog); - connect(loadButton, &QPushButton::clicked, this, [this, centerX, centerY, centerZ, poseR, poseP, poseY, length, width]() { - const QString fileName = QFileDialog::getOpenFileName( - this, "加载面数据", QString(), - "数据文件 (*.txt *.json *.dat);;所有文件 (*.*)"); - if (fileName.isEmpty()) { - return; - } - - QVector rows; - QString errorMessage; - if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { - QMessageBox::warning(this, "错误", errorMessage); - return; - } - - QVector surfaces; - QVector outlines; - int shapeCount = 0; - if (!makeSurfaceDataFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), surfaces, outlines, shapeCount, errorMessage)) { - QMessageBox::warning(this, "错误", errorMessage); - return; - } - - if (shapeCount == 1) { - const QVector& values = rows.first().values; - const bool hasPose = values.size() == 8; - centerX->setValue(values[0]); - centerY->setValue(values[1]); - centerZ->setValue(values[2]); - poseR->setValue(hasPose ? values[3] : 0.0); - poseP->setValue(hasPose ? values[4] : 0.0); - poseY->setValue(hasPose ? values[5] : 0.0); - length->setValue(hasPose ? values[6] : values[3]); - width->setValue(hasPose ? values[7] : values[4]); - } - - showBasicSurface(QString("%1 个面").arg(shapeCount), surfaces, outlines); - }); - - QPushButton* addButton = new QPushButton("添加", &dialog); - connect(addButton, &QPushButton::clicked, this, addCurrentSurface); - - QHBoxLayout* actionLayout = new QHBoxLayout(); - actionLayout->addWidget(loadButton); - actionLayout->addWidget(addButton); - actionLayout->addStretch(); - layout->addLayout(actionLayout); - - QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); - connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); - layout->addWidget(buttonBox); - - dialog.exec(); -} - -void CloudViewMainWindow::onShowTriangleShape() -{ - QVector3D center; + return; + } + + QVector rows; + QString errorMessage; + if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { + QMessageBox::warning(this, "错误", errorMessage); + return; + } + + QVector segments; + QVector centerPoints; + int shapeCount = 0; + if (!makeRectangleSegmentsFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), segments, centerPoints, shapeCount, errorMessage)) { + QMessageBox::warning(this, "错误", errorMessage); + return; + } + + if (shapeCount == 1) { + const QVector& values = rows.first().values; + centerX->setValue(values[0]); + centerY->setValue(values[1]); + centerZ->setValue(values[2]); + poseR->setValue(values[3]); + poseP->setValue(values[4]); + poseY->setValue(values[5]); + width->setValue(values[6]); + height->setValue(values[7]); + } + + showBasicShape(QString("%1 个矩形").arg(shapeCount), segments, centerPoints); + }); + + QPushButton* addButton = new QPushButton("添加", &dialog); + connect(addButton, &QPushButton::clicked, this, addCurrentRectangle); + + QHBoxLayout* actionLayout = new QHBoxLayout(); + actionLayout->addWidget(loadButton); + actionLayout->addWidget(addButton); + actionLayout->addStretch(); + layout->addLayout(actionLayout); + + QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); + connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + layout->addWidget(buttonBox); + + dialog.exec(); +} + +void CloudViewMainWindow::onShowSurfaceShape() +{ + QVector3D center; + QVector3D size; + getShapeFrame(m_originalCloud, center, size); + const double defaultLength = std::max(1.0f, size.x() * 0.5f); + const double defaultWidth = std::max(1.0f, size.y() * 0.5f); + + QDialog dialog(this); + dialog.setWindowTitle("面"); + dialog.setModal(true); + + QVBoxLayout* layout = new QVBoxLayout(&dialog); + + QLabel* tip = new QLabel("输入中心、姿态和长宽后点击添加;文件每行一个:{x,y,z}-{l,w} 或 {x,y,z}-{r,p,y}-{l,w}。", &dialog); + tip->setWordWrap(true); + tip->setStyleSheet("color: gray;"); + layout->addWidget(tip); + + QGroupBox* centerGroup = new QGroupBox("中心", &dialog); + QFormLayout* centerLayout = new QFormLayout(centerGroup); + QDoubleSpinBox* centerX = createShapeSpinBox(centerGroup, center.x()); + QDoubleSpinBox* centerY = createShapeSpinBox(centerGroup, center.y()); + QDoubleSpinBox* centerZ = createShapeSpinBox(centerGroup, center.z()); + centerLayout->addRow("X:", centerX); + centerLayout->addRow("Y:", centerY); + centerLayout->addRow("Z:", centerZ); + layout->addWidget(centerGroup); + + QGroupBox* poseGroup = new QGroupBox("姿态", &dialog); + QFormLayout* poseLayout = new QFormLayout(poseGroup); + QDoubleSpinBox* poseR = createShapeSpinBox(poseGroup, 0.0); + QDoubleSpinBox* poseP = createShapeSpinBox(poseGroup, 0.0); + QDoubleSpinBox* poseY = createShapeSpinBox(poseGroup, 0.0); + poseLayout->addRow("R/RX:", poseR); + poseLayout->addRow("P/RY:", poseP); + poseLayout->addRow("Y/RZ:", poseY); + layout->addWidget(poseGroup); + + QGroupBox* sizeGroup = new QGroupBox("尺寸", &dialog); + QFormLayout* sizeLayout = new QFormLayout(sizeGroup); + QDoubleSpinBox* length = createShapeSpinBox(sizeGroup, defaultLength, 0.000001); + QDoubleSpinBox* width = createShapeSpinBox(sizeGroup, defaultWidth, 0.000001); + sizeLayout->addRow("长:", length); + sizeLayout->addRow("宽:", width); + layout->addWidget(sizeGroup); + + auto addCurrentSurface = [this, centerX, centerY, centerZ, poseR, poseP, poseY, length, width]() { + const QVector3D inputCenter(static_cast(centerX->value()), + static_cast(centerY->value()), + static_cast(centerZ->value())); + const EulerRotationOrder order = currentShapeEulerOrder(m_comboEulerOrder); + const QVector surfaces = makeSurfaceQuads(inputCenter, + poseR->value(), + poseP->value(), + poseY->value(), + static_cast(length->value()), + static_cast(width->value()), + order); + const QVector outlines = makeSurfaceOutlineSegments(inputCenter, + poseR->value(), + poseP->value(), + poseY->value(), + static_cast(length->value()), + static_cast(width->value()), + order); + showBasicSurface("面", surfaces, outlines); + }; + + QPushButton* loadButton = new QPushButton("加载文件", &dialog); + connect(loadButton, &QPushButton::clicked, this, [this, centerX, centerY, centerZ, poseR, poseP, poseY, length, width]() { + const QString fileName = QFileDialog::getOpenFileName( + this, "加载面数据", QString(), + "数据文件 (*.txt *.json *.dat);;所有文件 (*.*)"); + if (fileName.isEmpty()) { + return; + } + + QVector rows; + QString errorMessage; + if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { + QMessageBox::warning(this, "错误", errorMessage); + return; + } + + QVector surfaces; + QVector outlines; + int shapeCount = 0; + if (!makeSurfaceDataFromRows(rows, currentShapeEulerOrder(m_comboEulerOrder), surfaces, outlines, shapeCount, errorMessage)) { + QMessageBox::warning(this, "错误", errorMessage); + return; + } + + if (shapeCount == 1) { + const QVector& values = rows.first().values; + const bool hasPose = values.size() == 8; + centerX->setValue(values[0]); + centerY->setValue(values[1]); + centerZ->setValue(values[2]); + poseR->setValue(hasPose ? values[3] : 0.0); + poseP->setValue(hasPose ? values[4] : 0.0); + poseY->setValue(hasPose ? values[5] : 0.0); + length->setValue(hasPose ? values[6] : values[3]); + width->setValue(hasPose ? values[7] : values[4]); + } + + showBasicSurface(QString("%1 个面").arg(shapeCount), surfaces, outlines); + }); + + QPushButton* addButton = new QPushButton("添加", &dialog); + connect(addButton, &QPushButton::clicked, this, addCurrentSurface); + + QHBoxLayout* actionLayout = new QHBoxLayout(); + actionLayout->addWidget(loadButton); + actionLayout->addWidget(addButton); + actionLayout->addStretch(); + layout->addLayout(actionLayout); + + QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); + connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + layout->addWidget(buttonBox); + + dialog.exec(); +} + +void CloudViewMainWindow::onShowTriangleShape() +{ + QVector3D center; QVector3D size; getShapeFrame(m_originalCloud, center, size); const float radius = std::max(1.0f, std::max(size.x(), size.y()) * 0.3f); @@ -2768,7 +2617,7 @@ void CloudViewMainWindow::onShowTriangleShape() QVBoxLayout* layout = new QVBoxLayout(&dialog); - QLabel* tip = new QLabel("输入三个顶点后点击添加;文件每行一个:{x,y,z}-{x,y,z}-{x,y,z}。", &dialog); + QLabel* tip = new QLabel("输入三个顶点后点击添加;文件每行一个:{x,y,z}-{x,y,z}-{x,y,z}。", &dialog); tip->setWordWrap(true); tip->setStyleSheet("color: gray;"); layout->addWidget(tip); @@ -2794,73 +2643,73 @@ void CloudViewMainWindow::onShowTriangleShape() QDoubleSpinBox* x3 = nullptr; QDoubleSpinBox* y3 = nullptr; QDoubleSpinBox* z3 = nullptr; - addPointGroup("点1", defaultP1, x1, y1, z1); - addPointGroup("点2", defaultP2, x2, y2, z2); - addPointGroup("点3", defaultP3, x3, y3, z3); - - auto addCurrentTriangle = [this, x1, y1, z1, x2, y2, z2, x3, y3, z3]() { - QVector points; - points.reserve(3); - points.append(QVector3D(static_cast(x1->value()), static_cast(y1->value()), static_cast(z1->value()))); - points.append(QVector3D(static_cast(x2->value()), static_cast(y2->value()), static_cast(z2->value()))); - points.append(QVector3D(static_cast(x3->value()), static_cast(y3->value()), static_cast(z3->value()))); - showBasicShape("三角形", makeClosedShapeSegments(points, 0.2f, 1.0f, 0.2f)); - }; - - QPushButton* loadButton = new QPushButton("加载文件", &dialog); - connect(loadButton, &QPushButton::clicked, this, [this, x1, y1, z1, x2, y2, z2, x3, y3, z3]() { - const QString fileName = QFileDialog::getOpenFileName( - this, "加载三角形数据", QString(), - "数据文件 (*.txt *.json *.dat);;所有文件 (*.*)"); + addPointGroup("点1", defaultP1, x1, y1, z1); + addPointGroup("点2", defaultP2, x2, y2, z2); + addPointGroup("点3", defaultP3, x3, y3, z3); + + auto addCurrentTriangle = [this, x1, y1, z1, x2, y2, z2, x3, y3, z3]() { + QVector points; + points.reserve(3); + points.append(QVector3D(static_cast(x1->value()), static_cast(y1->value()), static_cast(z1->value()))); + points.append(QVector3D(static_cast(x2->value()), static_cast(y2->value()), static_cast(z2->value()))); + points.append(QVector3D(static_cast(x3->value()), static_cast(y3->value()), static_cast(z3->value()))); + showBasicShape("三角形", makeClosedShapeSegments(points, 0.2f, 1.0f, 0.2f)); + }; + + QPushButton* loadButton = new QPushButton("加载文件", &dialog); + connect(loadButton, &QPushButton::clicked, this, [this, x1, y1, z1, x2, y2, z2, x3, y3, z3]() { + const QString fileName = QFileDialog::getOpenFileName( + this, "加载三角形数据", QString(), + "数据文件 (*.txt *.json *.dat);;所有文件 (*.*)"); if (fileName.isEmpty()) { - return; - } - - QVector rows; - QString errorMessage; - if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { - QMessageBox::warning(this, "错误", errorMessage); - return; - } - - QVector segments; - int shapeCount = 0; - if (!makeTriangleSegmentsFromRows(rows, segments, shapeCount, errorMessage)) { - QMessageBox::warning(this, "错误", errorMessage); - return; - } - - if (shapeCount == 1) { - const QVector& values = rows.first().values; - x1->setValue(values[0]); - y1->setValue(values[1]); - z1->setValue(values[2]); - x2->setValue(values[3]); - y2->setValue(values[4]); - z2->setValue(values[5]); - x3->setValue(values[6]); - y3->setValue(values[7]); - z3->setValue(values[8]); - } - - showBasicShape(QString("%1 个三角形").arg(shapeCount), segments); - }); - - QPushButton* addButton = new QPushButton("添加", &dialog); - connect(addButton, &QPushButton::clicked, this, addCurrentTriangle); - - QHBoxLayout* actionLayout = new QHBoxLayout(); - actionLayout->addWidget(loadButton); - actionLayout->addWidget(addButton); - actionLayout->addStretch(); - layout->addLayout(actionLayout); - - QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); - connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); - layout->addWidget(buttonBox); - - dialog.exec(); -} + return; + } + + QVector rows; + QString errorMessage; + if (!loadNumericRowsFromFile(fileName, rows, errorMessage)) { + QMessageBox::warning(this, "错误", errorMessage); + return; + } + + QVector segments; + int shapeCount = 0; + if (!makeTriangleSegmentsFromRows(rows, segments, shapeCount, errorMessage)) { + QMessageBox::warning(this, "错误", errorMessage); + return; + } + + if (shapeCount == 1) { + const QVector& values = rows.first().values; + x1->setValue(values[0]); + y1->setValue(values[1]); + z1->setValue(values[2]); + x2->setValue(values[3]); + y2->setValue(values[4]); + z2->setValue(values[5]); + x3->setValue(values[6]); + y3->setValue(values[7]); + z3->setValue(values[8]); + } + + showBasicShape(QString("%1 个三角形").arg(shapeCount), segments); + }); + + QPushButton* addButton = new QPushButton("添加", &dialog); + connect(addButton, &QPushButton::clicked, this, addCurrentTriangle); + + QHBoxLayout* actionLayout = new QHBoxLayout(); + actionLayout->addWidget(loadButton); + actionLayout->addWidget(addButton); + actionLayout->addStretch(); + layout->addLayout(actionLayout); + + QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); + connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + layout->addWidget(buttonBox); + + dialog.exec(); +} void CloudViewMainWindow::onClearAll() { @@ -2877,7 +2726,7 @@ void CloudViewMainWindow::onClearAll() // 清除选点信息 m_lblPoint1->setText("点1: --"); m_lblPoint2->setText("点2: --"); - clearMeasureResultDisplay(); + clearMeasureResultDisplay(); m_editPoint1X->setText("--"); m_editPoint1Y->setText("--"); m_editPoint1Z->setText("--"); @@ -2904,7 +2753,7 @@ void CloudViewMainWindow::onClearSelectedPoints() m_glWidget->clearPosePoints(); // 清除选点时也清除姿态 m_lblPoint1->setText("点1: --"); m_lblPoint2->setText("点2: --"); - clearMeasureResultDisplay(); + clearMeasureResultDisplay(); m_editPoint1X->setText("--"); m_editPoint1Y->setText("--"); m_editPoint1Z->setText("--"); @@ -2939,46 +2788,46 @@ void CloudViewMainWindow::onPointSelected(const SelectedPointInfo& point) statusBar()->showMessage(statusMsg); } -void CloudViewMainWindow::onTwoPointsSelected(const SelectedPointInfo& p1, const SelectedPointInfo& p2, float distance) -{ - updateSelectedPointsDisplay(); - updateMeasureResultDisplay(p1, p2, distance, "测量距离"); -} - -void CloudViewMainWindow::clearMeasureResultDisplay() -{ - m_lblDistance->setText("--"); - if (m_lblDeltaXYZ) { - m_lblDeltaXYZ->setText("--"); - } -} - -void CloudViewMainWindow::updateMeasureResultDisplay(const SelectedPointInfo& p1, - const SelectedPointInfo& p2, - float distance, - const QString& statusPrefix) -{ - const float dx = p2.x - p1.x; - const float dy = p2.y - p1.y; - const float dz = p2.z - p1.z; - - m_lblDistance->setText(QString("%1 mm").arg(distance, 0, 'f', 3)); - m_lblDeltaXYZ->setText(QString("X:%1 mm Y:%2 mm Z:%3 mm") - .arg(dx, 0, 'f', 3) - .arg(dy, 0, 'f', 3) - .arg(dz, 0, 'f', 3)); - - statusBar()->showMessage(QString("%1: %2 mm | X:%3 Y:%4 Z:%5") - .arg(statusPrefix) - .arg(distance, 0, 'f', 3) - .arg(dx, 0, 'f', 3) - .arg(dy, 0, 'f', 3) - .arg(dz, 0, 'f', 3)); -} - -void CloudViewMainWindow::updateSelectedPointsDisplay() -{ - auto selectedPoints = m_glWidget->getSelectedPoints(); +void CloudViewMainWindow::onTwoPointsSelected(const SelectedPointInfo& p1, const SelectedPointInfo& p2, float distance) +{ + updateSelectedPointsDisplay(); + updateMeasureResultDisplay(p1, p2, distance, "测量距离"); +} + +void CloudViewMainWindow::clearMeasureResultDisplay() +{ + m_lblDistance->setText("--"); + if (m_lblDeltaXYZ) { + m_lblDeltaXYZ->setText("--"); + } +} + +void CloudViewMainWindow::updateMeasureResultDisplay(const SelectedPointInfo& p1, + const SelectedPointInfo& p2, + float distance, + const QString& statusPrefix) +{ + const float dx = p2.x - p1.x; + const float dy = p2.y - p1.y; + const float dz = p2.z - p1.z; + + m_lblDistance->setText(QString("%1 mm").arg(distance, 0, 'f', 3)); + m_lblDeltaXYZ->setText(QString("X:%1 mm Y:%2 mm Z:%3 mm") + .arg(dx, 0, 'f', 3) + .arg(dy, 0, 'f', 3) + .arg(dz, 0, 'f', 3)); + + statusBar()->showMessage(QString("%1: %2 mm | X:%3 Y:%4 Z:%5") + .arg(statusPrefix) + .arg(distance, 0, 'f', 3) + .arg(dx, 0, 'f', 3) + .arg(dy, 0, 'f', 3) + .arg(dz, 0, 'f', 3)); +} + +void CloudViewMainWindow::updateSelectedPointsDisplay() +{ + auto selectedPoints = m_glWidget->getSelectedPoints(); if (selectedPoints.size() >= 1 && selectedPoints[0].valid) { QString text; @@ -3017,17 +2866,17 @@ void CloudViewMainWindow::updateSelectedPointsDisplay() m_editPoint2X->setText(QString::number(selectedPoints[1].x, 'f', 3)); m_editPoint2Y->setText(QString::number(selectedPoints[1].y, 'f', 3)); m_editPoint2Z->setText(QString::number(selectedPoints[1].z, 'f', 3)); - } else { - m_lblPoint2->setText("点2: --"); - m_editPoint2X->setText("--"); - m_editPoint2Y->setText("--"); - m_editPoint2Z->setText("--"); - } - - if (selectedPoints.size() < 2 || !selectedPoints[0].valid || !selectedPoints[1].valid) { - clearMeasureResultDisplay(); - } -} + } else { + m_lblPoint2->setText("点2: --"); + m_editPoint2X->setText("--"); + m_editPoint2Y->setText("--"); + m_editPoint2Z->setText("--"); + } + + if (selectedPoints.size() < 2 || !selectedPoints[0].valid || !selectedPoints[1].valid) { + clearMeasureResultDisplay(); + } +} void CloudViewMainWindow::onLineSelectModeChanged(bool checked) { @@ -4591,14 +4440,6 @@ void CloudViewMainWindow::onEulerOrderChanged(int index) LOG_INFO("[CloudView] Euler rotation order changed to: %s\n", orderName.toStdString().c_str()); } -void CloudViewMainWindow::onViewAnglesChanged(float rotX, float rotY, float rotZ) -{ - // 更新显示的角度值 - m_editRotX->setText(QString::number(rotX, 'f', 1)); - m_editRotY->setText(QString::number(rotY, 'f', 1)); - m_editRotZ->setText(QString::number(rotZ, 'f', 1)); -} - void CloudViewMainWindow::onPoint1CoordChanged() { // 读取编辑框中的坐标 @@ -4628,8 +4469,8 @@ void CloudViewMainWindow::onPoint1CoordChanged() // 如果启用了测距且有两个点,重新计算距离 auto updatedPoints = m_glWidget->getSelectedPoints(); if (m_glWidget->isMeasureDistanceEnabled() && updatedPoints.size() >= 2 && updatedPoints[1].valid) { - const float distance = m_glWidget->calculateDistance(updatedPoints[0], updatedPoints[1]); - updateMeasureResultDisplay(updatedPoints[0], updatedPoints[1], distance, "点1坐标已更新"); + const float distance = m_glWidget->calculateDistance(updatedPoints[0], updatedPoints[1]); + updateMeasureResultDisplay(updatedPoints[0], updatedPoints[1], distance, "点1坐标已更新"); } else { statusBar()->showMessage(QString("点1坐标已更新为 (%1, %2, %3)").arg(x, 0, 'f', 3).arg(y, 0, 'f', 3).arg(z, 0, 'f', 3)); } @@ -4666,8 +4507,8 @@ void CloudViewMainWindow::onPoint2CoordChanged() // 如果启用了测距,重新计算距离 auto updatedPoints = m_glWidget->getSelectedPoints(); if (m_glWidget->isMeasureDistanceEnabled() && updatedPoints.size() >= 2 && updatedPoints[0].valid) { - const float distance = m_glWidget->calculateDistance(updatedPoints[0], updatedPoints[1]); - updateMeasureResultDisplay(updatedPoints[0], updatedPoints[1], distance, "点2坐标已更新"); + const float distance = m_glWidget->calculateDistance(updatedPoints[0], updatedPoints[1]); + updateMeasureResultDisplay(updatedPoints[0], updatedPoints[1], distance, "点2坐标已更新"); } else { statusBar()->showMessage(QString("点2坐标已更新为 (%1, %2, %3)").arg(x, 0, 'f', 3).arg(y, 0, 'f', 3).arg(z, 0, 'f', 3)); } diff --git a/CloudView/main.cpp b/CloudView/main.cpp index 2229ffc..cf0e6a9 100644 --- a/CloudView/main.cpp +++ b/CloudView/main.cpp @@ -65,6 +65,14 @@ int main(int argc, char* argv[]) parser.addOption(triangleOption); parser.addOption(sequenceOption); parser.addPositionalArgument("files", "Point cloud or segment files to load."); + if (app.arguments().contains(QStringLiteral("--help")) || + app.arguments().contains(QStringLiteral("-h"))) { + parser.showHelp(0); + } + if (app.arguments().contains(QStringLiteral("--version")) || + app.arguments().contains(QStringLiteral("-v"))) { + parser.showVersion(); + } parser.process(app); CloudViewMainWindow::StartupFiles startupFiles; diff --git a/CloudView3D/CloudView3D.pro b/CloudView3D/CloudView3D.pro index 9d5fea8..17b78f5 100644 --- a/CloudView3D/CloudView3D.pro +++ b/CloudView3D/CloudView3D.pro @@ -4,6 +4,13 @@ CONFIG += c++17 staticlib TEMPLATE = lib TARGET = CloudView3D +win32:CONFIG(debug, debug|release) { + DESTDIR = $$PWD/debug +} +win32:CONFIG(release, debug|release) { + DESTDIR = $$PWD/release +} + win32-msvc { QMAKE_CXXFLAGS += /utf-8 QMAKE_CXXFLAGS += /bigobj diff --git a/CloudView3D/Inc/PointCloudGLWidget.h b/CloudView3D/Inc/PointCloudGLWidget.h index f974fa4..7af1aa0 100644 --- a/CloudView3D/Inc/PointCloudGLWidget.h +++ b/CloudView3D/Inc/PointCloudGLWidget.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #include #include @@ -11,8 +12,13 @@ #include #include -#include "CloudView3DTypes.h" -#include "VZNL_Types.h" +#include "CloudView3DTypes.h" +#include "VZNL_Types.h" + +class QToolButton; +class QPushButton; +class QPinchGesture; +class QResizeEvent; /** * @brief 点云显示颜色枚举 @@ -84,58 +90,58 @@ struct SelectedLineInfo /** * @brief 线段数据 */ -struct LineSegment -{ - float x1, y1, z1; // 起点 - float x2, y2, z2; // 终点 +struct LineSegment +{ + float x1, y1, z1; // 起点 + float x2, y2, z2; // 终点 float r, g, b; // 颜色 (0-1) float lineWidth; // 线宽,0 表示使用默认值(来自 RGBA 中 A > 1 的值) LineSegment() : x1(0), y1(0), z1(0), x2(0), y2(0), z2(0), r(1), g(1), b(1), lineWidth(0) {} LineSegment(float _x1, float _y1, float _z1, float _x2, float _y2, float _z2, float _r = 1.0f, float _g = 1.0f, float _b = 1.0f, float _lw = 0) - : x1(_x1), y1(_y1), z1(_z1), x2(_x2), y2(_y2), z2(_z2), r(_r), g(_g), b(_b), lineWidth(_lw) {} -}; - -/** - * @brief 面片数据 - */ -struct SurfaceQuad -{ - QVector3D p1, p2, p3, p4; - float r, g, b, a; - - SurfaceQuad() : p1(), p2(), p3(), p4(), r(0.0f), g(0.8f), b(1.0f), a(0.5f) {} - SurfaceQuad(const QVector3D& _p1, - const QVector3D& _p2, - const QVector3D& _p3, - const QVector3D& _p4, - float _r = 0.0f, - float _g = 0.8f, - float _b = 1.0f, - float _a = 0.5f) - : p1(_p1), p2(_p2), p3(_p3), p4(_p4), r(_r), g(_g), b(_b), a(_a) {} -}; - -struct BasicShapePoint -{ - float x, y, z; - float r, g, b; - float pointSize; - - BasicShapePoint() : x(0), y(0), z(0), r(1), g(1), b(1), pointSize(8.0f) {} - BasicShapePoint(float _x, - float _y, - float _z, - float _r = 1.0f, - float _g = 1.0f, - float _b = 1.0f, - float _size = 8.0f) - : x(_x), y(_y), z(_z), r(_r), g(_g), b(_b), pointSize(_size) {} -}; - -/** - * @brief 姿态点数据 - * + : x1(_x1), y1(_y1), z1(_z1), x2(_x2), y2(_y2), z2(_z2), r(_r), g(_g), b(_b), lineWidth(_lw) {} +}; + +/** + * @brief 面片数据 + */ +struct SurfaceQuad +{ + QVector3D p1, p2, p3, p4; + float r, g, b, a; + + SurfaceQuad() : p1(), p2(), p3(), p4(), r(0.0f), g(0.8f), b(1.0f), a(0.5f) {} + SurfaceQuad(const QVector3D& _p1, + const QVector3D& _p2, + const QVector3D& _p3, + const QVector3D& _p4, + float _r = 0.0f, + float _g = 0.8f, + float _b = 1.0f, + float _a = 0.5f) + : p1(_p1), p2(_p2), p3(_p3), p4(_p4), r(_r), g(_g), b(_b), a(_a) {} +}; + +struct BasicShapePoint +{ + float x, y, z; + float r, g, b; + float pointSize; + + BasicShapePoint() : x(0), y(0), z(0), r(1), g(1), b(1), pointSize(8.0f) {} + BasicShapePoint(float _x, + float _y, + float _z, + float _r = 1.0f, + float _g = 1.0f, + float _b = 1.0f, + float _size = 8.0f) + : x(_x), y(_y), z(_z), r(_r), g(_g), b(_b), pointSize(_size) {} +}; + +/** + * @brief 姿态点数据 + * * 坐标系约定(右手坐标系): * - X轴:红色,指向右 * - Y轴:绿色,指向上 @@ -170,12 +176,13 @@ public: void addPointCloud(const PointCloudXYZ& cloud, const QString& name = ""); void addPointCloud(const PointCloudXYZRGB& cloud, const QString& name = ""); + void appendPointCloud(const PointCloudXYZRGB& cloud, const QString& name = ""); void clearPointClouds(); - void setPointCloudColor(PointCloudColor color); - void setPointSize(float size); - void resetView(); - void fitToContent(); - void zoomIn(); + void setPointCloudColor(PointCloudColor color); + void setPointSize(float size); + void resetView(); + void fitToContent(); + void zoomIn(); void zoomOut(); /** * @brief 设置视角旋转角度(不改变缩放和平移) @@ -307,26 +314,26 @@ public: */ void clearLineSegments(); - /** - * @brief 设置基础图形线段 - */ - void setBasicShapeSegments(const QVector& segments); - - /** - * @brief 追加基础图形线段 - */ - void appendBasicShapeSegments(const QVector& segments); - - /** - * @brief 追加基础图形面片 - */ - void appendBasicShapeSurfaces(const QVector& surfaces); - void appendBasicShapePoints(const QVector& points); - - /** - * @brief 清除基础图形线段 - */ - void clearBasicShapeSegments(); + /** + * @brief 设置基础图形线段 + */ + void setBasicShapeSegments(const QVector& segments); + + /** + * @brief 追加基础图形线段 + */ + void appendBasicShapeSegments(const QVector& segments); + + /** + * @brief 追加基础图形面片 + */ + void appendBasicShapeSurfaces(const QVector& surfaces); + void appendBasicShapePoints(const QVector& points); + + /** + * @brief 清除基础图形线段 + */ + void clearBasicShapeSegments(); /** * @brief 添加姿态点 @@ -338,20 +345,21 @@ public: */ void clearPosePoints(); -signals: - void pointSelected(const SelectedPointInfo& point); - void twoPointsSelected(const SelectedPointInfo& p1, const SelectedPointInfo& p2, float distance); - void measurementFinished(const SelectedPointInfo& p1, - const SelectedPointInfo& p2, - float distance, - float dx, - float dy, - float dz); - void lineSelected(const SelectedLineInfo& line); - void viewAnglesChanged(float rotX, float rotY, float rotZ); +signals: + void pointSelected(const SelectedPointInfo& point); + void twoPointsSelected(const SelectedPointInfo& p1, const SelectedPointInfo& p2, float distance); + void measurementFinished(const SelectedPointInfo& p1, + const SelectedPointInfo& p2, + float distance, + float dx, + float dy, + float dz); + void lineSelected(const SelectedLineInfo& line); + void viewAnglesChanged(float rotX, float rotY, float rotZ); -protected: - void initializeGL() override; +protected: + bool event(QEvent* event) override; + void initializeGL() override; void resizeGL(int w, int h) override; void paintGL() override; void mousePressEvent(QMouseEvent* event) override; @@ -360,6 +368,7 @@ protected: void wheelEvent(QWheelEvent* event) override; void mouseDoubleClickEvent(QMouseEvent* event) override; void keyPressEvent(QKeyEvent* event) override; + void resizeEvent(QResizeEvent* event) override; private: struct PointCloudData @@ -455,31 +464,68 @@ private: }; void computeBoundingBox(); - void setCurrentColor(PointCloudColor color); - void setColorByIndex(int colorIndex); // 根据索引设置颜色 + void updateProjection(const QMatrix4x4& modelView); + float fittedCameraDistance(float padding = 1.05f) const; + float minimumCameraDistance() const; + QVector3D colorForIndex(int colorIndex) const; SelectedPointInfo pickPoint(int screenX, int screenY); void drawSelectedPoints(); void drawMeasurementLine(); void drawAxis(); - void drawSelectedLine(); // 绘制选中的线 - void drawLineSegments(); // 绘制线段 - void drawBasicShapeSurfaces(); // 绘制基础图形面片 - void drawBasicShapeSegments(); // 绘制基础图形 - void drawBasicShapePoints(); - void drawLineSegmentList(const QVector& segments); - void drawPosePoints(); // 绘制姿态点 + void drawSelectedLine(); // 绘制选中的线 + void drawLineSegments(); // 绘制线段 + void drawBasicShapeSurfaces(); // 绘制基础图形面片 + void drawBasicShapeSegments(); // 绘制基础图形 + void drawBasicShapePoints(); + void drawLineSegmentList(const QVector& segments); + void drawPosePoints(); // 绘制姿态点 void drawAxisLabels(); // 绘制坐标轴 XYZ 标注 void uploadToVBO(PointCloudData& data); // 上传数据到 VBO void releaseVBO(PointCloudData& data); // 释放 VBO 资源 + // GLES2 shader 渲染 + bool ensureShader(); + void drawCloud(PointCloudData& data, float pointSize); + void drawPrimitives(const QMatrix4x4& mvp, + const float* positions, + int vertexCount, + GLenum mode, + const float* colors, + const QVector3D& uniformColor, + float pointSize, + float alpha, + bool depthTest); + void resetRenderState(); + + // 覆盖控件:左侧竖向视图按钮 / 左下角旋转角度 / 右侧缩放 + void createOverlayControls(); + void repositionOverlayControls(); + void showViewAngleDialog(); + void updateViewAngleDisplay(); + bool handlePinchGesture(QPinchGesture* gesture); + std::vector m_pointClouds; QMatrix4x4 m_projection; QMatrix4x4 m_view; QMatrix4x4 m_model; + QMatrix4x4 m_mvp; // 当前帧的模型视图投影矩阵(点云主渲染用) - float m_distance; - float m_rotationX; + // GLES2 shader 渲染资源 + QOpenGLShaderProgram* m_shader; + QOpenGLBuffer m_dynamicVbo; // 动态叠加图元(选中点/线/轴/姿态等)的临时 VBO + bool m_shaderReady; + GLint m_mvpLocation; + GLint m_positionLocation; + GLint m_colorLocation; + GLint m_pointSizeLocation; + GLint m_alphaLocation; + GLint m_useVertexColorLocation; + GLint m_uniformColorLocation; + + float m_distance; + float m_pinchStartDistance; + float m_rotationX; float m_rotationY; float m_rotationZ; QQuaternion m_rotation; // 使用四元数存储旋转状态 @@ -510,12 +556,19 @@ private: int m_colorIndex; // 颜色轮换索引 static const int COLOR_COUNT = 7; // 可用颜色数量 - // 线段和姿态点数据 - QVector m_lineSegments; - QVector m_basicShapeSegments; - QVector m_basicShapeSurfaces; - QVector m_basicShapePoints; - QVector m_posePoints; -}; + // 线段和姿态点数据 + QVector m_lineSegments; + QVector m_basicShapeSegments; + QVector m_basicShapeSurfaces; + QVector m_basicShapePoints; + QVector m_posePoints; + + // 覆盖控件 + QWidget* m_viewToolbar; // 左侧竖向视图按钮容器 + QVector m_viewButtons; + QPushButton* m_viewAngleButton; // 左下角旋转角度显示按钮 + QToolButton* m_zoomInButton; + QToolButton* m_zoomOutButton; +}; #endif // POINT_CLOUD_GL_WIDGET_H diff --git a/CloudView3D/Src/PointCloudGLWidget.cpp b/CloudView3D/Src/PointCloudGLWidget.cpp index 72b9452..8937c52 100644 --- a/CloudView3D/Src/PointCloudGLWidget.cpp +++ b/CloudView3D/Src/PointCloudGLWidget.cpp @@ -1,8 +1,22 @@ #include "PointCloudGLWidget.h" #include #include +#include +#include +#include +#include +#include +#include +#include #include +#include +#include +#include +#include +#include +#include #include +#include #include #define CLOUD_LOG_INFO(...) \ @@ -20,9 +34,34 @@ #include #endif +namespace +{ +// 将角度归一化到 [-180, 180],避免往同一方向旋转时无限累加 +float NormalizeAngleDegrees(float angle) +{ + float wrapped = std::fmod(angle, 360.0f); + if (wrapped > 180.0f) { + wrapped -= 360.0f; + } else if (wrapped < -180.0f) { + wrapped += 360.0f; + } + return wrapped; +} +} + PointCloudGLWidget::PointCloudGLWidget(QWidget* parent) : QOpenGLWidget(parent) + , m_shader(nullptr) + , m_shaderReady(false) + , m_mvpLocation(0) + , m_positionLocation(0) + , m_colorLocation(0) + , m_pointSizeLocation(0) + , m_alphaLocation(0) + , m_useVertexColorLocation(0) + , m_uniformColorLocation(0) , m_distance(100.0f) + , m_pinchStartDistance(100.0f) , m_rotationX(0.0f) , m_rotationY(0.0f) , m_rotationZ(0.0f) @@ -41,12 +80,60 @@ PointCloudGLWidget::PointCloudGLWidget(QWidget* parent) , m_measureDistanceEnabled(false) , m_hasListHighlightPoint(false) , m_colorIndex(0) + , m_viewToolbar(nullptr) + , m_viewAngleButton(nullptr) + , m_zoomInButton(nullptr) + , m_zoomOutButton(nullptr) { setFocusPolicy(Qt::StrongFocus); setMouseTracking(true); + setAttribute(Qt::WA_AcceptTouchEvents, true); + grabGesture(Qt::PinchGesture); // 确保 m_model 是单位矩阵 m_model.setToIdentity(); + + createOverlayControls(); + connect(this, &PointCloudGLWidget::viewAnglesChanged, + this, [this](float, float, float) { updateViewAngleDisplay(); }); +} + +bool PointCloudGLWidget::event(QEvent* event) +{ + if (event->type() == QEvent::Gesture) { + auto* gestureEvent = static_cast(event); + if (auto* pinch = static_cast( + gestureEvent->gesture(Qt::PinchGesture))) { + const bool handled = handlePinchGesture(pinch); + if (handled) { + gestureEvent->accept(pinch); + return true; + } + } + } + return QOpenGLWidget::event(event); +} + +bool PointCloudGLWidget::handlePinchGesture(QPinchGesture* gesture) +{ + if (!gesture) { + return false; + } + + if (gesture->state() == Qt::GestureStarted) { + m_pinchStartDistance = m_distance; + } + + const qreal scale = gesture->totalScaleFactor(); + if (scale <= 0.0 || !std::isfinite(static_cast(scale))) { + return false; + } + + m_distance = qBound(minimumCameraDistance(), + m_pinchStartDistance / static_cast(scale), + 1.0e7f); + update(); + return true; } PointCloudGLWidget::~PointCloudGLWidget() @@ -56,6 +143,10 @@ PointCloudGLWidget::~PointCloudGLWidget() for (auto& cloudData : m_pointClouds) { releaseVBO(cloudData); } + if (m_shader) { + delete m_shader; + m_shader = nullptr; + } doneCurrent(); } @@ -105,6 +196,216 @@ void PointCloudGLWidget::releaseVBO(PointCloudData& data) data.vboCreated = false; } +void PointCloudGLWidget::createOverlayControls() +{ + const int buttonSize = 32; + const QString toolButtonStyle = + "QToolButton {" + " background-color: rgba(20, 36, 58, 200);" + " color: #d8e8f4;" + " border: 1px solid #2b4f77;" + " border-radius: 4px;" + " font-size: 13px;" + "}" + "QToolButton:hover { background-color: #204a72; }" + "QToolButton:pressed { background-color: #10243a; }"; + + // ── 左侧视图方向工具栏(纵向排布) ── + struct ViewPreset { + const char* label; + const char* tooltip; + float rx, ry, rz; + }; + // 坐标系定义:X向右,Y向下,Z朝后(与 CloudView 一致) + const ViewPreset presets[] = { + {"前", "正视 (XY面)", 180.0f, 0.0f, 0.0f}, + {"后", "后视", 180.0f, 180.0f, 0.0f}, + {"左", "左视 (YZ面)", 180.0f, 90.0f, 0.0f}, + {"右", "右视", 180.0f, -90.0f, 0.0f}, + {"上", "俯视 (XZ面)", 90.0f, 0.0f, 0.0f}, + {"下", "仰视", -90.0f, 0.0f, 0.0f}, + {"机", "机械臂", -90.0f, 90.0f, 0.0f}, + }; + + m_viewToolbar = new QWidget(this); + m_viewToolbar->setStyleSheet("background: transparent;"); + auto* toolbarLayout = new QVBoxLayout(m_viewToolbar); + toolbarLayout->setContentsMargins(4, 4, 4, 4); + toolbarLayout->setSpacing(4); + // 工具栏尺寸跟随内容,便于垂直居中定位 + toolbarLayout->setSizeConstraint(QLayout::SetFixedSize); + + for (const ViewPreset& preset : presets) { + auto* button = new QToolButton(m_viewToolbar); + button->setText(QString::fromUtf8(preset.label)); + button->setToolTip(QString::fromUtf8(preset.tooltip)); + button->setFixedSize(buttonSize, buttonSize); + button->setStyleSheet(toolButtonStyle); + const float rx = preset.rx; + const float ry = preset.ry; + const float rz = preset.rz; + connect(button, &QToolButton::clicked, this, [this, rx, ry, rz]() { + setViewAngles(rx, ry, rz); + }); + m_viewButtons.append(button); + toolbarLayout->addWidget(button); + } + toolbarLayout->addStretch(1); + m_viewToolbar->setFixedWidth(buttonSize + 12); + m_viewToolbar->show(); + + // ── 左下角旋转角度显示按钮(点击弹窗输入角度) ── + m_viewAngleButton = new QPushButton(this); + m_viewAngleButton->setStyleSheet( + "QPushButton {" + " background-color: rgba(20, 36, 58, 200);" + " color: #d8e8f4;" + " border: 1px solid #2b4f77;" + " border-radius: 4px;" + " padding: 4px 8px;" + " text-align: left;" + "}" + "QPushButton:hover { background-color: #204a72; }"); + connect(m_viewAngleButton, &QPushButton::clicked, + this, &PointCloudGLWidget::showViewAngleDialog); + // 显式指定字体,保证 fontMetrics 度量与实际渲染一致 + QFont angleFont = m_viewAngleButton->font(); + angleFont.setPixelSize(12); + m_viewAngleButton->setFont(angleFont); + // 竖向三行显示 RX/RY/RZ:宽度按最长一行算,高度按三行算 + const QString longestAngleLine = QStringLiteral("RX:-360.0"); + const int angleTextWidth = + m_viewAngleButton->fontMetrics().horizontalAdvance(longestAngleLine); + const int angleLineHeight = m_viewAngleButton->fontMetrics().lineSpacing(); + m_viewAngleButton->setFixedSize(angleTextWidth + 24, angleLineHeight * 3 + 16); + m_viewAngleButton->show(); + + // ── 右侧悬浮缩放按钮(垂直居中,与 CloudView 位置一致) ── + const QString zoomButtonStyle = + "QToolButton {" + " background-color: rgba(20, 36, 58, 200);" + " color: #e8f2fb;" + " border: 1px solid #2b4f77;" + " border-radius: 4px;" + " font-size: 26px;" + " font-weight: bold;" + "}" + "QToolButton:hover { background-color: #204a72; }" + "QToolButton:pressed { background-color: #10243a; }"; + const int zoomButtonSize = 28; + m_zoomInButton = new QToolButton(this); + m_zoomInButton->setText("+"); + m_zoomInButton->setFixedSize(zoomButtonSize, zoomButtonSize); + m_zoomInButton->setStyleSheet(zoomButtonStyle); + m_zoomInButton->setAutoRepeat(true); + m_zoomInButton->setAutoRepeatDelay(300); + m_zoomInButton->setAutoRepeatInterval(50); + connect(m_zoomInButton, &QToolButton::clicked, this, &PointCloudGLWidget::zoomIn); + m_zoomInButton->show(); + + m_zoomOutButton = new QToolButton(this); + m_zoomOutButton->setText("-"); + m_zoomOutButton->setFixedSize(zoomButtonSize, zoomButtonSize); + m_zoomOutButton->setStyleSheet(zoomButtonStyle); + m_zoomOutButton->setAutoRepeat(true); + m_zoomOutButton->setAutoRepeatDelay(300); + m_zoomOutButton->setAutoRepeatInterval(50); + connect(m_zoomOutButton, &QToolButton::clicked, this, &PointCloudGLWidget::zoomOut); + m_zoomOutButton->show(); + + updateViewAngleDisplay(); + repositionOverlayControls(); +} + +void PointCloudGLWidget::repositionOverlayControls() +{ + const int widgetWidth = width(); + const int widgetHeight = height(); + if (widgetWidth <= 0 || widgetHeight <= 0) { + return; + } + + // 左侧视图工具栏:垂直居中 + if (m_viewToolbar) { + m_viewToolbar->move(8, qMax(0, (widgetHeight - m_viewToolbar->height()) / 2)); + } + + // 左下角旋转角度按钮 + if (m_viewAngleButton) { + m_viewAngleButton->adjustSize(); + const int margin = 8; + m_viewAngleButton->move(margin, + widgetHeight - m_viewAngleButton->height() - margin); + } + + // 右侧缩放按钮:垂直居中 + if (m_zoomInButton && m_zoomOutButton) { + const int buttonWidth = m_zoomInButton->width(); + const int buttonHeight = m_zoomInButton->height(); + const int margin = 8; + const int gap = 4; + const int x = widgetWidth - buttonWidth - margin; + const int y = qMax(0, (widgetHeight - buttonHeight * 2 - gap) / 2); + m_zoomInButton->move(x, y); + m_zoomOutButton->move(x, y + buttonHeight + gap); + } +} + +void PointCloudGLWidget::resizeEvent(QResizeEvent* event) +{ + QOpenGLWidget::resizeEvent(event); + repositionOverlayControls(); +} + +void PointCloudGLWidget::updateViewAngleDisplay() +{ + if (!m_viewAngleButton) { + return; + } + m_viewAngleButton->setText(QStringLiteral("RX:%1\nRY:%2\nRZ:%3") + .arg(m_rotationX, 0, 'f', 1) + .arg(m_rotationY, 0, 'f', 1) + .arg(m_rotationZ, 0, 'f', 1)); +} + +void PointCloudGLWidget::showViewAngleDialog() +{ + QDialog dialog(this); + dialog.setWindowTitle(QStringLiteral("设置视图角度")); + dialog.setMinimumWidth(240); + + auto* form = new QFormLayout(&dialog); + auto* spinX = new QDoubleSpinBox(&dialog); + auto* spinY = new QDoubleSpinBox(&dialog); + auto* spinZ = new QDoubleSpinBox(&dialog); + for (QDoubleSpinBox* spin : {spinX, spinY, spinZ}) { + spin->setRange(-180.0, 180.0); + spin->setDecimals(1); + spin->setSingleStep(1.0); + spin->setSuffix(QStringLiteral(" °")); + } + spinX->setValue(m_rotationX); + spinY->setValue(m_rotationY); + spinZ->setValue(m_rotationZ); + form->addRow(QStringLiteral("RX"), spinX); + form->addRow(QStringLiteral("RY"), spinY); + form->addRow(QStringLiteral("RZ"), spinZ); + + auto* buttons = new QDialogButtonBox( + QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog); + buttons->button(QDialogButtonBox::Ok)->setText(QStringLiteral("确认")); + buttons->button(QDialogButtonBox::Cancel)->setText(QStringLiteral("取消")); + connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept); + connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + form->addRow(buttons); + + if (dialog.exec() == QDialog::Accepted) { + setViewAngles(static_cast(spinX->value()), + static_cast(spinY->value()), + static_cast(spinZ->value())); + } +} + void PointCloudGLWidget::initializeGL() { initializeOpenGLFunctions(); @@ -113,24 +414,229 @@ void PointCloudGLWidget::initializeGL() glClearColor(0.15f, 0.15f, 0.15f, 1.0f); glEnable(GL_DEPTH_TEST); - glEnable(GL_POINT_SMOOTH); - glHint(GL_POINT_SMOOTH_HINT, GL_NICEST); + // 桌面 GL(GL 3.2+)下,顶点着色器输出的 gl_PointSize 需显式开启才生效; + // GLES2 总是使用 shader 点大小,无需此开关。 + if (QOpenGLContext::currentContext() && + !QOpenGLContext::currentContext()->isOpenGLES()) { +#ifdef GL_PROGRAM_POINT_SIZE + glEnable(GL_PROGRAM_POINT_SIZE); +#endif + } + if (!m_dynamicVbo.isCreated()) { + m_dynamicVbo.create(); + } + m_shaderReady = false; + ensureShader(); +} + +bool PointCloudGLWidget::ensureShader() +{ + if (m_shaderReady) { + return m_shader != nullptr; + } + m_shaderReady = true; + + if (!m_shader) { + auto* program = new QOpenGLShaderProgram(); + program->bindAttributeLocation("aPosition", 0); + program->bindAttributeLocation("aColor", 1); + + if (!program->addShaderFromSourceCode( + QOpenGLShader::Vertex, + // GLSL ES 1.00 的 vertex shader 有默认 float 精度(highp), + // 但显式声明更稳妥,兼容性更好。 + "precision highp float;\n" + "attribute vec3 aPosition;\n" + "attribute vec3 aColor;\n" + "uniform mat4 uMvp;\n" + "uniform float uPointSize;\n" + "uniform float uAlpha;\n" + "uniform float uUseVertexColor;\n" + "uniform vec3 uColor;\n" + "varying vec3 vColor;\n" + "varying float vAlpha;\n" + "void main() {\n" + " gl_Position = uMvp * vec4(aPosition, 1.0);\n" + " gl_PointSize = uPointSize;\n" + " vColor = (uUseVertexColor > 0.5) ? aColor : uColor;\n" + " vAlpha = uAlpha;\n" + "}\n") || + !program->addShaderFromSourceCode( + QOpenGLShader::Fragment, + // GLSL ES 1.00 的 fragment shader 没有默认 float 精度, + // 必须显式声明,否则 Mali 等 GLES 驱动编译失败(S0032)。 + "precision highp float;\n" + "varying vec3 vColor;\n" + "varying float vAlpha;\n" + "void main() {\n" + " gl_FragColor = vec4(vColor, vAlpha);\n" + "}\n")) { + LOG_ERROR("[CloudView] Shader compile failed: %s\n", + program->log().isEmpty() + ? "unknown error" + : qPrintable(program->log())); + delete program; + return false; + } + + if (!program->link()) { + LOG_ERROR("[CloudView] Shader link failed: %s\n", + program->log().isEmpty() + ? "unknown error" + : qPrintable(program->log())); + delete program; + return false; + } + + m_shader = program; + m_positionLocation = m_shader->attributeLocation("aPosition"); + m_colorLocation = m_shader->attributeLocation("aColor"); + m_mvpLocation = m_shader->uniformLocation("uMvp"); + m_pointSizeLocation = m_shader->uniformLocation("uPointSize"); + m_alphaLocation = m_shader->uniformLocation("uAlpha"); + m_useVertexColorLocation = m_shader->uniformLocation("uUseVertexColor"); + m_uniformColorLocation = m_shader->uniformLocation("uColor"); + } + return m_shader != nullptr; +} + +QVector3D PointCloudGLWidget::colorForIndex(int colorIndex) const +{ + static const QVector3D colorTable[COLOR_COUNT] = { + QVector3D(0.75f, 0.75f, 0.75f), // 浅灰 + QVector3D(0.3f, 1.0f, 0.3f), // 浅绿 + QVector3D(0.3f, 0.3f, 1.0f), // 浅蓝 + QVector3D(1.0f, 1.0f, 0.3f), // 黄 + QVector3D(0.3f, 1.0f, 1.0f), // 青 + QVector3D(1.0f, 0.3f, 1.0f), // 品红 + QVector3D(1.0f, 1.0f, 1.0f) // 白 + }; + return colorTable[colorIndex % COLOR_COUNT]; +} + +void PointCloudGLWidget::drawCloud(PointCloudData& data, float pointSize) +{ + if (data.vertices.empty() || !m_shader) { + return; + } + m_shader->bind(); + glUniformMatrix4fv(m_mvpLocation, 1, GL_FALSE, m_mvp.constData()); + glUniform1f(m_pointSizeLocation, pointSize); + glUniform1f(m_alphaLocation, 1.0f); + // 兜底:桌面 GL 未开启 GL_PROGRAM_POINT_SIZE 时,点大小取自 GL_POINT_SIZE 状态 + glPointSize(pointSize); + + data.vertexBuffer.bind(); + glEnableVertexAttribArray(m_positionLocation); + glVertexAttribPointer(m_positionLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr); + + const bool useVertexColor = data.colorBuffer.isCreated() && !data.colors.empty(); + if (useVertexColor) { + data.colorBuffer.bind(); + glEnableVertexAttribArray(m_colorLocation); + glVertexAttribPointer(m_colorLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr); + glUniform1f(m_useVertexColorLocation, 1.0f); + } else { + glDisableVertexAttribArray(m_colorLocation); + const QVector3D color = colorForIndex(data.colorIndex); + glUniform3f(m_uniformColorLocation, color.x(), color.y(), color.z()); + glUniform1f(m_useVertexColorLocation, 0.0f); + } + + glDrawArrays(GL_POINTS, 0, static_cast(data.vertices.size() / 3)); + + glDisableVertexAttribArray(m_positionLocation); + if (useVertexColor) { + glDisableVertexAttribArray(m_colorLocation); + } + data.vertexBuffer.release(); + data.colorBuffer.release(); +} + +void PointCloudGLWidget::drawPrimitives(const QMatrix4x4& mvp, + const float* positions, + int vertexCount, + GLenum mode, + const float* colors, + const QVector3D& uniformColor, + float pointSize, + float alpha, + bool depthTest) +{ + if (!positions || vertexCount <= 0 || !m_shader) { + return; + } + m_shader->bind(); + glUniformMatrix4fv(m_mvpLocation, 1, GL_FALSE, mvp.constData()); + glUniform1f(m_pointSizeLocation, pointSize); + glUniform1f(m_alphaLocation, alpha); + // 兜底:桌面 GL 未开启 GL_PROGRAM_POINT_SIZE 时,点大小取自 GL_POINT_SIZE 状态 + glPointSize(pointSize); + if (depthTest) { + glEnable(GL_DEPTH_TEST); + } else { + glDisable(GL_DEPTH_TEST); + } + + const GLsizeiptr positionBytes = + static_cast(vertexCount) * 3 * sizeof(float); + const GLsizeiptr colorBytes = + colors ? static_cast(vertexCount) * 3 * sizeof(float) : 0; + + m_dynamicVbo.bind(); + m_dynamicVbo.allocate(positionBytes + colorBytes); + m_dynamicVbo.write(0, positions, positionBytes); + if (colors) { + m_dynamicVbo.write(positionBytes, colors, colorBytes); + } + + glEnableVertexAttribArray(m_positionLocation); + glVertexAttribPointer(m_positionLocation, 3, GL_FLOAT, GL_FALSE, 0, nullptr); + + if (colors) { + glEnableVertexAttribArray(m_colorLocation); + glVertexAttribPointer(m_colorLocation, 3, GL_FLOAT, GL_FALSE, 0, + reinterpret_cast(positionBytes)); + glUniform1f(m_useVertexColorLocation, 1.0f); + } else { + glDisableVertexAttribArray(m_colorLocation); + glUniform3f(m_uniformColorLocation, + uniformColor.x(), uniformColor.y(), uniformColor.z()); + glUniform1f(m_useVertexColorLocation, 0.0f); + } + + glDrawArrays(mode, 0, vertexCount); + + m_dynamicVbo.release(); + glDisableVertexAttribArray(m_positionLocation); + if (colors) { + glDisableVertexAttribArray(m_colorLocation); + } + glEnable(GL_DEPTH_TEST); +} + +void PointCloudGLWidget::resetRenderState() +{ + glDisableVertexAttribArray(m_positionLocation); + glDisableVertexAttribArray(m_colorLocation); + m_dynamicVbo.release(); + if (m_shader) { + m_shader->release(); + } + glEnable(GL_DEPTH_TEST); } void PointCloudGLWidget::resizeGL(int w, int h) { glViewport(0, 0, w, h); - - m_projection.setToIdentity(); - float aspect = static_cast(w) / static_cast(h > 0 ? h : 1); - // 放大裁剪范围以容纳激光雷达点云(最大可能跨越数十万 mm) - // near 取 1.0 保留 depth buffer 精度,far 放到 1e7 避免远点被裁掉 - m_projection.perspective(45.0f, aspect, 1.0f, 1.0e7f); } void PointCloudGLWidget::paintGL() { glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + if (!ensureShader()) { + return; + } m_view.setToIdentity(); // 平移在相机空间中进行,使鼠标拖动方向与屏幕方向一致 @@ -150,17 +656,9 @@ void PointCloudGLWidget::paintGL() euler.x(), euler.y(), euler.z()); } - glMatrixMode(GL_PROJECTION); - glLoadMatrixf(m_projection.constData()); - - glMatrixMode(GL_MODELVIEW); QMatrix4x4 modelView = m_view * m_model; - glLoadMatrixf(modelView.constData()); - - // 确保颜色数组禁用 - glDisableClientState(GL_COLOR_ARRAY); - - glPointSize(m_pointSize); + updateProjection(modelView); + m_mvp = m_projection * modelView; for (size_t cloudIdx = 0; cloudIdx < m_pointClouds.size(); ++cloudIdx) { auto& cloudData = m_pointClouds[cloudIdx]; @@ -173,73 +671,64 @@ void PointCloudGLWidget::paintGL() uploadToVBO(cloudData); } - glEnableClientState(GL_VERTEX_ARRAY); - - if (cloudData.vboCreated && cloudData.vertexBuffer.isCreated()) { - // 使用 VBO 渲染:数据已在 GPU 显存中,无需每帧 CPU→GPU 拷贝 - cloudData.vertexBuffer.bind(); - glVertexPointer(3, GL_FLOAT, 0, nullptr); - cloudData.vertexBuffer.release(); - } else { - // 回退到 CPU 指针(VBO 创建失败时) - glVertexPointer(3, GL_FLOAT, 0, cloudData.vertices.data()); - } - - // 如果有原始颜色且选择显示原始颜色 - if (cloudData.hasColor && !cloudData.colors.empty()) { - glEnableClientState(GL_COLOR_ARRAY); - if (cloudData.vboCreated && cloudData.colorBuffer.isCreated()) { - cloudData.colorBuffer.bind(); - glColorPointer(3, GL_FLOAT, 0, nullptr); - cloudData.colorBuffer.release(); - } else { - glColorPointer(3, GL_FLOAT, 0, cloudData.colors.data()); - } - } else { - // 使用点云自己的颜色索引 - glDisableClientState(GL_COLOR_ARRAY); - setColorByIndex(cloudData.colorIndex); - } - - glDrawArrays(GL_POINTS, 0, static_cast(cloudData.vertices.size() / 3)); - - glDisableClientState(GL_VERTEX_ARRAY); - glDisableClientState(GL_COLOR_ARRAY); + drawCloud(cloudData, m_pointSize); // 绘制自定义大小的点(RGBA 中 A > 1 的点) if (cloudData.hasCustomPointSizes) { glDepthFunc(GL_LEQUAL); // 允许在相同深度覆盖绘制 + const bool cloudHasColor = cloudData.colorBuffer.isCreated() && + !cloudData.colors.empty(); for (const auto& group : cloudData.customPointSizeGroups) { - glPointSize(group.pointSize); - glBegin(GL_POINTS); + QVector positions; + positions.reserve(static_cast(group.indices.size()) * 3); + QVector colors; + if (cloudHasColor) { + colors.reserve(static_cast(group.indices.size()) * 3); + } for (size_t idx : group.indices) { - size_t vi = idx * 3; + const size_t vi = idx * 3; if (vi + 2 < cloudData.vertices.size()) { - if (cloudData.hasColor && vi + 2 < cloudData.colors.size()) { - glColor3f(cloudData.colors[vi], cloudData.colors[vi + 1], cloudData.colors[vi + 2]); + positions.append(cloudData.vertices[vi]); + positions.append(cloudData.vertices[vi + 1]); + positions.append(cloudData.vertices[vi + 2]); + if (cloudHasColor && vi + 2 < cloudData.colors.size()) { + colors.append(cloudData.colors[vi]); + colors.append(cloudData.colors[vi + 1]); + colors.append(cloudData.colors[vi + 2]); } - glVertex3f(cloudData.vertices[vi], cloudData.vertices[vi + 1], cloudData.vertices[vi + 2]); } } - glEnd(); + if (!positions.isEmpty()) { + drawPrimitives(m_mvp, + positions.constData(), + positions.size() / 3, + GL_POINTS, + cloudHasColor ? colors.constData() : nullptr, + colorForIndex(cloudData.colorIndex), + group.pointSize, + 1.0f, + true); + } } - glDepthFunc(GL_LESS); // 恢复默认深度测试 - glPointSize(m_pointSize); // 恢复默认点大小 + glDepthFunc(GL_LESS); // 恢复默认深度测试 } } drawSelectedPoints(); - drawMeasurementLine(); - drawSelectedLine(); - drawLineSegments(); - drawBasicShapeSurfaces(); - drawBasicShapeSegments(); - drawBasicShapePoints(); - drawPosePoints(); + drawMeasurementLine(); + drawSelectedLine(); + drawLineSegments(); + drawBasicShapeSurfaces(); + drawBasicShapeSegments(); + drawBasicShapePoints(); + drawPosePoints(); // 最后绘制坐标系指示器(覆盖在所有内容之上) drawAxis(); + // 清理 GL 状态,避免影响 QPainter 标注 + resetRenderState(); + // 使用 QPainter 绘制坐标轴 XYZ 标注 drawAxisLabels(); } @@ -444,6 +933,98 @@ void PointCloudGLWidget::addPointCloud(const PointCloudXYZRGB& cloud, const QStr update(); } +void PointCloudGLWidget::appendPointCloud(const PointCloudXYZRGB& cloud, + const QString& name) +{ + if (cloud.empty()) { + return; + } + if (m_pointClouds.empty() || !m_pointClouds.back().hasColor) { + addPointCloud(cloud, name); + return; + } + + PointCloudData& data = m_pointClouds.back(); + if (!name.isEmpty()) { + data.name = name; + } + + const bool incomingHasLineInfo = !cloud.lineIndices.empty(); + data.hasLineInfo = data.hasLineInfo || incomingHasLineInfo; + const size_t originalIndexOffset = data.vertices.size() / 3; + const float epsilon = 1e-6f; + int previousLineIndex = -1; + int pointInLineIndex = 0; + + for (size_t index = 0; index < cloud.points.size(); ++index) { + const Point3DRGB& point = cloud.points[index]; + int lineIndex = -1; + if (incomingHasLineInfo && index < cloud.lineIndices.size()) { + lineIndex = cloud.lineIndices[index]; + } + if (lineIndex != previousLineIndex) { + pointInLineIndex = 0; + previousLineIndex = lineIndex; + } + + if (!std::isfinite(point.x) || !std::isfinite(point.y) || + !std::isfinite(point.z) || + (std::fabs(point.x) < epsilon && + std::fabs(point.y) < epsilon && + std::fabs(point.z) < epsilon)) { + ++pointInLineIndex; + continue; + } + + const size_t pointIndex = data.vertices.size() / 3; + data.vertices.push_back(point.x); + data.vertices.push_back(point.y); + data.vertices.push_back(point.z); + data.colors.push_back(point.r / 255.0f); + data.colors.push_back(point.g / 255.0f); + data.colors.push_back(point.b / 255.0f); + data.originalIndices.push_back( + static_cast(originalIndexOffset + index)); + data.pointInLineIndices.push_back(pointInLineIndex); + + if (lineIndex >= 0) { + data.lineIndices.push_back(lineIndex); + data.totalLines = qMax(data.totalLines, lineIndex + 1); + } + + if (point.pointSize > 1.0f) { + auto sizeGroup = std::find_if( + data.customPointSizeGroups.begin(), + data.customPointSizeGroups.end(), + [&point](const PointCloudData::PointSizeGroup& group) { + return group.pointSize == point.pointSize; + }); + if (sizeGroup == data.customPointSizeGroups.end()) { + PointCloudData::PointSizeGroup group; + group.pointSize = point.pointSize; + data.customPointSizeGroups.push_back(std::move(group)); + sizeGroup = data.customPointSizeGroups.end() - 1; + } + sizeGroup->indices.push_back(pointIndex); + data.hasCustomPointSizes = true; + } + ++pointInLineIndex; + } + + if (data.totalLines > 0) { + data.pointsPerLine = + static_cast(data.vertices.size() / 3) / data.totalLines; + } + + makeCurrent(); + uploadToVBO(data); + doneCurrent(); + + computeBoundingBox(); + m_distance = qMax(m_distance, fittedCameraDistance()); + update(); +} + void PointCloudGLWidget::clearPointClouds() { // 释放所有 VBO @@ -454,13 +1035,13 @@ void PointCloudGLWidget::clearPointClouds() doneCurrent(); m_pointClouds.clear(); - m_selectedPoints.clear(); - m_selectedLine = SelectedLineInfo(); - m_lineSegments.clear(); - m_basicShapeSegments.clear(); - m_basicShapeSurfaces.clear(); - m_basicShapePoints.clear(); - m_posePoints.clear(); + m_selectedPoints.clear(); + m_selectedLine = SelectedLineInfo(); + m_lineSegments.clear(); + m_basicShapeSegments.clear(); + m_basicShapeSurfaces.clear(); + m_basicShapePoints.clear(); + m_posePoints.clear(); m_minBound = QVector3D(-50, -50, -50); m_maxBound = QVector3D(50, 50, 50); m_center = QVector3D(0, 0, 0); @@ -481,14 +1062,11 @@ void PointCloudGLWidget::setPointSize(float size) update(); } -void PointCloudGLWidget::resetView() -{ - QVector3D size = m_maxBound - m_minBound; - float maxSize = qMax(qMax(size.x(), size.y()), size.z()); +void PointCloudGLWidget::resetView() +{ + QVector3D size = m_maxBound - m_minBound; + float maxSize = qMax(qMax(size.x(), size.y()), size.z()); - // 视距至少为 maxSize 的 2.5 倍,并确保不小于 10 - // 上限放宽至 1e7,适配激光雷达大范围点云 - m_distance = qBound(10.0f, maxSize * 2.5f, 1.0e7f); // 默认视角:面向 XY 平面,X 向右,Y 向下 // 绕 X 轴旋转 180°:Y 轴从向上翻转为向下 m_rotationX = 180.0f; @@ -496,43 +1074,140 @@ void PointCloudGLWidget::resetView() m_rotationZ = 0.0f; m_rotation = QQuaternion::fromEulerAngles(m_rotationX, m_rotationY, m_rotationZ); m_pan = QVector3D(0, 0, 0); + m_distance = fittedCameraDistance(); CLOUD_LOG_INFO("[CloudView] resetView: size=(%.3f, %.3f, %.3f) maxSize=%.3f distance=%.3f\n", size.x(), size.y(), size.z(), maxSize, m_distance); - emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ); - update(); -} - -void PointCloudGLWidget::fitToContent() -{ - computeBoundingBox(); - resetView(); -} - -void PointCloudGLWidget::zoomIn() -{ - m_distance *= 0.9f; - m_distance = qBound(0.1f, m_distance, 1.0e7f); + emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ); + update(); +} + +void PointCloudGLWidget::fitToContent() +{ + computeBoundingBox(); + resetView(); +} + +void PointCloudGLWidget::updateProjection(const QMatrix4x4& modelView) +{ + const QVector3D corners[] = { + {m_minBound.x(), m_minBound.y(), m_minBound.z()}, + {m_minBound.x(), m_minBound.y(), m_maxBound.z()}, + {m_minBound.x(), m_maxBound.y(), m_minBound.z()}, + {m_minBound.x(), m_maxBound.y(), m_maxBound.z()}, + {m_maxBound.x(), m_minBound.y(), m_minBound.z()}, + {m_maxBound.x(), m_minBound.y(), m_maxBound.z()}, + {m_maxBound.x(), m_maxBound.y(), m_minBound.z()}, + {m_maxBound.x(), m_maxBound.y(), m_maxBound.z()} + }; + + float minimumDepth = FLT_MAX; + float maximumDepth = -FLT_MAX; + for (const QVector3D& corner : corners) { + const float depth = -modelView.map(corner).z(); + minimumDepth = qMin(minimumDepth, depth); + maximumDepth = qMax(maximumDepth, depth); + } + + constexpr float kMinimumNearPlane = 0.01f; + float nearPlane = kMinimumNearPlane; + float farPlane = 1.0f; + if (maximumDepth > kMinimumNearPlane) { + // 点云贴近或跨过相机时保持很小的近裁剪距离,避免前侧点被裁掉。 + if (minimumDepth > kMinimumNearPlane) { + nearPlane = qMax(kMinimumNearPlane, minimumDepth * 0.5f); + } + // 远裁剪面仅覆盖当前内容,并限制 near/far 比值以保证深度缓冲精度。 + farPlane = qMax(nearPlane + 1.0f, maximumDepth * 1.5f); + nearPlane = qMax(nearPlane, farPlane / 100000.0f); + } + + const float aspect = static_cast(width()) / + static_cast(height() > 0 ? height() : 1); + m_projection.setToIdentity(); + m_projection.perspective(45.0f, aspect, nearPlane, farPlane); +} + +float PointCloudGLWidget::fittedCameraDistance(float padding) const +{ + const QVector3D corners[] = { + {m_minBound.x(), m_minBound.y(), m_minBound.z()}, + {m_minBound.x(), m_minBound.y(), m_maxBound.z()}, + {m_minBound.x(), m_maxBound.y(), m_minBound.z()}, + {m_minBound.x(), m_maxBound.y(), m_maxBound.z()}, + {m_maxBound.x(), m_minBound.y(), m_minBound.z()}, + {m_maxBound.x(), m_minBound.y(), m_maxBound.z()}, + {m_maxBound.x(), m_maxBound.y(), m_minBound.z()}, + {m_maxBound.x(), m_maxBound.y(), m_maxBound.z()}}; + + const float aspect = qMax(0.01f, + static_cast(width()) / + static_cast(height() > 0 ? height() : 1)); + const float tangent = std::tan(qDegreesToRadians(45.0f * 0.5f)); + const float safePadding = qMax(1.0f, padding); + float distance = minimumCameraDistance(); + for (const QVector3D& corner : corners) { + const QVector3D relative = + m_rotation.rotatedVector(corner - m_center); + const float horizontalDistance = + relative.z() + std::fabs(relative.x()) * safePadding / (tangent * aspect); + const float verticalDistance = + relative.z() + std::fabs(relative.y()) * safePadding / tangent; + distance = qMax(distance, qMax(horizontalDistance, verticalDistance)); + } + return qBound(10.0f, distance, 1.0e7f); +} + +float PointCloudGLWidget::minimumCameraDistance() const +{ + const QVector3D corners[] = { + {m_minBound.x(), m_minBound.y(), m_minBound.z()}, + {m_minBound.x(), m_minBound.y(), m_maxBound.z()}, + {m_minBound.x(), m_maxBound.y(), m_minBound.z()}, + {m_minBound.x(), m_maxBound.y(), m_maxBound.z()}, + {m_maxBound.x(), m_minBound.y(), m_minBound.z()}, + {m_maxBound.x(), m_minBound.y(), m_maxBound.z()}, + {m_maxBound.x(), m_maxBound.y(), m_minBound.z()}, + {m_maxBound.x(), m_maxBound.y(), m_maxBound.z()} + }; + + float frontExtent = 0.0f; + for (const QVector3D& corner : corners) { + const QVector3D relativePosition = corner - m_center; + frontExtent = qMax(frontExtent, + m_rotation.rotatedVector(relativePosition).z()); + } + + const float margin = qMax(0.1f, (m_maxBound - m_minBound).length() * 0.002f); + return qBound(0.1f, frontExtent + margin, 1.0e7f); +} + +void PointCloudGLWidget::zoomIn() +{ + m_distance *= 0.9f; + m_distance = qBound(minimumCameraDistance(), m_distance, 1.0e7f); update(); } void PointCloudGLWidget::zoomOut() { m_distance *= 1.1f; - m_distance = qBound(0.1f, m_distance, 1.0e7f); + m_distance = qBound(minimumCameraDistance(), m_distance, 1.0e7f); update(); } void PointCloudGLWidget::setViewAngles(float rotX, float rotY, float rotZ) { - m_rotationX = rotX; - m_rotationY = rotY; - m_rotationZ = rotZ; + // 归一化到 ±180°,避免往同一方向旋转时角度无限累加 + m_rotationX = NormalizeAngleDegrees(rotX); + m_rotationY = NormalizeAngleDegrees(rotY); + m_rotationZ = NormalizeAngleDegrees(rotZ); m_pan = QVector3D(0, 0, 0); // 从欧拉角转换为四元数(ZYX顺序) - m_rotation = QQuaternion::fromEulerAngles(rotX, rotY, rotZ); + m_rotation = QQuaternion::fromEulerAngles(m_rotationX, m_rotationY, m_rotationZ); + m_distance = qMax(m_distance, minimumCameraDistance()); CLOUD_LOG_INFO("[CloudView] setViewAngles: rotX=%.1f, rotY=%.1f, rotZ=%.1f\n", rotX, rotY, rotZ); @@ -861,99 +1536,68 @@ size_t PointCloudGLWidget::getAllCloudsByLines(std::vector(width()), static_cast(height())}; + const QMatrix4x4 modelView = m_view * m_model; + const QMatrix4x4 mvp = m_projection * modelView; // 转换屏幕 Y 坐标(OpenGL Y 轴向上) - int glScreenY = viewport[3] - screenY; + const int glScreenY = viewport[3] - screenY; float minScreenDist = FLT_MAX; size_t bestIndex = 0; @@ -985,18 +1626,24 @@ SelectedPointInfo PointCloudGLWidget::pickPoint(int screenX, int screenY) for (size_t cloudIdx = 0; cloudIdx < m_pointClouds.size(); ++cloudIdx) { const auto& cloudData = m_pointClouds[cloudIdx]; for (size_t i = 0; i < cloudData.vertices.size(); i += 3) { - float wx = cloudData.vertices[i]; - float wy = cloudData.vertices[i + 1]; - float wz = cloudData.vertices[i + 2]; + const float wx = cloudData.vertices[i]; + const float wy = cloudData.vertices[i + 1]; + const float wz = cloudData.vertices[i + 2]; - // 将世界坐标投影到屏幕 - GLdouble sx, sy, sz; - gluProject(wx, wy, wz, modelMatrix, projMatrix, viewport, &sx, &sy, &sz); + // 将世界坐标投影到屏幕(等价于 gluProject) + const QVector4D clip = mvp * QVector4D(wx, wy, wz, 1.0f); + if (std::fabs(clip.w()) < 1e-9f) { + continue; + } + const float ndcX = clip.x() / clip.w(); + const float ndcY = clip.y() / clip.w(); + const float sx = viewport[0] + (ndcX + 1.0f) * 0.5f * viewport[2]; + const float sy = viewport[1] + (ndcY + 1.0f) * 0.5f * viewport[3]; // 计算屏幕距离 - float dx = static_cast(sx) - screenX; - float dy = static_cast(sy) - glScreenY; - float screenDist = dx * dx + dy * dy; + const float dx = sx - screenX; + const float dy = sy - glScreenY; + const float screenDist = dx * dx + dy * dy; if (screenDist < minScreenDist) { minScreenDist = screenDist; @@ -1047,29 +1694,43 @@ SelectedPointInfo PointCloudGLWidget::pickPoint(int screenX, int screenY) void PointCloudGLWidget::drawSelectedPoints() { - glPointSize(10.0f); - glDisable(GL_DEPTH_TEST); - - glBegin(GL_POINTS); + QVector positions; + QVector colors; // 绘制选中的点(橙色) for (const auto& pt : m_selectedPoints) { if (pt.valid) { - glColor3f(1.0f, 0.5f, 0.0f); // 橙色 - glVertex3f(pt.x, pt.y, pt.z); + positions.append(pt.x); + positions.append(pt.y); + positions.append(pt.z); + colors.append(1.0f); + colors.append(0.5f); + colors.append(0.0f); } } // 绘制列表高亮点(蓝色,与选点区分) if (m_hasListHighlightPoint) { - glColor3f(0.0f, 0.5f, 1.0f); // 蓝色 - glVertex3f(m_listHighlightPoint.x(), m_listHighlightPoint.y(), m_listHighlightPoint.z()); + positions.append(m_listHighlightPoint.x()); + positions.append(m_listHighlightPoint.y()); + positions.append(m_listHighlightPoint.z()); + colors.append(0.0f); + colors.append(0.5f); + colors.append(1.0f); } - glEnd(); - - glEnable(GL_DEPTH_TEST); - glPointSize(m_pointSize); + if (positions.isEmpty()) { + return; + } + drawPrimitives(m_mvp, + positions.constData(), + positions.size() / 3, + GL_POINTS, + colors.constData(), + QVector3D(1.0f, 1.0f, 1.0f), + 10.0f, + 1.0f, + false); } void PointCloudGLWidget::drawMeasurementLine() @@ -1085,17 +1746,18 @@ void PointCloudGLWidget::drawMeasurementLine() return; } - glDisable(GL_DEPTH_TEST); - glLineWidth(2.0f); - - glBegin(GL_LINES); - glColor3f(0.0f, 1.0f, 0.0f); - glVertex3f(p1.x, p1.y, p1.z); - glVertex3f(p2.x, p2.y, p2.z); - glEnd(); - - glLineWidth(1.0f); - glEnable(GL_DEPTH_TEST); + const float positions[] = { + p1.x, p1.y, p1.z, + p2.x, p2.y, p2.z}; + drawPrimitives(m_mvp, + positions, + 2, + GL_LINES, + nullptr, + QVector3D(0.0f, 1.0f, 0.0f), + 1.0f, + 1.0f, + false); } void PointCloudGLWidget::drawSelectedLine() @@ -1113,22 +1775,18 @@ void PointCloudGLWidget::drawSelectedLine() return; } - // 高亮显示选中线上的所有点 - glPointSize(3.0f); - glDisable(GL_DEPTH_TEST); - - glBegin(GL_POINTS); - glColor3f(1.0f, 1.0f, 0.0f); // 黄色高亮 + // 高亮显示选中线上的所有点(黄色) + QVector positions; if (m_selectedLine.mode == LineSelectMode::Vertical) { // 纵向选线:显示同一条扫描线上的所有点 for (size_t i = 0; i < cloudData.lineIndices.size(); ++i) { if (cloudData.lineIndices[i] == m_selectedLine.lineIndex) { - size_t vertIdx = i * 3; + const size_t vertIdx = i * 3; if (vertIdx + 2 < cloudData.vertices.size()) { - glVertex3f(cloudData.vertices[vertIdx], - cloudData.vertices[vertIdx + 1], - cloudData.vertices[vertIdx + 2]); + positions.append(cloudData.vertices[vertIdx]); + positions.append(cloudData.vertices[vertIdx + 1]); + positions.append(cloudData.vertices[vertIdx + 2]); } } } @@ -1136,23 +1794,32 @@ void PointCloudGLWidget::drawSelectedLine() // 横向选线:显示所有线的相同原始索引点 if (cloudData.pointsPerLine > 0 && m_selectedLine.pointIndex >= 0) { for (size_t i = 0; i < cloudData.originalIndices.size(); ++i) { - int originalIdx = cloudData.originalIndices[i]; + const int originalIdx = cloudData.originalIndices[i]; // 原始索引 % 每线点数 == 选中的点索引 if (originalIdx % cloudData.pointsPerLine == m_selectedLine.pointIndex) { - size_t vertIdx = i * 3; + const size_t vertIdx = i * 3; if (vertIdx + 2 < cloudData.vertices.size()) { - glVertex3f(cloudData.vertices[vertIdx], - cloudData.vertices[vertIdx + 1], - cloudData.vertices[vertIdx + 2]); + positions.append(cloudData.vertices[vertIdx]); + positions.append(cloudData.vertices[vertIdx + 1]); + positions.append(cloudData.vertices[vertIdx + 2]); } } } } } - glEnd(); - glEnable(GL_DEPTH_TEST); - glPointSize(m_pointSize); + if (positions.isEmpty()) { + return; + } + drawPrimitives(m_mvp, + positions.constData(), + positions.size() / 3, + GL_POINTS, + nullptr, + QVector3D(1.0f, 1.0f, 0.0f), + 3.0f, + 1.0f, + false); } void PointCloudGLWidget::drawAxis() @@ -1170,58 +1837,35 @@ void PointCloudGLWidget::drawAxis() int axisX = viewport[2] - axisSize - margin; // 右下角 X int axisY = margin; // 右下角 Y(OpenGL Y 轴向上) - // 保存当前矩阵状态 - glMatrixMode(GL_PROJECTION); - glPushMatrix(); - glLoadIdentity(); - // 设置正交投影,覆盖整个视口 - glOrtho(0, viewport[2], 0, viewport[3], -100, 100); + // 正交投影 + 视图变换(等价于原 glOrtho + glTranslatef + glMultMatrixf) + QMatrix4x4 projection; + projection.setToIdentity(); + projection.ortho(0, viewport[2], 0, viewport[3], -100, 100); - glMatrixMode(GL_MODELVIEW); - glPushMatrix(); - glLoadIdentity(); + QMatrix4x4 modelView; + modelView.setToIdentity(); + modelView.translate(axisX + axisSize / 2.0f, axisY + axisSize / 2.0f, 0.0f); + QMatrix4x4 rotation; + rotation.rotate(m_rotation); + modelView *= rotation; - // 移动到右下角中心位置 - glTranslatef(axisX + axisSize / 2.0f, axisY + axisSize / 2.0f, 0.0f); + const QMatrix4x4 mvp = projection * modelView; - // 应用当前视图的旋转(使用四元数转换为矩阵) - QMatrix4x4 rotMatrix; - rotMatrix.rotate(m_rotation); - glMultMatrixf(rotMatrix.constData()); + const float axisLength = axisSize * 0.4f; // 坐标轴长度 + const float positions[] = { + 0.0f, 0.0f, 0.0f, axisLength, 0.0f, 0.0f, // X 轴 - 红色(向右) + 0.0f, 0.0f, 0.0f, 0.0f, axisLength, 0.0f, // Y 轴 - 绿色(向下) + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, axisLength // Z 轴 - 蓝色(向后) + }; + const float colors[] = { + 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f + }; // 关闭深度测试,确保坐标系始终可见 - glDisable(GL_DEPTH_TEST); - - float axisLength = axisSize * 0.4f; // 坐标轴长度 - glLineWidth(2.0f); - glBegin(GL_LINES); - - // X 轴 - 红色(向右) - glColor3f(1.0f, 0.0f, 0.0f); - glVertex3f(0.0f, 0.0f, 0.0f); - glVertex3f(axisLength, 0.0f, 0.0f); - - // Y 轴 - 绿色(向下,在屏幕坐标中是向上显示) - glColor3f(0.0f, 1.0f, 0.0f); - glVertex3f(0.0f, 0.0f, 0.0f); - glVertex3f(0.0f, axisLength, 0.0f); - - // Z 轴 - 蓝色(向后,在屏幕坐标中是向观察者) - glColor3f(0.0f, 0.0f, 1.0f); - glVertex3f(0.0f, 0.0f, 0.0f); - glVertex3f(0.0f, 0.0f, axisLength); - - glEnd(); - glLineWidth(1.0f); - - // 恢复深度测试 - glEnable(GL_DEPTH_TEST); - - // 恢复矩阵状态 - glPopMatrix(); - glMatrixMode(GL_PROJECTION); - glPopMatrix(); - glMatrixMode(GL_MODELVIEW); + drawPrimitives(mvp, positions, 6, GL_LINES, colors, + QVector3D(1.0f, 1.0f, 1.0f), 1.0f, 1.0f, false); } void PointCloudGLWidget::drawAxisLabels() @@ -1300,14 +1944,14 @@ void PointCloudGLWidget::mousePressEvent(QMouseEvent* event) emit pointSelected(point); - if (m_selectedPoints.size() == 2) { - const float distance = calculateDistance(m_selectedPoints[0], m_selectedPoints[1]); - const float dx = m_selectedPoints[1].x - m_selectedPoints[0].x; - const float dy = m_selectedPoints[1].y - m_selectedPoints[0].y; - const float dz = m_selectedPoints[1].z - m_selectedPoints[0].z; - emit twoPointsSelected(m_selectedPoints[0], m_selectedPoints[1], distance); - emit measurementFinished(m_selectedPoints[0], m_selectedPoints[1], distance, dx, dy, dz); - } + if (m_selectedPoints.size() == 2) { + const float distance = calculateDistance(m_selectedPoints[0], m_selectedPoints[1]); + const float dx = m_selectedPoints[1].x - m_selectedPoints[0].x; + const float dy = m_selectedPoints[1].y - m_selectedPoints[0].y; + const float dz = m_selectedPoints[1].z - m_selectedPoints[0].z; + emit twoPointsSelected(m_selectedPoints[0], m_selectedPoints[1], distance); + emit measurementFinished(m_selectedPoints[0], m_selectedPoints[1], distance, dx, dy, dz); + } } else { // 未启用测距:只保留一个点 m_selectedPoints.clear(); @@ -1388,14 +2032,14 @@ void PointCloudGLWidget::mouseDoubleClickEvent(QMouseEvent* event) emit pointSelected(point); - if (m_selectedPoints.size() == 2) { - const float distance = calculateDistance(m_selectedPoints[0], m_selectedPoints[1]); - const float dx = m_selectedPoints[1].x - m_selectedPoints[0].x; - const float dy = m_selectedPoints[1].y - m_selectedPoints[0].y; - const float dz = m_selectedPoints[1].z - m_selectedPoints[0].z; - emit twoPointsSelected(m_selectedPoints[0], m_selectedPoints[1], distance); - emit measurementFinished(m_selectedPoints[0], m_selectedPoints[1], distance, dx, dy, dz); - } + if (m_selectedPoints.size() == 2) { + const float distance = calculateDistance(m_selectedPoints[0], m_selectedPoints[1]); + const float dx = m_selectedPoints[1].x - m_selectedPoints[0].x; + const float dy = m_selectedPoints[1].y - m_selectedPoints[0].y; + const float dz = m_selectedPoints[1].z - m_selectedPoints[0].z; + emit twoPointsSelected(m_selectedPoints[0], m_selectedPoints[1], distance); + emit measurementFinished(m_selectedPoints[0], m_selectedPoints[1], distance, dx, dy, dz); + } } else { // 未启用测距:只保留一个点 m_selectedPoints.clear(); @@ -1420,7 +2064,7 @@ void PointCloudGLWidget::mouseMoveEvent(QMouseEvent* event) float angle = -delta.x() * 0.5f; // 取反修正方向 QQuaternion deltaRotation = QQuaternion::fromAxisAndAngle(QVector3D(0, 0, 1), angle); m_rotation = deltaRotation * m_rotation; - m_rotationZ += angle; + m_rotationZ = NormalizeAngleDegrees(m_rotationZ + angle); } else { // 普通左键拖动:基于当前物体坐标系的旋转 // 鼠标水平移动 -> 绕当前Y轴旋转 @@ -1439,12 +2083,13 @@ void PointCloudGLWidget::mouseMoveEvent(QMouseEvent* event) // 应用增量旋转(左乘,相机空间旋转) m_rotation = deltaRotation * m_rotation; - // 更新欧拉角(用于显示) - m_rotationX += angleX; - m_rotationY += angleY; + // 更新欧拉角(用于显示),归一化到 ±180° + m_rotationX = NormalizeAngleDegrees(m_rotationX + angleX); + m_rotationY = NormalizeAngleDegrees(m_rotationY + angleY); } // 发送角度变化信号 + m_distance = qMax(m_distance, minimumCameraDistance()); emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ); update(); } else if (m_middleButtonPressed) { @@ -1457,7 +2102,8 @@ void PointCloudGLWidget::mouseMoveEvent(QMouseEvent* event) float angle = -delta.x() * 0.5f; // 取反修正方向 QQuaternion deltaRotation = QQuaternion::fromAxisAndAngle(QVector3D(0, 0, 1), angle); m_rotation = deltaRotation * m_rotation; - m_rotationZ += angle; + m_rotationZ = NormalizeAngleDegrees(m_rotationZ + angle); + m_distance = qMax(m_distance, minimumCameraDistance()); emit viewAnglesChanged(m_rotationX, m_rotationY, m_rotationZ); update(); } @@ -1478,7 +2124,7 @@ void PointCloudGLWidget::wheelEvent(QWheelEvent* event) { float delta = event->angleDelta().y() / 120.0f; m_distance *= (1.0f - delta * 0.1f); - m_distance = qBound(0.1f, m_distance, 1.0e7f); + m_distance = qBound(minimumCameraDistance(), m_distance, 1.0e7f); update(); } @@ -1595,55 +2241,55 @@ void PointCloudGLWidget::clearLineSegments() update(); } -void PointCloudGLWidget::setBasicShapeSegments(const QVector& segments) -{ - m_basicShapeSegments = segments; - update(); -} - -void PointCloudGLWidget::appendBasicShapeSegments(const QVector& segments) -{ - if (segments.isEmpty()) { - return; - } - - for (const LineSegment& segment : segments) { - m_basicShapeSegments.append(segment); - } - update(); -} - -void PointCloudGLWidget::appendBasicShapeSurfaces(const QVector& surfaces) -{ - if (surfaces.isEmpty()) { - return; - } - - for (const SurfaceQuad& surface : surfaces) { - m_basicShapeSurfaces.append(surface); - } - update(); -} - -void PointCloudGLWidget::appendBasicShapePoints(const QVector& points) -{ - if (points.isEmpty()) { - return; - } - - for (const BasicShapePoint& point : points) { - m_basicShapePoints.append(point); - } - update(); -} - -void PointCloudGLWidget::clearBasicShapeSegments() -{ - m_basicShapeSegments.clear(); - m_basicShapeSurfaces.clear(); - m_basicShapePoints.clear(); - update(); -} +void PointCloudGLWidget::setBasicShapeSegments(const QVector& segments) +{ + m_basicShapeSegments = segments; + update(); +} + +void PointCloudGLWidget::appendBasicShapeSegments(const QVector& segments) +{ + if (segments.isEmpty()) { + return; + } + + for (const LineSegment& segment : segments) { + m_basicShapeSegments.append(segment); + } + update(); +} + +void PointCloudGLWidget::appendBasicShapeSurfaces(const QVector& surfaces) +{ + if (surfaces.isEmpty()) { + return; + } + + for (const SurfaceQuad& surface : surfaces) { + m_basicShapeSurfaces.append(surface); + } + update(); +} + +void PointCloudGLWidget::appendBasicShapePoints(const QVector& points) +{ + if (points.isEmpty()) { + return; + } + + for (const BasicShapePoint& point : points) { + m_basicShapePoints.append(point); + } + update(); +} + +void PointCloudGLWidget::clearBasicShapeSegments() +{ + m_basicShapeSegments.clear(); + m_basicShapeSurfaces.clear(); + m_basicShapePoints.clear(); + update(); +} void PointCloudGLWidget::addPosePoints(const QVector& poses) { @@ -1657,82 +2303,94 @@ void PointCloudGLWidget::clearPosePoints() update(); } -void PointCloudGLWidget::drawLineSegments() -{ - drawLineSegmentList(m_lineSegments); -} - -void PointCloudGLWidget::drawBasicShapeSurfaces() -{ - if (m_basicShapeSurfaces.isEmpty()) { - return; - } - - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glDepthMask(GL_FALSE); - - glBegin(GL_QUADS); - for (const SurfaceQuad& surface : m_basicShapeSurfaces) { - glColor4f(surface.r, surface.g, surface.b, surface.a); - glVertex3f(surface.p1.x(), surface.p1.y(), surface.p1.z()); - glVertex3f(surface.p2.x(), surface.p2.y(), surface.p2.z()); - glVertex3f(surface.p3.x(), surface.p3.y(), surface.p3.z()); - glVertex3f(surface.p4.x(), surface.p4.y(), surface.p4.z()); - } - glEnd(); - - glDepthMask(GL_TRUE); - glDisable(GL_BLEND); -} - -void PointCloudGLWidget::drawBasicShapeSegments() -{ - drawLineSegmentList(m_basicShapeSegments); -} - -void PointCloudGLWidget::drawBasicShapePoints() -{ - if (m_basicShapePoints.isEmpty()) { - return; - } - - glDisable(GL_DEPTH_TEST); - for (const BasicShapePoint& point : m_basicShapePoints) { - glPointSize(point.pointSize); - glBegin(GL_POINTS); - glColor3f(point.r, point.g, point.b); - glVertex3f(point.x, point.y, point.z); - glEnd(); - } - glEnable(GL_DEPTH_TEST); - glPointSize(m_pointSize); -} - -void PointCloudGLWidget::drawLineSegmentList(const QVector& segments) -{ - if (segments.isEmpty()) { - return; +void PointCloudGLWidget::drawLineSegments() +{ + drawLineSegmentList(m_lineSegments); +} + +void PointCloudGLWidget::drawBasicShapeSurfaces() +{ + if (m_basicShapeSurfaces.isEmpty()) { + return; + } + + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + glDepthMask(GL_FALSE); + + for (const SurfaceQuad& surface : m_basicShapeSurfaces) { + // GLES2 无 GL_QUADS,四边形拆成两个三角形 (p1,p2,p3) 和 (p1,p3,p4) + const float positions[] = { + surface.p1.x(), surface.p1.y(), surface.p1.z(), + surface.p2.x(), surface.p2.y(), surface.p2.z(), + surface.p3.x(), surface.p3.y(), surface.p3.z(), + surface.p1.x(), surface.p1.y(), surface.p1.z(), + surface.p3.x(), surface.p3.y(), surface.p3.z(), + surface.p4.x(), surface.p4.y(), surface.p4.z(), + }; + drawPrimitives(m_mvp, positions, 6, GL_TRIANGLES, nullptr, + QVector3D(surface.r, surface.g, surface.b), + 1.0f, surface.a, true); + } + + glDepthMask(GL_TRUE); + glDisable(GL_BLEND); +} + +void PointCloudGLWidget::drawBasicShapeSegments() +{ + drawLineSegmentList(m_basicShapeSegments); +} + +void PointCloudGLWidget::drawBasicShapePoints() +{ + if (m_basicShapePoints.isEmpty()) { + return; + } + + for (const BasicShapePoint& point : m_basicShapePoints) { + const float positions[] = {point.x, point.y, point.z}; + drawPrimitives(m_mvp, positions, 1, GL_POINTS, nullptr, + QVector3D(point.r, point.g, point.b), + point.pointSize, 1.0f, false); + } +} + +void PointCloudGLWidget::drawLineSegmentList(const QVector& segments) +{ + if (segments.isEmpty()) { + return; } // 按线宽分组绘制 // 收集所有不同的线宽值(0 表示默认 2.0f) std::map> widthGroups; for (int i = 0; i < segments.size(); ++i) { - float w = segments[i].lineWidth > 0 ? segments[i].lineWidth : 2.0f; + const float w = segments[i].lineWidth > 0 ? segments[i].lineWidth : 2.0f; widthGroups[w].append(i); } for (const auto& group : widthGroups) { - glLineWidth(group.first); - glBegin(GL_LINES); + QVector positions; + QVector colors; + positions.reserve(group.second.size() * 6); + colors.reserve(group.second.size() * 6); for (int idx : group.second) { - const auto& seg = segments[idx]; - glColor3f(seg.r, seg.g, seg.b); - glVertex3f(seg.x1, seg.y1, seg.z1); - glVertex3f(seg.x2, seg.y2, seg.z2); + const LineSegment& seg = segments[idx]; + positions.append(seg.x1); positions.append(seg.y1); positions.append(seg.z1); + positions.append(seg.x2); positions.append(seg.y2); positions.append(seg.z2); + colors.append(seg.r); colors.append(seg.g); colors.append(seg.b); + colors.append(seg.r); colors.append(seg.g); colors.append(seg.b); } - glEnd(); + if (positions.isEmpty()) { + continue; + } + // GLES2 下 glLineWidth 有效宽度常受限(部分 Mali 仅支持 1.0), + // 仍按原线宽设置,驱动不支持时自动退化为最接近的可用宽度。 + glLineWidth(group.first); + drawPrimitives(m_mvp, positions.constData(), positions.size() / 3, + GL_LINES, colors.constData(), + QVector3D(1.0f, 1.0f, 1.0f), 1.0f, 1.0f, true); } glLineWidth(1.0f); @@ -1747,67 +2405,58 @@ void PointCloudGLWidget::drawPosePoints() glLineWidth(2.0f); for (const auto& pose : m_posePoints) { - glPushMatrix(); - - // 移动到姿态点位置 - glTranslatef(pose.x, pose.y, pose.z); - - // 根据选择的欧拉角旋转顺序应用旋转 - // OpenGL的glRotatef是右乘,所以需要反向写 + // 移动到姿态点位置,并按欧拉角顺序旋转(QMatrix4x4::rotate 与 glRotatef 同为右乘) + QMatrix4x4 modelView; + modelView.setToIdentity(); + modelView.translate(pose.x, pose.y, pose.z); switch (m_eulerRotationOrder) { case EulerRotationOrder::ZYX: // Yaw-Pitch-Roll(最常用) - glRotatef(pose.rx, 1, 0, 0); // X轴旋转(最后应用) - glRotatef(pose.ry, 0, 1, 0); // Y轴旋转(第二个应用) - glRotatef(pose.rz, 0, 0, 1); // Z轴旋转(第一个应用) + modelView.rotate(pose.rx, 1, 0, 0); + modelView.rotate(pose.ry, 0, 1, 0); + modelView.rotate(pose.rz, 0, 0, 1); break; case EulerRotationOrder::XYZ: // Roll-Pitch-Yaw - glRotatef(pose.rz, 0, 0, 1); - glRotatef(pose.ry, 0, 1, 0); - glRotatef(pose.rx, 1, 0, 0); + modelView.rotate(pose.rz, 0, 0, 1); + modelView.rotate(pose.ry, 0, 1, 0); + modelView.rotate(pose.rx, 1, 0, 0); break; case EulerRotationOrder::ZXY: // Yaw-Roll-Pitch - glRotatef(pose.ry, 0, 1, 0); - glRotatef(pose.rx, 1, 0, 0); - glRotatef(pose.rz, 0, 0, 1); + modelView.rotate(pose.ry, 0, 1, 0); + modelView.rotate(pose.rx, 1, 0, 0); + modelView.rotate(pose.rz, 0, 0, 1); break; case EulerRotationOrder::YXZ: // Pitch-Roll-Yaw - glRotatef(pose.rz, 0, 0, 1); - glRotatef(pose.rx, 1, 0, 0); - glRotatef(pose.ry, 0, 1, 0); + modelView.rotate(pose.rz, 0, 0, 1); + modelView.rotate(pose.rx, 1, 0, 0); + modelView.rotate(pose.ry, 0, 1, 0); break; case EulerRotationOrder::XZY: // Roll-Yaw-Pitch - glRotatef(pose.ry, 0, 1, 0); - glRotatef(pose.rz, 0, 0, 1); - glRotatef(pose.rx, 1, 0, 0); + modelView.rotate(pose.ry, 0, 1, 0); + modelView.rotate(pose.rz, 0, 0, 1); + modelView.rotate(pose.rx, 1, 0, 0); break; case EulerRotationOrder::YZX: // Pitch-Yaw-Roll - glRotatef(pose.rx, 1, 0, 0); - glRotatef(pose.rz, 0, 0, 1); - glRotatef(pose.ry, 0, 1, 0); + modelView.rotate(pose.rx, 1, 0, 0); + modelView.rotate(pose.rz, 0, 0, 1); + modelView.rotate(pose.ry, 0, 1, 0); break; } + const QMatrix4x4 mvp = m_projection * modelView; + // 绘制坐标系(右手坐标系:X红右,Y绿上,Z蓝前) - glBegin(GL_LINES); - - // X轴 - 红色 - glColor3f(1.0f, 0.0f, 0.0f); - glVertex3f(0.0f, 0.0f, 0.0f); - glVertex3f(pose.scale, 0.0f, 0.0f); - - // Y轴 - 绿色 - glColor3f(0.0f, 1.0f, 0.0f); - glVertex3f(0.0f, 0.0f, 0.0f); - glVertex3f(0.0f, pose.scale, 0.0f); - - // Z轴 - 蓝色 - glColor3f(0.0f, 0.0f, 1.0f); - glVertex3f(0.0f, 0.0f, 0.0f); - glVertex3f(0.0f, 0.0f, pose.scale); - - glEnd(); - - glPopMatrix(); + const float positions[] = { + 0.0f, 0.0f, 0.0f, pose.scale, 0.0f, 0.0f, // X轴 - 红色 + 0.0f, 0.0f, 0.0f, 0.0f, pose.scale, 0.0f, // Y轴 - 绿色 + 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, pose.scale // Z轴 - 蓝色 + }; + const float colors[] = { + 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, + 0.0f, 1.0f, 0.0f, 0.0f, 1.0f, 0.0f, + 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 1.0f + }; + drawPrimitives(mvp, positions, 6, GL_LINES, colors, + QVector3D(1.0f, 1.0f, 1.0f), 1.0f, 1.0f, true); } glLineWidth(1.0f); diff --git a/Utils.pro b/Utils.pro index 143b212..06060a0 100644 --- a/Utils.pro +++ b/Utils.pro @@ -11,18 +11,9 @@ CloudView.file = CloudView/CloudView.pro # 添加子项目 SUBDIRS += \ VrCommon \ - VrUtils - -!equals(TARGET_APP, "ParkingSpaceGuideView") { - SUBDIRS += \ - CloudUtils \ - DataUtils -} - -win32-msvc:!equals(TARGET_APP, "ParkingSpaceGuideView") { - SUBDIRS += \ - CloudView3D \ - CloudView + VrUtils \ + CloudView3D \ + CloudView } # 设置依赖关系 diff --git a/VrCommon/VrCommon.pro b/VrCommon/VrCommon.pro index f985437..360462b 100644 --- a/VrCommon/VrCommon.pro +++ b/VrCommon/VrCommon.pro @@ -1,10 +1,17 @@ #CONFIG -= qt -TEMPLATE = lib -CONFIG += staticlib -CONFIG += c++17 - -# You can make your code fail to compile if it uses deprecated APIs. +TEMPLATE = lib +CONFIG += staticlib +CONFIG += c++17 + +win32:CONFIG(debug, debug|release) { + DESTDIR = $$PWD/debug +} +win32:CONFIG(release, debug|release) { + DESTDIR = $$PWD/release +} + +# You can make your code fail to compile if it uses deprecated APIs. # In order to do so, uncomment the following line. #DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000 # disables all the APIs deprecated before Qt 6.0.0 diff --git a/VrUtils/VrUtils.pro b/VrUtils/VrUtils.pro index b77d3d5..3f1220b 100644 --- a/VrUtils/VrUtils.pro +++ b/VrUtils/VrUtils.pro @@ -6,9 +6,16 @@ DEFINES += TINYXML2_EXPORT DEFINES += HAVE_SNPRINTF DEFINES += LOG4CPP_HAVE_SSTREAM=1 DEFINES += VR_UTILS_EXPORTS -CONFIG += c++11 - -# Unix平台特定配置 +CONFIG += c++11 + +win32:CONFIG(debug, debug|release) { + DESTDIR = $$PWD/debug +} +win32:CONFIG(release, debug|release) { + DESTDIR = $$PWD/release +} + +# Unix平台特定配置 unix:!macx { DEFINES += LOG4CPP_HAVE_UNISTD_H # 确保所有符号导出(对于静态库)