#include "vde/foundation/io_gltf.h" #include "vde/core/aabb.h" #include "vde/curves/tessellation.h" #include #include #include #include #include #include namespace vde::foundation { // ── Helpers ────────────────────────────────────────────────────────── static std::string vec3_json(double x, double y, double z) { std::ostringstream ss; ss << std::fixed << "[" << x << "," << y << "," << z << "]"; return ss.str(); } static void compute_normals(const mesh::HalfedgeMesh& mesh, std::vector& normals) { normals.resize(mesh.num_vertices() * 3, 0.0f); // For each face, compute face normal and accumulate to vertices for (size_t fi = 0; fi < mesh.num_faces(); ++fi) { auto vis = mesh.face_vertices(static_cast(fi)); if (vis.size() < 3) continue; const auto& v0 = mesh.vertex(vis[0]); const auto& v1 = mesh.vertex(vis[1]); const auto& v2 = mesh.vertex(vis[2]); // Cross product for face normal float nx = static_cast((v1.y() - v0.y()) * (v2.z() - v0.z()) - (v1.z() - v0.z()) * (v2.y() - v0.y())); float ny = static_cast((v1.z() - v0.z()) * (v2.x() - v0.x()) - (v1.x() - v0.x()) * (v2.z() - v0.z())); float nz = static_cast((v1.x() - v0.x()) * (v2.y() - v0.y()) - (v1.y() - v0.y()) * (v2.x() - v0.x())); for (size_t j = 0; j < 3 && j < vis.size(); ++j) { normals[vis[j] * 3 + 0] += nx; normals[vis[j] * 3 + 1] += ny; normals[vis[j] * 3 + 2] += nz; } } // Normalize for (size_t i = 0; i < mesh.num_vertices(); ++i) { float len = std::sqrt(normals[i * 3] * normals[i * 3] + normals[i * 3 + 1] * normals[i * 3 + 1] + normals[i * 3 + 2] * normals[i * 3 + 2]); if (len > 1e-10f) { normals[i * 3] /= len; normals[i * 3 + 1] /= len; normals[i * 3 + 2] /= len; } } } static void write_glb_header(std::ostream& out, uint32_t total_length) { uint32_t magic = 0x46546C67; // "glTF" uint32_t version = 2; out.write(reinterpret_cast(&magic), 4); out.write(reinterpret_cast(&version), 4); out.write(reinterpret_cast(&total_length), 4); } static void write_glb_chunk(std::ostream& out, uint32_t chunk_type, const std::string& data, uint32_t unpadded_len) { out.write(reinterpret_cast(&unpadded_len), 4); out.write(reinterpret_cast(&chunk_type), 4); out.write(data.data(), static_cast(data.size())); } // ── GLB binary buffer layout ───────────────────────────────────────── struct GlbBufferLayout { size_t positions_offset = 0; size_t positions_byte_len = 0; size_t normals_offset = 0; size_t normals_byte_len = 0; size_t indices_offset = 0; size_t indices_byte_len = 0; size_t total_bin_len = 0; }; static GlbBufferLayout compute_layout(const mesh::HalfedgeMesh& mesh, const GltfOptions& options) { GlbBufferLayout layout; size_t nv = mesh.num_vertices(); size_t nf = mesh.num_faces(); size_t index_count = nf * 3; layout.positions_offset = 0; layout.positions_byte_len = nv * 12; // 3 floats * 4 bytes layout.normals_offset = layout.positions_byte_len; layout.normals_byte_len = options.include_normals ? nv * 12 : 0; layout.indices_offset = layout.normals_offset + layout.normals_byte_len; layout.indices_byte_len = index_count * 4; // uint32 per index layout.total_bin_len = layout.indices_offset + layout.indices_byte_len; return layout; } static std::string generate_json(const mesh::HalfedgeMesh& mesh, const GltfOptions& options, const GlbBufferLayout& layout, const std::string& bin_uri = "") { size_t nv = mesh.num_vertices(); size_t nf = mesh.num_faces(); size_t index_count = nf * 3; core::AABB3D bounds = mesh.bounds(); std::ostringstream ss; ss << std::fixed << std::setprecision(6); ss << "{\n"; ss << " \"asset\":{\"version\":\"2.0\",\"generator\":\"ViewDesignEngine\"},\n"; ss << " \"scene\":0,\n"; ss << " \"scenes\":[{\"nodes\":[0]}],\n"; ss << " \"nodes\":[{\"mesh\":0}],\n"; // Mesh primitive ss << " \"meshes\":[{\"primitives\":[{\"attributes\":{"; ss << "\"POSITION\":0"; if (options.include_normals) ss << ",\"NORMAL\":1"; ss << "},\"indices\":" << (options.include_normals ? 2 : 1) << "}]}],\n"; // Buffer ss << " \"buffers\":[{"; if (!bin_uri.empty()) ss << "\"uri\":\"" << bin_uri << "\","; ss << "\"byteLength\":" << layout.total_bin_len << "}],\n"; // Buffer views ss << " \"bufferViews\":[\n"; ss << " {\"buffer\":0,\"byteOffset\":" << layout.positions_offset << ",\"byteLength\":" << layout.positions_byte_len << ",\"target\":34962},\n"; if (options.include_normals) { ss << " {\"buffer\":0,\"byteOffset\":" << layout.normals_offset << ",\"byteLength\":" << layout.normals_byte_len << ",\"target\":34962},\n"; } ss << " {\"buffer\":0,\"byteOffset\":" << layout.indices_offset << ",\"byteLength\":" << layout.indices_byte_len << ",\"target\":34963}\n"; ss << " ],\n"; // Accessors ss << " \"accessors\":[\n"; ss << " {\"bufferView\":0,\"componentType\":5126,\"count\":" << nv << ",\"type\":\"VEC3\",\"max\":" << vec3_json(bounds.max().x(), bounds.max().y(), bounds.max().z()) << ",\"min\":" << vec3_json(bounds.min().x(), bounds.min().y(), bounds.min().z()) << "},\n"; int acc_index = 1; if (options.include_normals) { ss << " {\"bufferView\":1,\"componentType\":5126,\"count\":" << nv << ",\"type\":\"VEC3\"},\n"; acc_index = 2; } ss << " {\"bufferView\":" << (options.include_normals ? 2 : 1) << ",\"componentType\":5125,\"count\":" << index_count << ",\"type\":\"SCALAR\"}\n"; ss << " ]\n"; ss << "}"; return ss.str(); } // ── Write binary data ───────────────────────────────────────────────── static void write_binary_data(std::ostream& bin, const mesh::HalfedgeMesh& mesh, const GltfOptions& options, const GlbBufferLayout& layout) { size_t nv = mesh.num_vertices(); size_t nf = mesh.num_faces(); // Positions for (size_t i = 0; i < nv; ++i) { const auto& v = mesh.vertex(i); float fx = static_cast(v.x()); float fy = static_cast(v.y()); float fz = static_cast(v.z()); bin.write(reinterpret_cast(&fx), 4); bin.write(reinterpret_cast(&fy), 4); bin.write(reinterpret_cast(&fz), 4); } // Normals if (options.include_normals) { std::vector normals; compute_normals(mesh, normals); bin.write(reinterpret_cast(normals.data()), static_cast(normals.size() * sizeof(float))); } // Indices for (size_t i = 0; i < nf; ++i) { auto vis = mesh.face_vertices(static_cast(i)); for (size_t j = 0; j < std::min(vis.size(), size_t(3)); ++j) { uint32_t idx = static_cast(vis[j]); bin.write(reinterpret_cast(&idx), 4); } } } // ── Public API ──────────────────────────────────────────────────────── bool write_gltf(const std::string& filepath, const mesh::HalfedgeMesh& mesh, const GltfOptions& options) { if (mesh.num_vertices() == 0 || mesh.num_faces() == 0) return false; auto layout = compute_layout(mesh, options); if (options.binary) { std::string json = generate_json(mesh, options, layout); // ── GLB format ── uint32_t json_unpadded_len = static_cast(json.size()); // Pad JSON chunk to 4-byte alignment with spaces std::string json_padded = json; while (json_padded.size() % 4 != 0) json_padded += ' '; uint32_t json_padded_len = static_cast(json_padded.size()); // Serialize binary data std::string bin_data; uint32_t bin_unpadded_len = static_cast(layout.total_bin_len); if (layout.total_bin_len > 0) { std::ostringstream bin_stream(std::ios::binary); write_binary_data(bin_stream, mesh, options, layout); bin_data = bin_stream.str(); while (bin_data.size() % 4 != 0) bin_data.push_back('\0'); } // Total = header(12) + json_chunk_header(8) + json_padded + [bin_chunk_header(8) + bin_padded] uint32_t total = 12 + 8 + json_padded_len; if (!bin_data.empty()) total += 8 + static_cast(bin_data.size()); std::ofstream file(filepath, std::ios::binary); if (!file) return false; write_glb_header(file, total); write_glb_chunk(file, 0x4E4F534A, json_padded, json_unpadded_len); // "JSON" if (!bin_data.empty()) { write_glb_chunk(file, 0x004E4942, bin_data, bin_unpadded_len); // "BIN\0" } return true; } else { // ── glTF JSON + separate .bin ── // Determine bin filename from output path std::string dot = "."; size_t dot_pos = filepath.rfind(dot); std::string binpath; std::string bin_basename; if (dot_pos != std::string::npos) { binpath = filepath.substr(0, dot_pos) + ".bin"; // Extract just the filename size_t slash_pos = binpath.rfind('/'); bin_basename = (slash_pos != std::string::npos) ? binpath.substr(slash_pos + 1) : binpath; } else { binpath = filepath + ".bin"; bin_basename = binpath; } std::string json = generate_json(mesh, options, layout, bin_basename); std::ofstream file(filepath); if (!file) return false; file << json; std::ofstream bin(binpath, std::ios::binary); if (!bin) return false; write_binary_data(bin, mesh, options, layout); return true; } } bool write_brep_gltf(const std::string& filepath, const brep::BrepModel& model, int tessellation_res) { if (model.num_faces() == 0) return false; // Collect all vertices and triangles from tessellated faces std::vector all_verts; std::vector> all_tris; for (size_t fi = 0; fi < model.num_faces(); ++fi) { const auto& face = model.face(static_cast(fi)); if (face.loops.empty()) continue; // Get the surface for this face int surf_id = face.surface_id; if (surf_id < 0 || surf_id >= static_cast(model.num_surfaces())) continue; const auto& surf = model.surface(surf_id); // Inline tessellation: sample surface uniformly, create quad mesh int res = std::max(4, tessellation_res); std::vector verts; std::vector> tris; auto [u_min, u_max] = surf.domain_u(); auto [v_min, v_max] = surf.domain_v(); for (int j = 0; j <= res; ++j) { double v = v_min + (v_max - v_min) * static_cast(j) / res; for (int i = 0; i <= res; ++i) { double u = u_min + (u_max - u_min) * static_cast(i) / res; verts.push_back(surf.evaluate(u, v)); } } for (int j = 0; j < res; ++j) { for (int i = 0; i < res; ++i) { int a = j * (res + 1) + i; int b = a + 1; int c = a + (res + 1); int d = c + 1; tris.push_back({a, b, d}); tris.push_back({a, d, c}); } } // Offset indices int base = static_cast(all_verts.size()); for (const auto& v : verts) all_verts.push_back(v); for (const auto& t : tris) { all_tris.push_back({t[0] + base, t[1] + base, t[2] + base}); } } if (all_verts.empty() || all_tris.empty()) return false; // Build HalfedgeMesh from tessellated data mesh::HalfedgeMesh mesh; mesh.build_from_triangles(all_verts, all_tris); // Export as GLB GltfOptions opt; opt.binary = true; opt.include_normals = true; return write_gltf(filepath, mesh, opt); } } // namespace vde::foundation