#include "mesh_service.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace sdl3cpp::services::impl { namespace { constexpr unsigned int kAssimpLoadFlags = aiProcess_Triangulate | aiProcess_JoinIdenticalVertices | aiProcess_PreTransformVertices | aiProcess_GenNormals; struct ZipArchiveDeleter { void operator()(zip_t* archive) const { if (archive) { zip_close(archive); } } }; struct ZipFileDeleter { void operator()(zip_file_t* file) const { if (file) { zip_fclose(file); } } }; std::string BuildZipErrorMessage(int errorCode) { zip_error_t zipError; zip_error_init_with_code(&zipError, errorCode); std::string message = zip_error_strerror(&zipError); zip_error_fini(&zipError); return message; } std::string BuildZipArchiveErrorMessage(zip_t* archive) { if (!archive) { return "unknown zip archive error"; } zip_error_t* zipError = zip_get_error(archive); if (!zipError) { return "unknown zip archive error"; } return zip_error_strerror(zipError); } std::string GetExtensionHint(const std::string& entryPath, const std::string& fallback) { std::filesystem::path entry(entryPath); std::string ext = entry.extension().string(); if (!ext.empty() && ext.front() == '.') { ext.erase(ext.begin()); } if (!ext.empty()) { return ext; } return fallback; } std::string NormalizeExtension(std::string extension) { std::transform(extension.begin(), extension.end(), extension.begin(), [](unsigned char value) { return static_cast(std::tolower(value)); }); return extension; } class ArchiveMapAwareIOSystem : public Assimp::DefaultIOSystem { public: bool Exists(const char* pFile) const override { const std::string trimmed = TrimArchiveSuffix(pFile); return Assimp::DefaultIOSystem::Exists(trimmed.c_str()); } Assimp::IOStream* Open(const char* pFile, const char* pMode = "rb") override { const std::string trimmed = TrimArchiveSuffix(pFile); return Assimp::DefaultIOSystem::Open(trimmed.c_str(), pMode); } private: static std::string TrimArchiveSuffix(const char* path) { if (!path) { return std::string(); } std::string trimmed(path); const std::string::size_type comma = trimmed.find(','); if (comma != std::string::npos) { trimmed.erase(comma); } return trimmed; } }; bool ReadArchiveEntry(zip_t* archive, const std::string& entryPath, const zip_stat_t& entryStat, std::vector& buffer, std::string& outError) { if (!archive) { outError = "Archive handle is null"; return false; } if (entryStat.size == 0) { outError = "Archive entry is empty: " + entryPath; return false; } if (entryStat.size > std::numeric_limits::max()) { outError = "Archive entry exceeds addressable size: " + entryPath; return false; } std::unique_ptr file( zip_fopen(archive, entryPath.c_str(), ZIP_FL_ENC_GUESS)); if (!file) { outError = "Failed to open archive entry: " + BuildZipArchiveErrorMessage(archive); return false; } size_t entrySize = static_cast(entryStat.size); buffer.assign(entrySize, 0); zip_int64_t totalRead = 0; while (static_cast(totalRead) < entrySize) { zip_int64_t bytesRead = zip_fread(file.get(), buffer.data() + totalRead, entrySize - static_cast(totalRead)); if (bytesRead < 0) { outError = "Failed to read archive entry: " + BuildZipArchiveErrorMessage(archive); return false; } if (bytesRead == 0) { break; } totalRead += bytesRead; } if (static_cast(totalRead) != entrySize) { outError = "Archive entry read incomplete: " + entryPath; return false; } return true; } #pragma pack(push, 1) struct BspHeader { char id[4]; int32_t version; }; struct BspLump { int32_t offset; int32_t length; }; struct BspVertex { float position[3]; float texCoord[2]; float lightmapCoord[2]; float normal[3]; uint8_t color[4]; }; struct BspFace { int32_t textureId; int32_t effect; int32_t type; int32_t vertexIndex; int32_t numVertices; int32_t meshVertIndex; int32_t numMeshVerts; int32_t lightmapId; int32_t lightmapCorner[2]; int32_t lightmapSize[2]; float lightmapPos[3]; float lightmapVecs[2][3]; float normal[3]; int32_t patchSize[2]; }; #pragma pack(pop) static_assert(sizeof(BspHeader) == 8, "Unexpected BSP header size"); static_assert(sizeof(BspLump) == 8, "Unexpected BSP lump size"); static_assert(sizeof(BspVertex) == 44, "Unexpected BSP vertex size"); static_assert(sizeof(BspFace) == 104, "Unexpected BSP face size"); bool BuildPayloadFromBspBuffer(const std::vector& buffer, MeshPayload& outPayload, std::string& outError, const std::shared_ptr& logger) { constexpr int32_t kBspExpectedVersion = 46; constexpr size_t kBspLumpCount = 17; constexpr size_t kBspVerticesLump = 10; constexpr size_t kBspMeshVertsLump = 11; constexpr size_t kBspFacesLump = 13; constexpr int32_t kBspFacePolygon = 1; constexpr int32_t kBspFaceMesh = 3; if (buffer.size() < sizeof(BspHeader) + kBspLumpCount * sizeof(BspLump)) { outError = "BSP buffer is too small to contain header and lumps"; return false; } BspHeader header{}; std::memcpy(&header, buffer.data(), sizeof(BspHeader)); if (std::string(header.id, sizeof(header.id)) != "IBSP") { outError = "BSP header mismatch: expected IBSP"; return false; } if (header.version != kBspExpectedVersion) { outError = "Unsupported BSP version: " + std::to_string(header.version); return false; } std::array lumps{}; std::memcpy(lumps.data(), buffer.data() + sizeof(BspHeader), kBspLumpCount * sizeof(BspLump)); auto validateLump = [&](size_t index, size_t elementSize, size_t& outCount, const std::string& label) -> bool { const BspLump& lump = lumps[index]; if (lump.offset < 0 || lump.length < 0) { outError = "BSP " + label + " lump has negative bounds"; return false; } size_t offset = static_cast(lump.offset); size_t length = static_cast(lump.length); if (offset + length > buffer.size()) { outError = "BSP " + label + " lump exceeds buffer size"; return false; } if (length % elementSize != 0) { outError = "BSP " + label + " lump size is not aligned"; return false; } outCount = length / elementSize; return true; }; size_t vertexCount = 0; size_t meshVertCount = 0; size_t faceCount = 0; if (!validateLump(kBspVerticesLump, sizeof(BspVertex), vertexCount, "vertex")) { return false; } if (!validateLump(kBspMeshVertsLump, sizeof(int32_t), meshVertCount, "meshvert")) { return false; } if (!validateLump(kBspFacesLump, sizeof(BspFace), faceCount, "face")) { return false; } if (vertexCount == 0) { outError = "BSP contains no vertices"; return false; } if (meshVertCount == 0 || faceCount == 0) { outError = "BSP contains no face indices"; return false; } const uint8_t* vertexData = buffer.data() + lumps[kBspVerticesLump].offset; const uint8_t* meshVertData = buffer.data() + lumps[kBspMeshVertsLump].offset; const uint8_t* faceData = buffer.data() + lumps[kBspFacesLump].offset; std::vector vertices(vertexCount); std::memcpy(vertices.data(), vertexData, vertexCount * sizeof(BspVertex)); std::vector meshVerts(meshVertCount); std::memcpy(meshVerts.data(), meshVertData, meshVertCount * sizeof(int32_t)); outPayload.positions.resize(vertexCount); outPayload.normals.resize(vertexCount); outPayload.colors.resize(vertexCount); outPayload.texcoords.resize(vertexCount); outPayload.indices.clear(); for (size_t i = 0; i < vertexCount; ++i) { const BspVertex& vertex = vertices[i]; outPayload.positions[i] = {vertex.position[0], vertex.position[1], vertex.position[2]}; outPayload.normals[i] = {vertex.normal[0], vertex.normal[1], vertex.normal[2]}; outPayload.colors[i] = { static_cast(vertex.color[0]) / 255.0f, static_cast(vertex.color[1]) / 255.0f, static_cast(vertex.color[2]) / 255.0f }; outPayload.texcoords[i] = {vertex.texCoord[0], vertex.texCoord[1]}; } size_t trianglesBuilt = 0; size_t trianglesSkipped = 0; for (size_t faceIndex = 0; faceIndex < faceCount; ++faceIndex) { BspFace face{}; std::memcpy(&face, faceData + faceIndex * sizeof(BspFace), sizeof(BspFace)); if (face.type != kBspFacePolygon && face.type != kBspFaceMesh) { continue; } if (face.numMeshVerts < 3) { continue; } for (int32_t i = 0; i + 2 < face.numMeshVerts; i += 3) { int32_t meshIndex = face.meshVertIndex + i; if (meshIndex < 0 || static_cast(meshIndex + 2) >= meshVerts.size()) { ++trianglesSkipped; continue; } int32_t index0 = face.vertexIndex + meshVerts[static_cast(meshIndex)]; int32_t index1 = face.vertexIndex + meshVerts[static_cast(meshIndex + 1)]; int32_t index2 = face.vertexIndex + meshVerts[static_cast(meshIndex + 2)]; if (index0 < 0 || index1 < 0 || index2 < 0) { ++trianglesSkipped; continue; } if (static_cast(index0) >= vertexCount || static_cast(index1) >= vertexCount || static_cast(index2) >= vertexCount) { ++trianglesSkipped; continue; } outPayload.indices.push_back(static_cast(index0)); outPayload.indices.push_back(static_cast(index1)); outPayload.indices.push_back(static_cast(index2)); ++trianglesBuilt; } } if (logger) { logger->Trace("MeshService", "BuildPayloadFromBspBuffer", "vertexCount=" + std::to_string(vertexCount) + ", meshVertCount=" + std::to_string(meshVertCount) + ", faceCount=" + std::to_string(faceCount) + ", trianglesBuilt=" + std::to_string(trianglesBuilt) + ", texcoordCount=" + std::to_string(outPayload.texcoords.size()) + ", trianglesSkipped=" + std::to_string(trianglesSkipped)); } if (outPayload.indices.empty()) { outError = "BSP contains no triangle faces"; return false; } if (outPayload.positions.size() > std::numeric_limits::max()) { outError = "Mesh vertex count exceeds uint16_t index range: " + std::to_string(outPayload.positions.size()); return false; } return true; } bool WriteArchiveEntryToTempFile(const std::vector& buffer, const std::string& entryPath, const std::string& extensionHint, std::filesystem::path& outPath, std::string& outError) { std::error_code tempError; std::filesystem::path tempDirectory = std::filesystem::temp_directory_path(tempError); if (tempError) { outError = "Failed to resolve temp directory: " + tempError.message(); return false; } std::filesystem::path entryName(entryPath); std::string baseName = entryName.stem().string(); if (baseName.empty()) { baseName = "archive_entry"; } auto timestampMs = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()).count(); std::string fileName = "sdl3cpp_" + baseName + "_" + std::to_string(timestampMs); if (!extensionHint.empty()) { fileName += "." + extensionHint; } outPath = tempDirectory / fileName; std::ofstream output(outPath, std::ios::binary | std::ios::trunc); if (!output) { outError = "Failed to create temp file: " + outPath.string(); return false; } output.write(reinterpret_cast(buffer.data()), static_cast(buffer.size())); if (!output) { outError = "Failed to write temp file: " + outPath.string(); return false; } return true; } aiColor3D ResolveMaterialColor(const aiScene* scene, const aiMesh* mesh) { aiColor3D defaultColor(0.6f, 0.8f, 1.0f); if (!scene || !mesh) { return defaultColor; } if (mesh->mMaterialIndex < scene->mNumMaterials) { const aiMaterial* material = scene->mMaterials[mesh->mMaterialIndex]; aiColor4D diffuse; if (material && material->Get(AI_MATKEY_COLOR_DIFFUSE, diffuse) == AI_SUCCESS) { return aiColor3D(diffuse.r, diffuse.g, diffuse.b); } } return defaultColor; } struct MeshBounds { aiVector3D min; aiVector3D max; }; MeshBounds ComputeMeshBounds(const aiMesh* mesh) { MeshBounds bounds{}; bounds.min = aiVector3D(std::numeric_limits::max()); bounds.max = aiVector3D(std::numeric_limits::lowest()); if (!mesh || !mesh->mNumVertices) { return bounds; } for (unsigned i = 0; i < mesh->mNumVertices; ++i) { const aiVector3D& v = mesh->mVertices[i]; bounds.min.x = std::min(bounds.min.x, v.x); bounds.min.y = std::min(bounds.min.y, v.y); bounds.min.z = std::min(bounds.min.z, v.z); bounds.max.x = std::max(bounds.max.x, v.x); bounds.max.y = std::max(bounds.max.y, v.y); bounds.max.z = std::max(bounds.max.z, v.z); } return bounds; } float NormalizeCoord(float value, float minValue, float maxValue) { float range = maxValue - minValue; if (range == 0.0f) { return 0.0f; } return (value - minValue) / range; } std::array ComputeFallbackTexcoord(const aiVector3D& position, const aiVector3D& normal, const MeshBounds& bounds) { float ax = std::fabs(normal.x); float ay = std::fabs(normal.y); float az = std::fabs(normal.z); if (ax >= ay && ax >= az) { return {NormalizeCoord(position.z, bounds.min.z, bounds.max.z), NormalizeCoord(position.y, bounds.min.y, bounds.max.y)}; } if (ay >= ax && ay >= az) { return {NormalizeCoord(position.x, bounds.min.x, bounds.max.x), NormalizeCoord(position.z, bounds.min.z, bounds.max.z)}; } return {NormalizeCoord(position.x, bounds.min.x, bounds.max.x), NormalizeCoord(position.y, bounds.min.y, bounds.max.y)}; } bool AppendMeshPayload(const aiScene* scene, const aiMesh* mesh, MeshPayload& outPayload, std::string& outError, size_t& outIndicesAdded) { outIndicesAdded = 0; if (!mesh || !mesh->mNumVertices) { outError = "Mesh contains no vertices"; return false; } size_t vertexOffset = outPayload.positions.size(); if (vertexOffset > std::numeric_limits::max()) { outError = "Mesh vertex count exceeds uint32_t index range"; return false; } aiColor3D materialColor = ResolveMaterialColor(scene, mesh); const MeshBounds bounds = ComputeMeshBounds(mesh); const bool hasTexcoords = mesh->HasTextureCoords(0) && mesh->mTextureCoords[0]; size_t positionsStart = outPayload.positions.size(); size_t normalsStart = outPayload.normals.size(); size_t colorsStart = outPayload.colors.size(); size_t texcoordsStart = outPayload.texcoords.size(); size_t indicesStart = outPayload.indices.size(); outPayload.positions.reserve(positionsStart + mesh->mNumVertices); outPayload.normals.reserve(normalsStart + mesh->mNumVertices); outPayload.colors.reserve(colorsStart + mesh->mNumVertices); outPayload.texcoords.reserve(texcoordsStart + mesh->mNumVertices); outPayload.indices.reserve(indicesStart + mesh->mNumFaces * 3); for (unsigned i = 0; i < mesh->mNumVertices; ++i) { const aiVector3D& vertex = mesh->mVertices[i]; outPayload.positions.push_back({vertex.x, vertex.y, vertex.z}); aiVector3D normal(0.0f, 0.0f, 1.0f); if (mesh->HasNormals()) { normal = mesh->mNormals[i]; } outPayload.normals.push_back({normal.x, normal.y, normal.z}); aiColor3D color = materialColor; if (mesh->HasVertexColors(0) && mesh->mColors[0]) { const aiColor4D& vertexColor = mesh->mColors[0][i]; color = aiColor3D(vertexColor.r, vertexColor.g, vertexColor.b); } outPayload.colors.push_back({color.r, color.g, color.b}); if (hasTexcoords) { const aiVector3D& uv = mesh->mTextureCoords[0][i]; outPayload.texcoords.push_back({uv.x, uv.y}); } else { outPayload.texcoords.push_back(ComputeFallbackTexcoord(vertex, normal, bounds)); } } for (unsigned faceIndex = 0; faceIndex < mesh->mNumFaces; ++faceIndex) { const aiFace& face = mesh->mFaces[faceIndex]; if (face.mNumIndices != 3) { continue; } outPayload.indices.push_back(static_cast(face.mIndices[0]) + static_cast(vertexOffset)); outPayload.indices.push_back(static_cast(face.mIndices[1]) + static_cast(vertexOffset)); outPayload.indices.push_back(static_cast(face.mIndices[2]) + static_cast(vertexOffset)); } outIndicesAdded = outPayload.indices.size() - indicesStart; if (outIndicesAdded == 0) { outPayload.positions.resize(positionsStart); outPayload.normals.resize(normalsStart); outPayload.colors.resize(colorsStart); outPayload.indices.resize(indicesStart); outError = "Mesh contains no triangle faces"; return false; } return true; } bool BuildPayloadFromScene(const aiScene* scene, bool combineMeshes, MeshPayload& outPayload, std::string& outError, const std::shared_ptr& logger) { if (!scene) { outError = "Assimp scene is null"; return false; } if (scene->mNumMeshes == 0) { outError = "Scene contains no meshes"; return false; } outPayload.positions.clear(); outPayload.normals.clear(); outPayload.colors.clear(); outPayload.indices.clear(); if (!combineMeshes) { size_t indicesAdded = 0; if (!AppendMeshPayload(scene, scene->mMeshes[0], outPayload, outError, indicesAdded)) { return false; } return true; } size_t totalIndicesAdded = 0; for (unsigned meshIndex = 0; meshIndex < scene->mNumMeshes; ++meshIndex) { const aiMesh* mesh = scene->mMeshes[meshIndex]; std::string meshError; size_t indicesAdded = 0; if (!AppendMeshPayload(scene, mesh, outPayload, meshError, indicesAdded)) { if (logger) { logger->Trace("MeshService", "BuildPayloadFromScene", "Skipping mesh " + std::to_string(meshIndex) + ": " + meshError); } continue; } totalIndicesAdded += indicesAdded; } if (totalIndicesAdded == 0) { outError = "Scene contains no triangle faces"; return false; } return true; } } // namespace MeshService::MeshService(std::shared_ptr configService, std::shared_ptr logger) : configService_(std::move(configService)), logger_(std::move(logger)) { if (logger_) { logger_->Trace("MeshService", "MeshService", "configService=" + std::string(configService_ ? "set" : "null")); } } bool MeshService::LoadFromFile(const std::string& requestedPath, MeshPayload& outPayload, std::string& outError) { if (logger_) { logger_->Trace("MeshService", "LoadFromFile", "requestedPath=" + requestedPath); } std::filesystem::path resolved; if (!ResolvePath(requestedPath, resolved, outError)) { return false; } if (!std::filesystem::exists(resolved)) { outError = "Mesh file not found: " + resolved.string(); return false; } Assimp::Importer importer; const aiScene* scene = importer.ReadFile(resolved.string(), kAssimpLoadFlags); if (!scene) { outError = importer.GetErrorString() ? importer.GetErrorString() : "Assimp failed to load mesh"; return false; } return BuildPayloadFromScene(scene, false, outPayload, outError, logger_); } bool MeshService::LoadFromArchive(const std::string& archivePath, const std::string& entryPath, MeshPayload& outPayload, std::string& outError) { if (logger_) { logger_->Trace("MeshService", "LoadFromArchive", "archivePath=" + archivePath + ", entryPath=" + entryPath); } std::filesystem::path resolvedArchive; if (!ResolvePath(archivePath, resolvedArchive, outError)) { return false; } if (!std::filesystem::exists(resolvedArchive)) { outError = "Archive file not found: " + resolvedArchive.string(); return false; } if (entryPath.empty()) { outError = "Archive entry path is empty"; return false; } std::string extensionHint = GetExtensionHint(entryPath, "bsp"); std::string normalizedExtension = NormalizeExtension(extensionHint); auto buildPayload = [&](const aiScene* loadedScene, std::string& error) -> bool { if (!BuildPayloadFromScene(loadedScene, true, outPayload, error, logger_)) { return false; } if (outPayload.positions.size() > std::numeric_limits::max()) { error = "Mesh vertex count exceeds uint16_t index range: " + std::to_string(outPayload.positions.size()); return false; } return true; }; int errorCode = 0; std::unique_ptr archive( zip_open(resolvedArchive.string().c_str(), ZIP_RDONLY, &errorCode)); if (!archive) { outError = "Failed to open archive: " + BuildZipErrorMessage(errorCode); return false; } zip_stat_t entryStat; if (zip_stat(archive.get(), entryPath.c_str(), ZIP_FL_ENC_GUESS, &entryStat) != 0) { outError = "Archive entry not found: " + entryPath; return false; } if (normalizedExtension == "bsp") { if (entryStat.size == 0) { outError = "Archive entry is empty: " + entryPath; return false; } if (logger_) { logger_->Trace("MeshService", "LoadFromArchive", "Detected BSP entry; using archive importer. archive=" + resolvedArchive.string() + ", entry=" + entryPath); logger_->Trace("MeshService", "LoadFromArchive", "BSP entry size=" + std::to_string(entryStat.size)); } std::string importName = resolvedArchive.string() + "," + entryPath; Assimp::Importer importer; importer.SetIOHandler(new ArchiveMapAwareIOSystem()); const aiScene* scene = importer.ReadFile(importName, kAssimpLoadFlags); if (scene && logger_) { unsigned int rootMeshes = 0; unsigned int rootChildren = 0; if (scene->mRootNode) { rootMeshes = scene->mRootNode->mNumMeshes; rootChildren = scene->mRootNode->mNumChildren; } logger_->Trace("MeshService", "LoadFromArchive", "BSP scene stats: meshes=" + std::to_string(scene->mNumMeshes) + ", materials=" + std::to_string(scene->mNumMaterials) + ", textures=" + std::to_string(scene->mNumTextures) + ", rootMeshes=" + std::to_string(rootMeshes) + ", rootChildren=" + std::to_string(rootChildren)); } std::string assimpError; if (!scene) { assimpError = importer.GetErrorString() ? importer.GetErrorString() : "Assimp failed to load BSP from archive"; } else if (buildPayload(scene, assimpError)) { return true; } if (logger_) { logger_->Trace("MeshService", "LoadFromArchive", "Assimp BSP import did not yield meshes, falling back to BSP parser: " + assimpError); } std::vector buffer; std::string readError; if (!ReadArchiveEntry(archive.get(), entryPath, entryStat, buffer, readError)) { outError = "Failed to read BSP entry for fallback parser: " + readError; return false; } if (!BuildPayloadFromBspBuffer(buffer, outPayload, outError, logger_)) { return false; } return true; } std::vector buffer; if (!ReadArchiveEntry(archive.get(), entryPath, entryStat, buffer, outError)) { return false; } Assimp::Importer importer; const aiScene* scene = importer.ReadFileFromMemory( buffer.data(), buffer.size(), kAssimpLoadFlags, extensionHint.c_str()); if (!scene) { std::string memoryError = importer.GetErrorString() ? importer.GetErrorString() : "Assimp failed to load archive entry"; if (logger_) { logger_->Trace("MeshService", "LoadFromArchive", "ReadFileFromMemory failed: " + memoryError + ", entryPath=" + entryPath + ", extensionHint=" + extensionHint); } std::filesystem::path tempPath; std::string tempError; if (!WriteArchiveEntryToTempFile(buffer, entryPath, extensionHint, tempPath, tempError)) { outError = "Assimp failed to load archive entry from memory: " + memoryError + "; " + tempError; return false; } if (logger_) { logger_->Trace("MeshService", "LoadFromArchive", "Falling back to temp file: " + tempPath.string()); } Assimp::Importer fallbackImporter; const aiScene* fallbackScene = fallbackImporter.ReadFile(tempPath.string(), kAssimpLoadFlags); std::error_code removeError; std::filesystem::remove(tempPath, removeError); if (removeError && logger_) { logger_->Trace("MeshService", "LoadFromArchive", "Failed to remove temp file: " + tempPath.string() + ", error=" + removeError.message()); } if (!fallbackScene) { std::string fallbackError = fallbackImporter.GetErrorString() ? fallbackImporter.GetErrorString() : "Assimp failed to load temp file"; outError = "Assimp failed to load archive entry from memory: " + memoryError + "; fallback to temp file failed: " + fallbackError; return false; } if (!buildPayload(fallbackScene, outError)) { return false; } return true; } if (!buildPayload(scene, outError)) { return false; } return true; } bool MeshService::ResolvePath(const std::string& requestedPath, std::filesystem::path& resolvedPath, std::string& outError) const { if (!configService_) { outError = "Config service not available"; return false; } std::filesystem::path resolved(requestedPath); if (!resolved.is_absolute()) { resolved = configService_->GetScriptPath().parent_path() / resolved; } std::error_code ec; resolved = std::filesystem::weakly_canonical(resolved, ec); if (ec) { outError = "Failed to resolve path: " + ec.message(); return false; } resolvedPath = std::move(resolved); return true; } void MeshService::PushMeshToLua(lua_State* L, const MeshPayload& payload) { if (logger_) { logger_->Trace("MeshService", "PushMeshToLua", "positions.size=" + std::to_string(payload.positions.size()) + ", normals.size=" + std::to_string(payload.normals.size()) + ", colors.size=" + std::to_string(payload.colors.size()) + ", texcoords.size=" + std::to_string(payload.texcoords.size()) + ", indices.size=" + std::to_string(payload.indices.size()) + ", luaStateIsNull=" + std::string(L ? "false" : "true")); } lua_newtable(L); lua_newtable(L); for (size_t vertexIndex = 0; vertexIndex < payload.positions.size(); ++vertexIndex) { lua_newtable(L); lua_newtable(L); for (int component = 0; component < 3; ++component) { lua_pushnumber(L, payload.positions[vertexIndex][component]); lua_rawseti(L, -2, component + 1); } lua_setfield(L, -2, "position"); lua_newtable(L); std::array normal = {0.0f, 0.0f, 1.0f}; if (vertexIndex < payload.normals.size()) { normal = payload.normals[vertexIndex]; } for (int component = 0; component < 3; ++component) { lua_pushnumber(L, normal[component]); lua_rawseti(L, -2, component + 1); } lua_setfield(L, -2, "normal"); lua_newtable(L); for (int component = 0; component < 3; ++component) { lua_pushnumber(L, payload.colors[vertexIndex][component]); lua_rawseti(L, -2, component + 1); } lua_setfield(L, -2, "color"); lua_newtable(L); std::array texcoord = {0.0f, 0.0f}; if (vertexIndex < payload.texcoords.size()) { texcoord = payload.texcoords[vertexIndex]; } for (int component = 0; component < 2; ++component) { lua_pushnumber(L, texcoord[component]); lua_rawseti(L, -2, component + 1); } lua_setfield(L, -2, "texcoord"); lua_rawseti(L, -2, static_cast(vertexIndex + 1)); } lua_setfield(L, -2, "vertices"); lua_newtable(L); for (size_t index = 0; index < payload.indices.size(); ++index) { lua_pushinteger(L, static_cast(payload.indices[index]) + 1); lua_rawseti(L, -2, static_cast(index + 1)); } lua_setfield(L, -2, "indices"); } } // namespace sdl3cpp::services::impl