mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-09 17:31:45 +00:00
ifcviewer: rename sidecar transfer/record types; drop unused element hierarchy (sidecar v17)
Rename the streamer/sidecar transfer and record types to describe what they are rather than how they move: MeshChunk -> StreamedMesh InstanceChunk -> StreamedInstance InstanceCpu -> InstanceInfo PackedElementInfo -> ElementTableRecord uploadMeshChunk -> uploadStreamedMesh uploadInstanceChunk -> uploadStreamedInstance buildMeshChunk -> buildStreamedMesh and the two post-index sidecar metadata blocks: "critical" metadata -> "geometry" metadata (meshes/instances/georef/TOC) "deferred" metadata -> "element" metadata (elements + string table) parseSidecarCritical -> parseSidecarGeometryMetadata parseSidecarDeferred -> parseSidecarElementMetadata The one behavioural change: the element hierarchy (parent_id) was carried through ElementInfo, ElementTableRecord, and the sidecar element table but never consumed, so drop it and bump SIDECAR_VERSION 16 -> 17. No back-compat: regenerate sidecars. sample.ifcview is regenerated at v17. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -89,7 +89,7 @@ std::optional<express::Base> ElementRegistry::findEntity(uint32_t object_id) con
|
||||
}
|
||||
|
||||
void ElementRegistry::onSidecarElementsReady(uint32_t /*model_id*/,
|
||||
std::vector<PackedElementInfo> elements,
|
||||
std::vector<ElementTableRecord> elements,
|
||||
std::string string_table) {
|
||||
auto string_from_table = [&](uint32_t offset, uint32_t length) -> QString {
|
||||
if (length == 0 || offset + length > string_table.size()) return {};
|
||||
@@ -101,7 +101,6 @@ void ElementRegistry::onSidecarElementsReady(uint32_t /*model_id*/,
|
||||
info.object_id = packed_element.object_id;
|
||||
info.model_id = packed_element.model_id;
|
||||
info.ifc_id = packed_element.ifc_id;
|
||||
info.parent_id = packed_element.parent_id;
|
||||
info.guid = string_from_table(packed_element.guid_offset, packed_element.guid_length);
|
||||
info.name = string_from_table(packed_element.name_offset, packed_element.name_length);
|
||||
info.type = string_from_table(packed_element.type_offset, packed_element.type_length);
|
||||
@@ -115,7 +114,6 @@ void ElementRegistry::onStreamedElementsReady(uint32_t /*model_id*/, std::vector
|
||||
info.object_id = element.object_id;
|
||||
info.model_id = element.model_id;
|
||||
info.ifc_id = element.ifc_id;
|
||||
info.parent_id = element.parent_id;
|
||||
info.guid = QString::fromStdString(element.guid);
|
||||
info.name = QString::fromStdString(element.name);
|
||||
info.type = QString::fromStdString(element.type);
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
#include <vector>
|
||||
|
||||
class SceneLoader;
|
||||
struct PackedElementInfo;
|
||||
struct ElementTableRecord;
|
||||
struct ElementInfo;
|
||||
|
||||
namespace bonsaiviewer {
|
||||
@@ -39,7 +39,6 @@ struct BasicElementInfo {
|
||||
uint32_t object_id = 0;
|
||||
uint32_t model_id = 0;
|
||||
int ifc_id = 0;
|
||||
int parent_id = 0;
|
||||
QString guid;
|
||||
QString name;
|
||||
QString type;
|
||||
@@ -59,7 +58,7 @@ public:
|
||||
|
||||
private:
|
||||
void onSidecarElementsReady(uint32_t model_id,
|
||||
std::vector<PackedElementInfo> elements,
|
||||
std::vector<ElementTableRecord> elements,
|
||||
std::string string_table);
|
||||
void onStreamedElementsReady(uint32_t model_id, std::vector<ElementInfo> elements);
|
||||
|
||||
|
||||
Binary file not shown.
@@ -131,7 +131,7 @@ ModelGeoref computeModelGeoref(ifcopenshell::file* ifc_file);
|
||||
// Build a FederatedFalseOrigin guess so that a model lands near the
|
||||
// federation origin instead of out at its surveyor coordinates. Designed
|
||||
// to work without an open IFC file so it's usable from sidecar-only loads
|
||||
// (the inputs are all derivable from the resident MeshInfo + InstanceCpu
|
||||
// (the inputs are all derivable from the resident MeshInfo + InstanceInfo
|
||||
// data + ModelGeoref).
|
||||
//
|
||||
// Position: `first_geometry_point_m` is a point that actually lies on the
|
||||
|
||||
@@ -144,7 +144,7 @@ std::vector<ElementInfo> GeometryStreamer::drainElements() {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Build a mesh chunk (local coords, 28-byte interleaved vertices) from a
|
||||
// Build a streamed mesh record (local coords, 28-byte interleaved vertices) from a
|
||||
// TriangulationElement. Per-vertex color is baked from material_ids so that
|
||||
// triangulations with per-face materials still render correctly.
|
||||
// Vertex rebasing: when `offset` is non-zero, every vertex position is
|
||||
@@ -153,13 +153,13 @@ std::vector<ElementInfo> GeometryStreamer::drainElements() {
|
||||
// compensates by post-multiplying each instance's PlacementTransformation
|
||||
// by T(+offset), which is mathematically the identity overall but moves
|
||||
// the magnitude off the float-precision-sensitive vertex column.
|
||||
static MeshChunk buildMeshChunk(uint32_t model_id,
|
||||
static StreamedMesh buildStreamedMesh(uint32_t model_id,
|
||||
uint32_t local_mesh_id,
|
||||
const IfcGeom::TriangulationElement* elem,
|
||||
const Eigen::Vector3d& offset) {
|
||||
MeshChunk chunk;
|
||||
chunk.model_id = model_id;
|
||||
chunk.local_mesh_id = local_mesh_id;
|
||||
StreamedMesh mesh;
|
||||
mesh.model_id = model_id;
|
||||
mesh.local_mesh_id = local_mesh_id;
|
||||
|
||||
const auto& geom = elem->geometry();
|
||||
const auto& verts = geom.verts();
|
||||
@@ -168,7 +168,7 @@ static MeshChunk buildMeshChunk(uint32_t model_id,
|
||||
const auto& materials = geom.materials();
|
||||
const auto& material_ids = geom.material_ids();
|
||||
|
||||
if (verts.empty() || faces.empty()) return chunk;
|
||||
if (verts.empty() || faces.empty()) return mesh;
|
||||
|
||||
const size_t num_verts_src = verts.size() / 3;
|
||||
const size_t num_tris = faces.size() / 3;
|
||||
@@ -184,8 +184,8 @@ static MeshChunk buildMeshChunk(uint32_t model_id,
|
||||
std::unordered_map<uint64_t, uint32_t> remap;
|
||||
remap.reserve(num_verts_src);
|
||||
|
||||
chunk.vertices.reserve(num_verts_src * INSTANCED_VERTEX_STRIDE_FLOATS);
|
||||
chunk.indices.reserve(faces.size());
|
||||
mesh.vertices.reserve(num_verts_src * INSTANCED_VERTEX_STRIDE_FLOATS);
|
||||
mesh.indices.reserve(faces.size());
|
||||
|
||||
// Track local AABB as we emit vertices.
|
||||
float local_aabb_min[3] = { std::numeric_limits<float>::max(),
|
||||
@@ -201,16 +201,16 @@ static MeshChunk buildMeshChunk(uint32_t model_id,
|
||||
if (it != remap.end()) return it->second;
|
||||
|
||||
const uint32_t new_idx = static_cast<uint32_t>(
|
||||
chunk.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS);
|
||||
mesh.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS);
|
||||
|
||||
// Subtract in double, narrow to float — preserves precision when
|
||||
// verts are far from origin and offset cancels the magnitude.
|
||||
float px = static_cast<float>(verts[orig_idx * 3 + 0] - offset.x());
|
||||
float py = static_cast<float>(verts[orig_idx * 3 + 1] - offset.y());
|
||||
float pz = static_cast<float>(verts[orig_idx * 3 + 2] - offset.z());
|
||||
chunk.vertices.push_back(px);
|
||||
chunk.vertices.push_back(py);
|
||||
chunk.vertices.push_back(pz);
|
||||
mesh.vertices.push_back(px);
|
||||
mesh.vertices.push_back(py);
|
||||
mesh.vertices.push_back(pz);
|
||||
if (px < local_aabb_min[0]) local_aabb_min[0] = px;
|
||||
if (px > local_aabb_max[0]) local_aabb_max[0] = px;
|
||||
if (py < local_aabb_min[1]) local_aabb_min[1] = py;
|
||||
@@ -219,13 +219,13 @@ static MeshChunk buildMeshChunk(uint32_t model_id,
|
||||
if (pz > local_aabb_max[2]) local_aabb_max[2] = pz;
|
||||
|
||||
if (orig_idx * 3 + 2 < normals.size()) {
|
||||
chunk.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 0]));
|
||||
chunk.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 1]));
|
||||
chunk.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 2]));
|
||||
mesh.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 0]));
|
||||
mesh.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 1]));
|
||||
mesh.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 2]));
|
||||
} else {
|
||||
chunk.vertices.push_back(0.0f);
|
||||
chunk.vertices.push_back(1.0f);
|
||||
chunk.vertices.push_back(0.0f);
|
||||
mesh.vertices.push_back(0.0f);
|
||||
mesh.vertices.push_back(1.0f);
|
||||
mesh.vertices.push_back(0.0f);
|
||||
}
|
||||
|
||||
MaterialInfo m;
|
||||
@@ -235,7 +235,7 @@ static MeshChunk buildMeshChunk(uint32_t model_id,
|
||||
uint32_t packed = packRGBA8(m);
|
||||
float packed_as_float;
|
||||
std::memcpy(&packed_as_float, &packed, sizeof(float));
|
||||
chunk.vertices.push_back(packed_as_float);
|
||||
mesh.vertices.push_back(packed_as_float);
|
||||
|
||||
remap.emplace(key, new_idx);
|
||||
return new_idx;
|
||||
@@ -243,19 +243,19 @@ static MeshChunk buildMeshChunk(uint32_t model_id,
|
||||
|
||||
for (size_t t = 0; t < num_tris; ++t) {
|
||||
const int mat_id = have_per_tri_material ? material_ids[t] : -1;
|
||||
chunk.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 0]), mat_id));
|
||||
chunk.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 1]), mat_id));
|
||||
chunk.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 2]), mat_id));
|
||||
mesh.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 0]), mat_id));
|
||||
mesh.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 1]), mat_id));
|
||||
mesh.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 2]), mat_id));
|
||||
}
|
||||
|
||||
if (chunk.vertices.empty()) {
|
||||
if (mesh.vertices.empty()) {
|
||||
for (int a = 0; a < 3; ++a) local_aabb_min[a] = local_aabb_max[a] = 0.0f;
|
||||
}
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
chunk.local_aabb_min[a] = local_aabb_min[a];
|
||||
chunk.local_aabb_max[a] = local_aabb_max[a];
|
||||
mesh.local_aabb_min[a] = local_aabb_min[a];
|
||||
mesh.local_aabb_max[a] = local_aabb_max[a];
|
||||
}
|
||||
return chunk;
|
||||
return mesh;
|
||||
}
|
||||
|
||||
// Port of ifcopenshell.util.representation.get_prioritised_contexts: rank every
|
||||
@@ -562,7 +562,6 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
info.guid = tri_elem->guid();
|
||||
info.name = tri_elem->name();
|
||||
info.type = tri_elem->type();
|
||||
info.parent_id = tri_elem->parent_id();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(elements_mutex_);
|
||||
pending_elements_.push_back(std::move(info));
|
||||
@@ -604,19 +603,19 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
}
|
||||
}
|
||||
|
||||
MeshChunk mesh_chunk =
|
||||
buildMeshChunk(model_id_, local_mesh_id, tri_elem, offset);
|
||||
StreamedMesh streamed_mesh =
|
||||
buildStreamedMesh(model_id_, local_mesh_id, tri_elem, offset);
|
||||
MeshAabb mesh_aabb;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
mesh_aabb.lmin[a] = mesh_chunk.local_aabb_min[a];
|
||||
mesh_aabb.lmax[a] = mesh_chunk.local_aabb_max[a];
|
||||
mesh_aabb.lmin[a] = streamed_mesh.local_aabb_min[a];
|
||||
mesh_aabb.lmax[a] = streamed_mesh.local_aabb_max[a];
|
||||
mesh_aabb.offset[a] = offset[a];
|
||||
}
|
||||
mesh_aabb.has_offset = (offset.squaredNorm() > 0.0);
|
||||
if (mesh_aabbs.size() <= local_mesh_id) mesh_aabbs.resize(local_mesh_id + 1);
|
||||
mesh_aabbs[local_mesh_id] = mesh_aabb;
|
||||
if (!mesh_chunk.indices.empty()) {
|
||||
emit meshReady(std::move(mesh_chunk));
|
||||
if (!streamed_mesh.indices.empty()) {
|
||||
emit meshReady(std::move(streamed_mesh));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -635,7 +634,7 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
mat_d.block<3, 1>(0, 3) += mat_d.block<3, 3>(0, 0) * mesh_rebase_offset;
|
||||
}
|
||||
|
||||
InstanceChunk inst;
|
||||
StreamedInstance inst;
|
||||
inst.model_id = model_id_;
|
||||
inst.local_mesh_id = local_mesh_id;
|
||||
inst.object_id = object_id;
|
||||
|
||||
@@ -41,7 +41,6 @@ struct ElementInfo {
|
||||
std::string guid;
|
||||
std::string name;
|
||||
std::string type;
|
||||
int parent_id;
|
||||
};
|
||||
|
||||
class GeometryStreamer : public QObject {
|
||||
@@ -70,8 +69,8 @@ public:
|
||||
|
||||
signals:
|
||||
void progressChanged(int percent);
|
||||
void meshReady(MeshChunk chunk);
|
||||
void instanceReady(InstanceChunk chunk);
|
||||
void meshReady(StreamedMesh mesh);
|
||||
void instanceReady(StreamedInstance instance_record);
|
||||
void finished();
|
||||
void cancelled();
|
||||
void errorOccurred(const QString& message);
|
||||
|
||||
@@ -84,7 +84,7 @@ bool findInstanceInModels(
|
||||
if (it == model_data.object_id_to_instance.end()) continue;
|
||||
const uint32_t instance_index = it->second;
|
||||
if (instance_index >= model_data.instances.size()) continue;
|
||||
const InstanceCpu& instance = model_data.instances[instance_index];
|
||||
const InstanceInfo& instance = model_data.instances[instance_index];
|
||||
out.model_id = model_id;
|
||||
out.mesh_id = instance.mesh_id;
|
||||
std::memcpy(out.placement_transformation,
|
||||
|
||||
@@ -66,7 +66,7 @@ void composeInstance(
|
||||
|
||||
// Result of a successful findInstance lookup. The placement_transformation
|
||||
// is double[16] column-major (pre-CoordinateOperation / FederatedFalseOrigin
|
||||
// / ModelTransformation) — the same convention as InstanceCpu so the
|
||||
// / ModelTransformation) — the same convention as InstanceInfo so the
|
||||
// measurement / picking tools can re-compose at need.
|
||||
struct InstanceLookup {
|
||||
uint32_t model_id = 0;
|
||||
|
||||
@@ -39,8 +39,8 @@
|
||||
static constexpr int INSTANCED_VERTEX_STRIDE_BYTES = 12;
|
||||
|
||||
// Streamer-side intermediate format: 7 floats per vertex (pos3 + normal3 +
|
||||
// color-as-float). GeometryStreamer writes this into MeshChunk.vertices;
|
||||
// ViewportWindow::uploadMeshChunk quantizes it down to STRIDE_BYTES on the
|
||||
// color-as-float). GeometryStreamer writes this into StreamedMesh.vertices;
|
||||
// ViewportWindow::uploadStreamedMesh quantizes it down to STRIDE_BYTES on the
|
||||
// way to the VBO. Not the GPU layout — purely a transfer convention.
|
||||
static constexpr int INSTANCED_VERTEX_STRIDE_FLOATS = 7;
|
||||
|
||||
@@ -109,7 +109,7 @@ static_assert(sizeof(InstanceGpu) == 80, "InstanceGpu must be 80 bytes");
|
||||
// uploaded to the SSBO, and used to compute world_aabb_*. When ViewportWindow's
|
||||
// stage matrices are all identity (default), transform is the float rendering
|
||||
// copy of placement_transformation.
|
||||
struct InstanceCpu {
|
||||
struct InstanceInfo {
|
||||
uint32_t mesh_id = 0; // index into meshes array
|
||||
uint32_t object_id = 0;
|
||||
uint32_t color_override_rgba8 = 0;
|
||||
@@ -120,12 +120,12 @@ struct InstanceCpu {
|
||||
float world_aabb_max[3]{};
|
||||
};
|
||||
|
||||
// Chunks emitted by the streamer to the viewport (main thread).
|
||||
// Transfer records emitted by the streamer to the viewport (main thread).
|
||||
|
||||
// Emitted the first time a representation id is seen. Carries the mesh
|
||||
// geometry in local coords. `local_mesh_id` is the streamer-assigned id
|
||||
// within this model.
|
||||
struct MeshChunk {
|
||||
struct StreamedMesh {
|
||||
uint32_t model_id = 0;
|
||||
uint32_t local_mesh_id = 0;
|
||||
std::vector<float> vertices; // 7 floats * N_verts (pos3+norm3+color1_packed)
|
||||
@@ -135,9 +135,9 @@ struct MeshChunk {
|
||||
};
|
||||
|
||||
// Emitted for every placement (every triangulation element from the
|
||||
// iterator). For the first instance of a mesh, the MeshChunk is emitted
|
||||
// iterator). For the first instance of a mesh, the StreamedMesh is emitted
|
||||
// just before this.
|
||||
struct InstanceChunk {
|
||||
struct StreamedInstance {
|
||||
uint32_t model_id = 0;
|
||||
uint32_t local_mesh_id = 0;
|
||||
uint32_t object_id = 0;
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
#include "InstancedGeometry.h"
|
||||
#include "BufferPool.h"
|
||||
#include "ChunkPlanner.h" // WGPU_CHUNK_VERTEX_BYTES_LIMIT (shared with bake)
|
||||
#include "SidecarCache.h" // PackedElementInfo (deferred property metadata)
|
||||
#include "SidecarCache.h" // ElementTableRecord (element metadata)
|
||||
|
||||
// Per-model wgpu state. Mirrors the GL backend's ModelGpuData but with
|
||||
// wgpu handles. Stage 2 only allocates and uploads the four core buffers;
|
||||
@@ -305,26 +305,27 @@ struct ModelGpuData {
|
||||
bool streaming_from_web = false;
|
||||
// Web analog of streaming_file_path: which registered JS byte-source
|
||||
// (Module.__ifcvSources[id] = a picked File or a remote URL) this model's
|
||||
// chunk + deferred reads pull from. Lets several federated models stream
|
||||
// chunk + element metadata reads pull from. Lets several federated models stream
|
||||
// from different files at once, mirroring the desktop per-model path.
|
||||
int web_source_id = 0;
|
||||
|
||||
// v15 deferred property metadata (web, on-demand). The IFC element tree
|
||||
// (elements + string_table — names/GUIDs/hierarchy, for UI/picking, never
|
||||
// v15 element metadata (web, on-demand). The IFC element metadata
|
||||
// (elements + string_table — names/GUIDs, for UI/picking, never
|
||||
// rendering) lives in a separate file block fetched only when a consumer
|
||||
// asks, so first paint doesn't wait on it. Empty until
|
||||
// loadDeferredMetadataWeb fetches [deferred_meta_offset, +bytes) and parses
|
||||
// it; deferred_meta_loaded latches so it fetches at most once.
|
||||
std::vector<PackedElementInfo> elements;
|
||||
// loadElementMetadataWeb fetches [element_metadata_comp_offset, +bytes) and parses
|
||||
// it; element_metadata_loaded latches so it fetches at most once.
|
||||
std::vector<ElementTableRecord> elements;
|
||||
std::string string_table;
|
||||
// v16: the deferred block is a single zstd frame at deferred_comp_offset of
|
||||
// deferred_comp_size bytes, expanding to deferred_raw_size.
|
||||
uint64_t deferred_comp_offset = 0;
|
||||
uint64_t deferred_comp_size = 0;
|
||||
uint64_t deferred_raw_size = 0;
|
||||
bool deferred_meta_loaded = false;
|
||||
// v16: the element metadata block is a single zstd frame at
|
||||
// element_metadata_comp_offset of element_metadata_comp_size bytes,
|
||||
// expanding to element_metadata_raw_size.
|
||||
uint64_t element_metadata_comp_offset = 0;
|
||||
uint64_t element_metadata_comp_size = 0;
|
||||
uint64_t element_metadata_raw_size = 0;
|
||||
bool element_metadata_loaded = false;
|
||||
// applyCachedModel rebases instance object_ids by this base to keep them
|
||||
// globally unique across models; deferred elements carry the sidecar's
|
||||
// globally unique across models; element metadata records carry the sidecar's
|
||||
// original (local) ids, so they're rebased by the same amount on load.
|
||||
uint32_t object_id_base = 0;
|
||||
|
||||
@@ -378,10 +379,10 @@ struct ModelGpuData {
|
||||
|
||||
// CPU side, kept for cull / picking / federation recompose.
|
||||
std::vector<MeshInfo> meshes;
|
||||
std::vector<InstanceCpu> instances;
|
||||
std::vector<InstanceInfo> instances;
|
||||
|
||||
// Per-mesh "any vertex has alpha < 255?" flag, indexed by mesh_id.
|
||||
// Populated at uploadMeshChunk / applyStreamedChunk as vertex bytes
|
||||
// Populated at uploadStreamedMesh / applyStreamedChunk as vertex bytes
|
||||
// become CPU-resident. Used at cull time to classify each instance
|
||||
// into the opaque or transparent draw partition: an instance with
|
||||
// color_override_rgba8==0 (the "use baked vertex color" sentinel)
|
||||
|
||||
@@ -279,7 +279,7 @@ void SceneLoader::applySidecarData(uint32_t mid, StreamingSidecar metadata) {
|
||||
model.has_georef = true;
|
||||
}
|
||||
|
||||
std::vector<PackedElementInfo> elements = std::move(d.elements);
|
||||
std::vector<ElementTableRecord> elements = std::move(d.elements);
|
||||
std::string stbl = std::move(d.string_table);
|
||||
|
||||
viewport_->applyCachedModel(mid, std::move(metadata));
|
||||
@@ -331,24 +331,24 @@ void SceneLoader::onStreamerProgressChanged(int percent) {
|
||||
emit progressChanged(percent);
|
||||
}
|
||||
|
||||
void SceneLoader::onStreamerMeshReady(MeshChunk chunk) {
|
||||
viewport_->uploadMeshChunk(chunk);
|
||||
void SceneLoader::onStreamerMeshReady(StreamedMesh mesh) {
|
||||
viewport_->uploadStreamedMesh(mesh);
|
||||
if (loading_model_id_ != 0) {
|
||||
auto it = models_.find(loading_model_id_);
|
||||
if (it != models_.end() && it->second.sidecar_builder) {
|
||||
it->second.sidecar_builder->onMeshReady(chunk);
|
||||
it->second.sidecar_builder->onMeshReady(mesh);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SceneLoader::onStreamerInstanceReady(InstanceChunk chunk) {
|
||||
void SceneLoader::onStreamerInstanceReady(StreamedInstance instance_record) {
|
||||
if (loading_model_id_ != 0) {
|
||||
auto it = models_.find(loading_model_id_);
|
||||
if (it != models_.end() && it->second.sidecar_builder) {
|
||||
it->second.sidecar_builder->onInstanceReady(chunk);
|
||||
it->second.sidecar_builder->onInstanceReady(instance_record);
|
||||
}
|
||||
}
|
||||
viewport_->uploadInstanceChunk(chunk);
|
||||
viewport_->uploadStreamedInstance(instance_record);
|
||||
}
|
||||
|
||||
void SceneLoader::onElementPollTick() {
|
||||
|
||||
@@ -102,7 +102,7 @@ signals:
|
||||
// packed element set. Consumer is responsible for decoding + tree/
|
||||
// property-map population. Moved arguments — avoid unnecessary copies.
|
||||
void sidecarElementsReady(uint32_t mid,
|
||||
std::vector<PackedElementInfo> elements,
|
||||
std::vector<ElementTableRecord> elements,
|
||||
std::string string_table);
|
||||
void loadedFromSidecar(uint32_t mid, qint64 elapsed_ms);
|
||||
|
||||
@@ -128,8 +128,8 @@ signals:
|
||||
|
||||
private slots:
|
||||
void onStreamerProgressChanged(int percent);
|
||||
void onStreamerMeshReady(MeshChunk chunk);
|
||||
void onStreamerInstanceReady(InstanceChunk chunk);
|
||||
void onStreamerMeshReady(StreamedMesh mesh);
|
||||
void onStreamerInstanceReady(StreamedInstance instance_record);
|
||||
void onStreamerFinished();
|
||||
void onStreamerCancelled();
|
||||
void onStreamerError(const QString& msg);
|
||||
|
||||
@@ -37,14 +37,14 @@ SidecarBuilder::SidecarBuilder(QObject* parent)
|
||||
{
|
||||
}
|
||||
|
||||
void SidecarBuilder::onMeshReady(const MeshChunk& chunk) {
|
||||
if (chunk.vertices.empty() || chunk.indices.empty()) return;
|
||||
void SidecarBuilder::onMeshReady(const StreamedMesh& mesh) {
|
||||
if (mesh.vertices.empty() || mesh.indices.empty()) return;
|
||||
|
||||
// Streamer format: 7 floats/vertex (pos3 + normal3 + color-as-float).
|
||||
const size_t n_verts = chunk.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS;
|
||||
const size_t n_verts = mesh.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS;
|
||||
|
||||
// Recompute a tight local AABB from the actual vertex positions, same
|
||||
// way ViewportWindow::uploadMeshChunk does so the .ifcview byte layout
|
||||
// way ViewportWindow::uploadStreamedMesh does so the .ifcview byte layout
|
||||
// matches the live-render path.
|
||||
float bmin[3] = { std::numeric_limits<float>::infinity(),
|
||||
std::numeric_limits<float>::infinity(),
|
||||
@@ -53,7 +53,7 @@ void SidecarBuilder::onMeshReady(const MeshChunk& chunk) {
|
||||
-std::numeric_limits<float>::infinity(),
|
||||
-std::numeric_limits<float>::infinity() };
|
||||
for (size_t i = 0; i < n_verts; ++i) {
|
||||
const float* vertex = chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS;
|
||||
const float* vertex = mesh.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
if (vertex[a] < bmin[a]) bmin[a] = vertex[a];
|
||||
if (vertex[a] > bmax[a]) bmax[a] = vertex[a];
|
||||
@@ -68,7 +68,7 @@ void SidecarBuilder::onMeshReady(const MeshChunk& chunk) {
|
||||
const size_t vb_offset = sidecar_data_.vertices.size();
|
||||
sidecar_data_.vertices.resize(vb_offset + n_verts * INSTANCED_VERTEX_STRIDE_BYTES);
|
||||
for (size_t i = 0; i < n_verts; ++i) {
|
||||
quantizeVertex(chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS,
|
||||
quantizeVertex(mesh.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS,
|
||||
bmin, extent_recip,
|
||||
sidecar_data_.vertices.data() + vb_offset
|
||||
+ i * INSTANCED_VERTEX_STRIDE_BYTES);
|
||||
@@ -76,13 +76,13 @@ void SidecarBuilder::onMeshReady(const MeshChunk& chunk) {
|
||||
|
||||
const size_t ib_offset = sidecar_data_.indices.size();
|
||||
sidecar_data_.indices.insert(sidecar_data_.indices.end(),
|
||||
chunk.indices.begin(), chunk.indices.end());
|
||||
mesh.indices.begin(), mesh.indices.end());
|
||||
|
||||
MeshInfo info;
|
||||
info.vbo_byte_offset = static_cast<uint32_t>(vb_offset);
|
||||
info.vertex_count = static_cast<uint32_t>(n_verts);
|
||||
info.ebo_byte_offset = static_cast<uint32_t>(ib_offset * sizeof(uint32_t));
|
||||
info.index_count = static_cast<uint32_t>(chunk.indices.size());
|
||||
info.index_count = static_cast<uint32_t>(mesh.indices.size());
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
info.local_aabb_min[a] = bmin[a];
|
||||
info.local_aabb_max[a] = bmax[a];
|
||||
@@ -92,30 +92,30 @@ void SidecarBuilder::onMeshReady(const MeshChunk& chunk) {
|
||||
info.lod1_ebo_byte_offset = 0;
|
||||
info.lod1_index_count = 0;
|
||||
|
||||
if (sidecar_data_.meshes.size() <= chunk.local_mesh_id) {
|
||||
sidecar_data_.meshes.resize(chunk.local_mesh_id + 1);
|
||||
if (sidecar_data_.meshes.size() <= mesh.local_mesh_id) {
|
||||
sidecar_data_.meshes.resize(mesh.local_mesh_id + 1);
|
||||
}
|
||||
sidecar_data_.meshes[chunk.local_mesh_id] = info;
|
||||
sidecar_data_.meshes[mesh.local_mesh_id] = info;
|
||||
}
|
||||
|
||||
void SidecarBuilder::onInstanceReady(const InstanceChunk& chunk) {
|
||||
InstanceCpu instance;
|
||||
instance.mesh_id = chunk.local_mesh_id;
|
||||
instance.object_id = chunk.object_id;
|
||||
instance.color_override_rgba8 = chunk.color_override_rgba8;
|
||||
instance.model_id = chunk.model_id;
|
||||
void SidecarBuilder::onInstanceReady(const StreamedInstance& instance_record) {
|
||||
InstanceInfo instance;
|
||||
instance.mesh_id = instance_record.local_mesh_id;
|
||||
instance.object_id = instance_record.object_id;
|
||||
instance.color_override_rgba8 = instance_record.color_override_rgba8;
|
||||
instance.model_id = instance_record.model_id;
|
||||
|
||||
// The streamer's chunk.transform is the double-precision
|
||||
// The streamer's instance transform is the double-precision
|
||||
// placement_transformation. The cached float transform/world_aabb is only
|
||||
// an identity-stage baseline; applyCachedModel recomposes from placement
|
||||
// against the consumer's stage matrices at load time.
|
||||
std::memcpy(instance.placement_transformation, chunk.transform,
|
||||
std::memcpy(instance.placement_transformation, instance_record.transform,
|
||||
sizeof(instance.placement_transformation));
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
instance.transform[i] = static_cast<float>(chunk.transform[i]);
|
||||
instance.transform[i] = static_cast<float>(instance_record.transform[i]);
|
||||
}
|
||||
std::memcpy(instance.world_aabb_min, chunk.world_aabb_min, sizeof(instance.world_aabb_min));
|
||||
std::memcpy(instance.world_aabb_max, chunk.world_aabb_max, sizeof(instance.world_aabb_max));
|
||||
std::memcpy(instance.world_aabb_min, instance_record.world_aabb_min, sizeof(instance.world_aabb_min));
|
||||
std::memcpy(instance.world_aabb_max, instance_record.world_aabb_max, sizeof(instance.world_aabb_max));
|
||||
|
||||
sidecar_data_.instances.push_back(instance);
|
||||
}
|
||||
@@ -140,11 +140,10 @@ SidecarData SidecarBuilder::finalize(const ModelGeoref& georef,
|
||||
sidecar_data_.map_unit_to_meters = georef.units.map_unit_to_meters;
|
||||
|
||||
for (const auto& info : elements) {
|
||||
PackedElementInfo packed;
|
||||
ElementTableRecord packed;
|
||||
packed.object_id = info.object_id;
|
||||
packed.model_id = info.model_id;
|
||||
packed.ifc_id = info.ifc_id;
|
||||
packed.parent_id = info.parent_id;
|
||||
|
||||
packed.guid_offset = static_cast<uint32_t>(sidecar_data_.string_table.size());
|
||||
packed.guid_length = static_cast<uint32_t>(info.guid.size());
|
||||
|
||||
@@ -58,8 +58,8 @@ public:
|
||||
|
||||
// Accumulator interface. Safe to call repeatedly from the same thread the
|
||||
// streamer signals are delivered to.
|
||||
void onMeshReady(const MeshChunk& chunk);
|
||||
void onInstanceReady(const InstanceChunk& chunk);
|
||||
void onMeshReady(const StreamedMesh& mesh);
|
||||
void onInstanceReady(const StreamedInstance& instance_record);
|
||||
|
||||
// Finishes assembly using the georef + element batch the host collected
|
||||
// during streaming. Returns the assembled SidecarData by move; the
|
||||
@@ -76,4 +76,3 @@ private:
|
||||
};
|
||||
|
||||
#endif // SIDECARBUILDER_H
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
// MeshInfo[num_meshes]
|
||||
//
|
||||
// uint32_t num_instances
|
||||
// InstanceCpu[num_instances] (already sorted by mesh_id; v13 layout)
|
||||
// InstanceInfo[num_instances] (already sorted by mesh_id; v13 layout)
|
||||
//
|
||||
// uint32_t has_coordinate_operation (v11+)
|
||||
// double[16] coordinate_operation_meters (v11+; column-major)
|
||||
@@ -38,7 +38,7 @@
|
||||
// double map_unit_to_meters (v11+)
|
||||
//
|
||||
// uint32_t num_elements
|
||||
// PackedElementInfo[num_elements]
|
||||
// ElementTableRecord[num_elements]
|
||||
// uint32_t string_table_bytes
|
||||
// char[string_table_bytes]
|
||||
|
||||
@@ -205,24 +205,24 @@ bool writeSidecar(const std::string& ifc_path, const SidecarData& data) {
|
||||
if (!wrU64(std::uint64_t(geom_end - geom_start))) { fclose(f); return false; }
|
||||
if (fseek(f, geom_end, SEEK_SET) != 0) { fclose(f); return false; }
|
||||
|
||||
// --- Critical metadata block (zstd): meshes, instances, georef, chunk TOC
|
||||
std::vector<std::uint8_t> critical_metadata;
|
||||
appendVec(critical_metadata, data.meshes);
|
||||
appendVec(critical_metadata, data.instances);
|
||||
appendBytes(critical_metadata, &data.has_coordinate_operation, 4);
|
||||
appendBytes(critical_metadata, data.coordinate_operation_meters, sizeof(double) * 16);
|
||||
appendBytes(critical_metadata, &data.project_length_to_meters, sizeof(double));
|
||||
appendBytes(critical_metadata, &data.map_unit_to_meters, sizeof(double));
|
||||
appendVec(critical_metadata, chunks);
|
||||
if (!wrBlock(critical_metadata)) { fclose(f); return false; }
|
||||
// --- Geometry metadata block (zstd): meshes, instances, georef, chunk TOC
|
||||
std::vector<std::uint8_t> geometry_metadata;
|
||||
appendVec(geometry_metadata, data.meshes);
|
||||
appendVec(geometry_metadata, data.instances);
|
||||
appendBytes(geometry_metadata, &data.has_coordinate_operation, 4);
|
||||
appendBytes(geometry_metadata, data.coordinate_operation_meters, sizeof(double) * 16);
|
||||
appendBytes(geometry_metadata, &data.project_length_to_meters, sizeof(double));
|
||||
appendBytes(geometry_metadata, &data.map_unit_to_meters, sizeof(double));
|
||||
appendVec(geometry_metadata, chunks);
|
||||
if (!wrBlock(geometry_metadata)) { fclose(f); return false; }
|
||||
|
||||
// --- Deferred metadata block (zstd): element tree + string table ---------
|
||||
std::vector<std::uint8_t> deferred_metadata;
|
||||
appendVec(deferred_metadata, data.elements);
|
||||
// --- Element metadata block (zstd): elements + string table --------------
|
||||
std::vector<std::uint8_t> element_metadata;
|
||||
appendVec(element_metadata, data.elements);
|
||||
std::uint32_t stbl_len = static_cast<std::uint32_t>(data.string_table.size());
|
||||
appendBytes(deferred_metadata, &stbl_len, 4);
|
||||
appendBytes(deferred_metadata, data.string_table.data(), stbl_len);
|
||||
if (!wrBlock(deferred_metadata)) { fclose(f); return false; }
|
||||
appendBytes(element_metadata, &stbl_len, 4);
|
||||
appendBytes(element_metadata, data.string_table.data(), stbl_len);
|
||||
if (!wrBlock(element_metadata)) { fclose(f); return false; }
|
||||
|
||||
fclose(f);
|
||||
return true;
|
||||
@@ -286,12 +286,12 @@ std::optional<SidecarData> readSidecar(const std::string& ifc_path) {
|
||||
out.assign(std::size_t(raw), 0);
|
||||
return SidecarCompress::decompress(z.data(), z.size(), out.data(), out.size());
|
||||
};
|
||||
std::vector<std::uint8_t> critical_metadata, deferred_metadata;
|
||||
if (!readBlock(critical_metadata) || !readBlock(deferred_metadata)) return fail();
|
||||
std::vector<std::uint8_t> geometry_metadata, element_metadata;
|
||||
if (!readBlock(geometry_metadata) || !readBlock(element_metadata)) return fail();
|
||||
fclose(f);
|
||||
|
||||
SidecarData data;
|
||||
BufReader cr{ critical_metadata.data(), critical_metadata.size() };
|
||||
BufReader cr{ geometry_metadata.data(), geometry_metadata.size() };
|
||||
if (!cr.takeVec(data.meshes)) return std::nullopt;
|
||||
if (!cr.takeVec(data.instances)) return std::nullopt;
|
||||
if (!cr.take(&data.has_coordinate_operation, 4)) return std::nullopt;
|
||||
@@ -300,7 +300,7 @@ std::optional<SidecarData> readSidecar(const std::string& ifc_path) {
|
||||
if (!cr.take(&data.map_unit_to_meters, sizeof(double))) return std::nullopt;
|
||||
if (!cr.takeVec(data.chunks)) return std::nullopt;
|
||||
|
||||
BufReader dr{ deferred_metadata.data(), deferred_metadata.size() };
|
||||
BufReader dr{ element_metadata.data(), element_metadata.size() };
|
||||
if (!dr.takeVec(data.elements)) return std::nullopt;
|
||||
std::uint32_t stbl_len = 0;
|
||||
if (!dr.take(&stbl_len, 4)) return std::nullopt;
|
||||
|
||||
@@ -46,7 +46,7 @@ static constexpr uint32_t SIDECAR_MAGIC = 0x49465657; // "IFVW"
|
||||
// same cache serves either source format. Staleness is user-managed
|
||||
// (delete the sidecar to force a rebuild).
|
||||
// v9 = unused `reserved` field dropped from header (16 B -> 12 B).
|
||||
// v10 = InstanceCpu gains placement_transformation[16] alongside transform[16]
|
||||
// v10 = InstanceInfo gains placement_transformation[16] alongside transform[16]
|
||||
// — record grew from 104 B to 168 B. placement_transformation is the
|
||||
// raw streamer output; transform is the composed FederatedFalseOrigin ·
|
||||
// ModelTransformation · CoordinateOperation · placement_transformation
|
||||
@@ -59,8 +59,8 @@ static constexpr uint32_t SIDECAR_MAGIC = 0x49465657; // "IFVW"
|
||||
// georef without re-parsing the IFC source. Edits to the IFC's
|
||||
// IfcMapConversion do NOT invalidate the sidecar — delete the
|
||||
// .ifcview manually if you change the source's georef parameters.
|
||||
// v12 = InstanceCpu::placement_transformation is double[16], and
|
||||
// InstanceChunk carries the streamer placement as double[16]. This keeps
|
||||
// v12 = InstanceInfo::placement_transformation is double[16], and
|
||||
// StreamedInstance carries the streamer placement as double[16]. This keeps
|
||||
// large IFC placements exact until CoordinateOperation / FederatedFalseOrigin
|
||||
// composition has reduced them to viewport-local float-sized values.
|
||||
// v13 = Map unit scale in cached ModelGeoref is derived from
|
||||
@@ -73,23 +73,25 @@ static constexpr uint32_t SIDECAR_MAGIC = 0x49465657; // "IFVW"
|
||||
// Morton quantisation isn't bit-identical across toolchains (x86 baker vs
|
||||
// wasm loader), so it must be baked in. No back-compat: v13 sidecars are
|
||||
// rejected (regenerate them).
|
||||
// v15 = The post-index metadata is split into a render-CRITICAL block (meshes,
|
||||
// instances, georef, chunk TOC) followed by a DEFERRED block (elements +
|
||||
// string_table — the IFC element tree, used for UI/picking, never for
|
||||
// rendering), with the critical block's byte length written just after
|
||||
// the index section. The web loader reads only the critical block before
|
||||
// v15 = The post-index metadata is split into a geometry metadata block (meshes,
|
||||
// instances, georef, chunk TOC) followed by an element metadata block (elements +
|
||||
// string_table — IFC element metadata, used for UI/picking, never for
|
||||
// rendering), with the geometry block's byte length written just after
|
||||
// the index section. The web loader reads only the geometry block before
|
||||
// painting, so first geometry no longer waits on the property data; the
|
||||
// deferred block is fetched lazily (or skipped where unused). Desktop
|
||||
// element block is fetched lazily (or skipped where unused). Desktop
|
||||
// reads both. No back-compat: regenerate sidecars.
|
||||
// v16 = Geometry + metadata are zstd-COMPRESSED. Each chunk's vertex bytes and
|
||||
// index bytes are stored as two independent zstd frames (so per-chunk
|
||||
// Range streaming still works — you fetch + decompress just one chunk),
|
||||
// and the critical + deferred metadata blocks are single zstd frames.
|
||||
// and the geometry + element metadata blocks are single zstd frames.
|
||||
// The chunk TOC records each chunk's compressed blob offsets/sizes plus
|
||||
// the raw (decompressed) sizes. ~3-5x fewer bytes over the wire while
|
||||
// keeping HTTP Range intact (unlike server Content-Encoding). No
|
||||
// back-compat: regenerate sidecars.
|
||||
static constexpr uint32_t SIDECAR_VERSION = 16;
|
||||
// v17 = Removes unused element hierarchy metadata. No back-compat: regenerate
|
||||
// sidecars.
|
||||
static constexpr uint32_t SIDECAR_VERSION = 17;
|
||||
static constexpr uint32_t SIDECAR_ENDIAN = 0x01020304;
|
||||
|
||||
// Chunk table-of-contents entry (v16). A chunk is a CONTIGUOUS range of meshes
|
||||
@@ -111,11 +113,10 @@ struct SidecarChunk {
|
||||
|
||||
// Fixed-size element record. Strings are stored as (offset, length) pairs
|
||||
// into a separate string table.
|
||||
struct PackedElementInfo {
|
||||
struct ElementTableRecord {
|
||||
uint32_t object_id;
|
||||
uint32_t model_id;
|
||||
int32_t ifc_id;
|
||||
int32_t parent_id;
|
||||
uint32_t guid_offset;
|
||||
uint32_t guid_length;
|
||||
uint32_t name_offset;
|
||||
@@ -134,7 +135,7 @@ struct SidecarData {
|
||||
|
||||
// Mesh dictionary and per-instance data.
|
||||
std::vector<MeshInfo> meshes; // indexed by local_mesh_id
|
||||
std::vector<InstanceCpu> instances; // sorted by mesh_id
|
||||
std::vector<InstanceInfo> instances; // sorted by mesh_id
|
||||
|
||||
// CoordinateOperation cache (v11+). Mirrors ModelGeoref so a sidecar
|
||||
// load can apply georef without re-parsing the IFC source.
|
||||
@@ -149,8 +150,8 @@ struct SidecarData {
|
||||
double map_unit_to_meters = 1.0;
|
||||
uint32_t has_coordinate_operation = 0;
|
||||
|
||||
// Element tree metadata.
|
||||
std::vector<PackedElementInfo> elements;
|
||||
// Element metadata.
|
||||
std::vector<ElementTableRecord> elements;
|
||||
std::string string_table;
|
||||
|
||||
// Streaming chunk TOC. Always written on disk (v14); geometry is laid out
|
||||
|
||||
@@ -88,7 +88,7 @@ void reorderSidecarByMorton(SidecarData& sd) {
|
||||
std::vector<std::uint8_t> new_vertices; new_vertices.reserve(sd.vertices.size());
|
||||
std::vector<std::uint32_t> new_indices; new_indices.reserve(sd.indices.size());
|
||||
std::vector<MeshInfo> new_meshes(mesh_count);
|
||||
std::vector<InstanceCpu> new_instances; new_instances.reserve(sd.instances.size());
|
||||
std::vector<InstanceInfo> new_instances; new_instances.reserve(sd.instances.size());
|
||||
|
||||
// Pass A: vertices + LOD0 indices + instances, mesh-by-mesh in the new
|
||||
// order, recording the new offsets on each MeshInfo.
|
||||
@@ -112,7 +112,7 @@ void reorderSidecarByMorton(SidecarData& sd) {
|
||||
new_mesh_info.first_instance = std::uint32_t(new_instances.size());
|
||||
new_mesh_info.instance_count = std::uint32_t(insts_by_mesh[old].size());
|
||||
for (std::uint32_t instance_index : insts_by_mesh[old]) {
|
||||
InstanceCpu instance = sd.instances[instance_index];
|
||||
InstanceInfo instance = sd.instances[instance_index];
|
||||
instance.mesh_id = new_mesh_index;
|
||||
new_instances.push_back(instance);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
//
|
||||
// Pure transform (no Qt / no wgpu): meshes, vertices, indices (LOD0 + LOD1),
|
||||
// and instances are all rebuilt in the new order with vbo/ebo/lod1 offsets,
|
||||
// MeshInfo.first_instance, and InstanceCpu.mesh_id remapped consistently.
|
||||
// MeshInfo.first_instance, and InstanceInfo.mesh_id remapped consistently.
|
||||
// Index values are mesh-local, so they move unchanged. Element/georef/string
|
||||
// data is mesh-independent and untouched. No-op for < 2 meshes.
|
||||
//
|
||||
|
||||
@@ -25,9 +25,9 @@
|
||||
// uint32 num_indices
|
||||
// uint32[num_indices] index data <-- streaming skips
|
||||
// uint32 num_meshes + MeshInfo[] <-- streaming reads
|
||||
// uint32 num_instances + InstanceCpu[] <-- streaming reads
|
||||
// uint32 num_instances + InstanceInfo[] <-- streaming reads
|
||||
// uint32 has_coord_op + double[16] + 2× double <-- streaming reads
|
||||
// uint32 num_elements + PackedElementInfo[] <-- streaming reads
|
||||
// uint32 num_elements + ElementTableRecord[] <-- streaming reads
|
||||
// uint32 string_table_bytes + char[] <-- streaming reads
|
||||
//
|
||||
// Streaming reader returns offsets to the two skipped sections so chunks
|
||||
@@ -100,8 +100,8 @@ bool parseSidecarHead(const uint8_t* data, size_t n, uint64_t& out_geom_bytes) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseSidecarCritical(const uint8_t* data, size_t n, SidecarData& out) {
|
||||
// v15 render-critical block: meshes, instances, georef, chunk TOC.
|
||||
bool parseSidecarGeometryMetadata(const uint8_t* data, size_t n, SidecarData& out) {
|
||||
// v15 geometry metadata block: meshes, instances, georef, chunk TOC.
|
||||
BufCursor c{data, n};
|
||||
if (!c.takeVec(out.meshes)) return false;
|
||||
if (!c.takeVec(out.instances)) return false;
|
||||
@@ -113,8 +113,8 @@ bool parseSidecarCritical(const uint8_t* data, size_t n, SidecarData& out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool parseSidecarDeferred(const uint8_t* data, size_t n, SidecarData& out) {
|
||||
// v15 deferred block: element tree + string table (UI/picking, not rendered).
|
||||
bool parseSidecarElementMetadata(const uint8_t* data, size_t n, SidecarData& out) {
|
||||
// v15+ element metadata block: elements + string table (UI/picking, not rendered).
|
||||
BufCursor c{data, n};
|
||||
if (!c.takeVec(out.elements)) return false;
|
||||
uint32_t stbl_len = 0;
|
||||
@@ -166,16 +166,18 @@ std::optional<StreamingSidecar> readSidecarMetadataOnly(const std::string& ifc_p
|
||||
return SidecarCompress::decompress(z.data(), z.size(), raw.data(), raw.size());
|
||||
};
|
||||
|
||||
std::vector<uint8_t> crit, def;
|
||||
if (!readBlock(crit)) return fail();
|
||||
if (!readBlock(def, &out.deferred_comp_offset, &out.deferred_comp_size,
|
||||
&out.deferred_raw_size)) return fail();
|
||||
std::vector<uint8_t> geometry_metadata, element_metadata;
|
||||
if (!readBlock(geometry_metadata)) return fail();
|
||||
if (!readBlock(element_metadata, &out.element_metadata_comp_offset, &out.element_metadata_comp_size,
|
||||
&out.element_metadata_raw_size)) return fail();
|
||||
std::fclose(f);
|
||||
|
||||
// Desktop reads both blocks up front; the web path reads only critical
|
||||
// before painting and fetches the deferred block on demand.
|
||||
if (!parseSidecarCritical(crit.data(), crit.size(), out.meta)) return std::nullopt;
|
||||
if (!parseSidecarDeferred(def.data(), def.size(), out.meta)) return std::nullopt;
|
||||
// Desktop reads both blocks up front; the web path reads only geometry
|
||||
// metadata before painting and fetches the element metadata block on demand.
|
||||
if (!parseSidecarGeometryMetadata(geometry_metadata.data(), geometry_metadata.size(), out.meta))
|
||||
return std::nullopt;
|
||||
if (!parseSidecarElementMetadata(element_metadata.data(), element_metadata.size(), out.meta))
|
||||
return std::nullopt;
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
struct StreamingSidecar {
|
||||
// Everything except vertices + indices — same shape as SidecarData but
|
||||
// with empty vertices / indices vectors. The renderer uses meshes /
|
||||
// instances / georef / chunks immediately (elements/strings deferred).
|
||||
// instances / georef / chunks immediately (elements/strings are element metadata).
|
||||
SidecarData meta;
|
||||
|
||||
// v16: the compressed geometry section starts here. Each chunk's two zstd
|
||||
@@ -54,12 +54,13 @@ struct StreamingSidecar {
|
||||
// a per-chunk load fetches [that, +*_comp_size) and decompresses to *_raw_size.
|
||||
uint64_t geometry_section_offset = 0;
|
||||
|
||||
// v16 deferred (property) block locator: a single zstd frame at
|
||||
// deferred_comp_offset of deferred_comp_size bytes → deferred_raw_size. The
|
||||
// web loader fetches it on demand (elements/strings); desktop reads it up front.
|
||||
uint64_t deferred_comp_offset = 0;
|
||||
uint64_t deferred_comp_size = 0;
|
||||
uint64_t deferred_raw_size = 0;
|
||||
// v16 element metadata block locator: a single zstd frame at
|
||||
// element_metadata_comp_offset of element_metadata_comp_size bytes →
|
||||
// element_metadata_raw_size. The web loader fetches it on demand
|
||||
// (elements/strings); desktop reads it up front.
|
||||
uint64_t element_metadata_comp_offset = 0;
|
||||
uint64_t element_metadata_comp_size = 0;
|
||||
uint64_t element_metadata_raw_size = 0;
|
||||
|
||||
// Resolved on-disk path so subsequent chunk reads can re-open / seek.
|
||||
std::string file_path;
|
||||
@@ -104,18 +105,18 @@ inline constexpr std::size_t SIDECAR_HEAD_BYTES = 20;
|
||||
bool parseSidecarHead(const std::uint8_t* data, std::size_t n,
|
||||
std::uint64_t& out_geom_bytes);
|
||||
|
||||
// Parse the v15 render-CRITICAL metadata block (mesh dict, instance dict,
|
||||
// Parse the v15 geometry metadata block (mesh dict, instance dict,
|
||||
// georef, chunk TOC) — everything needed to set up + draw the scene. `data`
|
||||
// points at the first critical byte; `n` is critical_meta_bytes. Returns false
|
||||
// points at the first geometry metadata byte; `n` is geometry_metadata_bytes. Returns false
|
||||
// on any bounds overrun, leaving out_meta partially filled.
|
||||
bool parseSidecarCritical(const std::uint8_t* data, std::size_t n,
|
||||
SidecarData& out_meta);
|
||||
bool parseSidecarGeometryMetadata(const std::uint8_t* data, std::size_t n,
|
||||
SidecarData& out_meta);
|
||||
|
||||
// Parse the v15 DEFERRED metadata block (element table + string table — the
|
||||
// Parse the v15 element metadata block (element table + string table — the
|
||||
// IFC property tree, used for UI/picking, never for rendering). Fetched on
|
||||
// demand. `data` points at the first deferred byte; `n` is its length.
|
||||
bool parseSidecarDeferred(const std::uint8_t* data, std::size_t n,
|
||||
SidecarData& out_meta);
|
||||
// demand. `data` points at the first element metadata byte; `n` is its length.
|
||||
bool parseSidecarElementMetadata(const std::uint8_t* data, std::size_t n,
|
||||
SidecarData& out_meta);
|
||||
|
||||
// A coalesced read plan: a single contiguous source read whose bytes are
|
||||
// scattered into the destination at the recorded offsets. Merging adjacent
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
// Inline helpers that turn streamer-format vertices (7 floats per vertex:
|
||||
// pos3 + normal3 + color-as-float) into the 12 B quantized VBO layout used
|
||||
// by both the viewport's GPU buffers and the .ifcview sidecar. Shared
|
||||
// between ViewportWindow::uploadMeshChunk and SidecarBuilder so the
|
||||
// between ViewportWindow::uploadStreamedMesh and SidecarBuilder so the
|
||||
// on-disk format stays identical to what the viewport renders.
|
||||
|
||||
#ifndef VERTEXQUANTIZATION_H
|
||||
|
||||
@@ -2858,8 +2858,8 @@ void ViewportCore::cullModelCpuUpload(ModelGpuData& m) {
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Sidecar / direct load (#84-q): applyCachedModel + uploadMeshChunk +
|
||||
// uploadInstanceChunk + finalizeModel
|
||||
// Sidecar / direct load (#84-q): applyCachedModel + uploadStreamedMesh +
|
||||
// uploadStreamedInstance + finalizeModel
|
||||
// ===========================================================================
|
||||
|
||||
#include "ChunkPlanner.h"
|
||||
@@ -3220,14 +3220,14 @@ void ViewportCore::applyCachedModel(std::uint32_t model_id,
|
||||
host_->requestFrame();
|
||||
}
|
||||
|
||||
void ViewportCore::uploadMeshChunk(const MeshChunk& chunk) {
|
||||
if (chunk.vertices.empty() || chunk.indices.empty()) return;
|
||||
SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, chunk.model_id);
|
||||
void ViewportCore::uploadStreamedMesh(const StreamedMesh& mesh) {
|
||||
if (mesh.vertices.empty() || mesh.indices.empty()) return;
|
||||
SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, mesh.model_id);
|
||||
|
||||
// Streamer format: 7 floats / vertex (pos3 + normal3 + color-as-float).
|
||||
// Same quantisation as SidecarBuilder::onMeshReady so direct-load and
|
||||
// sidecar-load produce byte-identical GPU buffers.
|
||||
const std::size_t n_verts = chunk.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS;
|
||||
const std::size_t n_verts = mesh.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS;
|
||||
|
||||
float bmin[3] = { std::numeric_limits<float>::infinity(),
|
||||
std::numeric_limits<float>::infinity(),
|
||||
@@ -3236,7 +3236,7 @@ void ViewportCore::uploadMeshChunk(const MeshChunk& chunk) {
|
||||
-std::numeric_limits<float>::infinity(),
|
||||
-std::numeric_limits<float>::infinity() };
|
||||
for (std::size_t i = 0; i < n_verts; ++i) {
|
||||
const float* vertex = chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS;
|
||||
const float* vertex = mesh.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
if (vertex[a] < bmin[a]) bmin[a] = vertex[a];
|
||||
if (vertex[a] > bmax[a]) bmax[a] = vertex[a];
|
||||
@@ -3251,7 +3251,7 @@ void ViewportCore::uploadMeshChunk(const MeshChunk& chunk) {
|
||||
const std::size_t vb_offset = s.vertices.size();
|
||||
s.vertices.resize(vb_offset + n_verts * INSTANCED_VERTEX_STRIDE_BYTES);
|
||||
for (std::size_t i = 0; i < n_verts; ++i) {
|
||||
quantizeVertex(chunk.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS,
|
||||
quantizeVertex(mesh.vertices.data() + i * INSTANCED_VERTEX_STRIDE_FLOATS,
|
||||
bmin, extent_recip,
|
||||
s.vertices.data() + vb_offset
|
||||
+ i * INSTANCED_VERTEX_STRIDE_BYTES);
|
||||
@@ -3259,13 +3259,13 @@ void ViewportCore::uploadMeshChunk(const MeshChunk& chunk) {
|
||||
|
||||
const std::size_t ib_offset = s.indices.size();
|
||||
s.indices.insert(s.indices.end(),
|
||||
chunk.indices.begin(), chunk.indices.end());
|
||||
mesh.indices.begin(), mesh.indices.end());
|
||||
|
||||
MeshInfo info{};
|
||||
info.vbo_byte_offset = std::uint32_t(vb_offset);
|
||||
info.vertex_count = std::uint32_t(n_verts);
|
||||
info.ebo_byte_offset = std::uint32_t(ib_offset * sizeof(std::uint32_t));
|
||||
info.index_count = std::uint32_t(chunk.indices.size());
|
||||
info.index_count = std::uint32_t(mesh.indices.size());
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
info.local_aabb_min[a] = bmin[a];
|
||||
info.local_aabb_max[a] = bmax[a];
|
||||
@@ -3275,27 +3275,27 @@ void ViewportCore::uploadMeshChunk(const MeshChunk& chunk) {
|
||||
info.lod1_ebo_byte_offset = 0;
|
||||
info.lod1_index_count = 0;
|
||||
|
||||
if (s.meshes.size() <= chunk.local_mesh_id) {
|
||||
s.meshes.resize(chunk.local_mesh_id + 1);
|
||||
if (s.meshes.size() <= mesh.local_mesh_id) {
|
||||
s.meshes.resize(mesh.local_mesh_id + 1);
|
||||
}
|
||||
s.meshes[chunk.local_mesh_id] = info;
|
||||
s.meshes[mesh.local_mesh_id] = info;
|
||||
}
|
||||
|
||||
void ViewportCore::uploadInstanceChunk(const InstanceChunk& chunk) {
|
||||
SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, chunk.model_id);
|
||||
void ViewportCore::uploadStreamedInstance(const StreamedInstance& instance_record) {
|
||||
SidecarData& s = getOrCreateDirectStaging(pending_direct_loads_, instance_record.model_id);
|
||||
|
||||
InstanceInfo instance{};
|
||||
instance.mesh_id = chunk.local_mesh_id;
|
||||
instance.object_id = chunk.object_id;
|
||||
instance.color_override_rgba8 = chunk.color_override_rgba8;
|
||||
instance.model_id = chunk.model_id;
|
||||
std::memcpy(instance.placement_transformation, chunk.transform,
|
||||
instance.mesh_id = instance_record.local_mesh_id;
|
||||
instance.object_id = instance_record.object_id;
|
||||
instance.color_override_rgba8 = instance_record.color_override_rgba8;
|
||||
instance.model_id = instance_record.model_id;
|
||||
std::memcpy(instance.placement_transformation, instance_record.transform,
|
||||
sizeof(instance.placement_transformation));
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
instance.transform[i] = float(chunk.transform[i]);
|
||||
instance.transform[i] = float(instance_record.transform[i]);
|
||||
}
|
||||
std::memcpy(instance.world_aabb_min, chunk.world_aabb_min, sizeof(instance.world_aabb_min));
|
||||
std::memcpy(instance.world_aabb_max, chunk.world_aabb_max, sizeof(instance.world_aabb_max));
|
||||
std::memcpy(instance.world_aabb_min, instance_record.world_aabb_min, sizeof(instance.world_aabb_min));
|
||||
std::memcpy(instance.world_aabb_max, instance_record.world_aabb_max, sizeof(instance.world_aabb_max));
|
||||
|
||||
s.instances.push_back(instance);
|
||||
}
|
||||
|
||||
@@ -472,8 +472,8 @@ public:
|
||||
// the viewer one mesh + one instance at a time, then calls
|
||||
// finalizeModel once everything's staged. The staging map lives on
|
||||
// ViewportCore so both halves can share it.
|
||||
void uploadMeshChunk(const MeshChunk& chunk);
|
||||
void uploadInstanceChunk(const InstanceChunk& chunk);
|
||||
void uploadStreamedMesh(const StreamedMesh& mesh);
|
||||
void uploadStreamedInstance(const StreamedInstance& instance_record);
|
||||
void finalizeModel(std::uint32_t model_id);
|
||||
|
||||
// ---- Cross-chunk + screenshot capture (#84-v) -------------------------
|
||||
@@ -1162,8 +1162,8 @@ private:
|
||||
// completes.
|
||||
std::string pending_screenshot_path_;
|
||||
|
||||
// Bonsai direct-load staging map. uploadMeshChunk +
|
||||
// uploadInstanceChunk append into entries keyed by model_id; the
|
||||
// Bonsai direct-load staging map. uploadStreamedMesh +
|
||||
// uploadStreamedInstance append into entries keyed by model_id; the
|
||||
// finalizeModel call moves the entry out, hands it to
|
||||
// applyCachedModel, and uploads the chunk slices synchronously.
|
||||
std::unordered_map<std::uint32_t, std::unique_ptr<SidecarData>>
|
||||
|
||||
@@ -550,17 +550,19 @@ void ViewportWindow::applyCachedModel(uint32_t model_id, StreamingSidecar metada
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Direct-IFC ingestion (mirrors GL ViewportWindow::uploadMeshChunk /
|
||||
// uploadInstanceChunk / finalizeModel). Streamer pushes chunks; we stage
|
||||
// Direct-IFC ingestion (mirrors GL ViewportWindow::uploadStreamedMesh /
|
||||
// uploadStreamedInstance / finalizeModel). Streamer pushes transfer records; we stage
|
||||
// them into a SidecarData-shaped buffer and commit at finalize via the
|
||||
// same chunk planner the sidecar load uses.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// getOrCreateDirectStaging moved to ViewportCore (anon namespace) (#84-q).
|
||||
|
||||
void ViewportWindow::uploadMeshChunk(const MeshChunk& chunk) { core_.uploadMeshChunk(chunk); }
|
||||
void ViewportWindow::uploadStreamedMesh(const StreamedMesh& mesh) { core_.uploadStreamedMesh(mesh); }
|
||||
|
||||
void ViewportWindow::uploadInstanceChunk(const InstanceChunk& chunk) { core_.uploadInstanceChunk(chunk); }
|
||||
void ViewportWindow::uploadStreamedInstance(const StreamedInstance& instance_record) {
|
||||
core_.uploadStreamedInstance(instance_record);
|
||||
}
|
||||
|
||||
void ViewportWindow::finalizeModel(uint32_t model_id) { core_.finalizeModel(model_id); }
|
||||
|
||||
|
||||
@@ -122,8 +122,8 @@ public:
|
||||
struct StreamingSidecar metadata);
|
||||
|
||||
// Direct-IFC ingestion (mirrors GL ViewportWindow). The host (typically
|
||||
// a GeometryStreamer running on a worker) calls uploadMeshChunk +
|
||||
// uploadInstanceChunk once per representation / placement as the IFC
|
||||
// a GeometryStreamer running on a worker) calls uploadStreamedMesh +
|
||||
// uploadStreamedInstance once per representation / placement as the IFC
|
||||
// triangulates; finalizeModel commits when the iterator finishes.
|
||||
// Staged in CPU memory; finalizeModel runs the chunk planner over the
|
||||
// staged data, allocates pool slices, and uploads — same render path
|
||||
@@ -131,8 +131,8 @@ public:
|
||||
// every chunk lands `is_resident=true` immediately. The streamer's
|
||||
// model_id is passed through unchanged; the viewport's globally-unique
|
||||
// object_id rebasing happens at finalize time.
|
||||
void uploadMeshChunk(const struct MeshChunk& chunk);
|
||||
void uploadInstanceChunk(const struct InstanceChunk& chunk);
|
||||
void uploadStreamedMesh(const struct StreamedMesh& mesh);
|
||||
void uploadStreamedInstance(const struct StreamedInstance& instance_record);
|
||||
void finalizeModel(uint32_t model_id);
|
||||
|
||||
void removeModel(uint32_t model_id);
|
||||
|
||||
@@ -283,7 +283,7 @@ namespace {
|
||||
ModelGpuData make_model_with_one_instance(uint32_t object_id, uint32_t mesh_id,
|
||||
double placement_tx) {
|
||||
ModelGpuData m;
|
||||
InstanceCpu inst{};
|
||||
InstanceInfo inst{};
|
||||
inst.mesh_id = mesh_id;
|
||||
inst.object_id = object_id;
|
||||
// Column-major identity with a tx for verification.
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
// vertex quantization used to fill it.
|
||||
//
|
||||
// quantizeVertex / octEncodeNormal (VertexQuantization.h) are the shared
|
||||
// production helpers: ViewportWindow::uploadMeshChunk and SidecarBuilder both
|
||||
// production helpers: ViewportWindow::uploadStreamedMesh and SidecarBuilder both
|
||||
// route through them so the rendered VBO and the on-disk .ifcview record are
|
||||
// byte-identical. The tests exercise that real implementation directly:
|
||||
// - runtime size/alignment assertions (defense in depth for the static_asserts)
|
||||
@@ -245,14 +245,14 @@ TEST_CASE("quantizeVertex passes the packed color through unchanged", "[instgeom
|
||||
REQUIRE(std::memcmp(dst + INSTANCED_VERTEX_COLOR_OFFSET, rgba, 4) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("MeshChunk and InstanceChunk default-init to zeroed metadata", "[instgeom]") {
|
||||
MeshChunk mc;
|
||||
TEST_CASE("StreamedMesh and StreamedInstance default-init to zeroed metadata", "[instgeom]") {
|
||||
StreamedMesh mc;
|
||||
REQUIRE(mc.model_id == 0);
|
||||
REQUIRE(mc.local_mesh_id == 0);
|
||||
REQUIRE(mc.vertices.empty());
|
||||
REQUIRE(mc.indices.empty());
|
||||
|
||||
InstanceChunk ic;
|
||||
StreamedInstance ic;
|
||||
REQUIRE(ic.model_id == 0);
|
||||
REQUIRE(ic.local_mesh_id == 0);
|
||||
REQUIRE(ic.object_id == 0);
|
||||
|
||||
@@ -83,7 +83,7 @@ SidecarData buildFixture() {
|
||||
|
||||
sd.instances.resize(5);
|
||||
for (size_t i = 0; i < sd.instances.size(); ++i) {
|
||||
InstanceCpu& inst = sd.instances[i];
|
||||
InstanceInfo& inst = sd.instances[i];
|
||||
inst.mesh_id = (i < 3) ? 0u : 1u;
|
||||
inst.object_id = uint32_t(100 + i);
|
||||
inst.color_override_rgba8 = uint32_t(0xAA000000u | (i * 0x010203u));
|
||||
@@ -109,11 +109,10 @@ SidecarData buildFixture() {
|
||||
sd.string_table = std::string("\0Wall\0Slab\0", 11); // includes embedded NULs
|
||||
sd.elements.resize(3);
|
||||
for (size_t i = 0; i < sd.elements.size(); ++i) {
|
||||
PackedElementInfo& e = sd.elements[i];
|
||||
ElementTableRecord& e = sd.elements[i];
|
||||
e.object_id = uint32_t(100 + i);
|
||||
e.model_id = 1;
|
||||
e.ifc_id = int32_t(1000 + i);
|
||||
e.parent_id = (i == 0) ? -1 : int32_t(100);
|
||||
e.guid_offset = 0; e.guid_length = 0;
|
||||
e.name_offset = 1; e.name_length = 4; // "Wall"
|
||||
e.type_offset = 6; e.type_length = 4; // "Slab"
|
||||
@@ -136,10 +135,10 @@ bool sidecarDataEqual(const SidecarData& a, const SidecarData& b) {
|
||||
if (std::memcmp(&a.meshes[i], &b.meshes[i], sizeof(MeshInfo)) != 0) return false;
|
||||
}
|
||||
for (size_t i = 0; i < a.instances.size(); ++i) {
|
||||
if (std::memcmp(&a.instances[i], &b.instances[i], sizeof(InstanceCpu)) != 0) return false;
|
||||
if (std::memcmp(&a.instances[i], &b.instances[i], sizeof(InstanceInfo)) != 0) return false;
|
||||
}
|
||||
for (size_t i = 0; i < a.elements.size(); ++i) {
|
||||
if (std::memcmp(&a.elements[i], &b.elements[i], sizeof(PackedElementInfo)) != 0) return false;
|
||||
if (std::memcmp(&a.elements[i], &b.elements[i], sizeof(ElementTableRecord)) != 0) return false;
|
||||
}
|
||||
|
||||
// v11 georef block.
|
||||
@@ -155,10 +154,12 @@ bool sidecarDataEqual(const SidecarData& a, const SidecarData& b) {
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("MeshInfo and InstanceCpu have stable layouts (sidecar wire format)", "[sidecar]") {
|
||||
TEST_CASE("MeshInfo and InstanceInfo have stable layouts (sidecar wire format)", "[sidecar]") {
|
||||
REQUIRE(sizeof(MeshInfo) == 56);
|
||||
REQUIRE(sizeof(InstanceInfo) == 232);
|
||||
REQUIRE(sizeof(InstanceGpu) == 80);
|
||||
REQUIRE(SIDECAR_VERSION == 16);
|
||||
REQUIRE(sizeof(ElementTableRecord) == 36);
|
||||
REQUIRE(SIDECAR_VERSION == 17);
|
||||
REQUIRE(sizeof(SidecarChunk) == 56);
|
||||
REQUIRE(SIDECAR_MAGIC == 0x49465657u);
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ SidecarData buildFixture() {
|
||||
for (uint32_t k = 0; k < 3; ++k) { // outer loop = interleave
|
||||
for (int i = 0; i < N; ++i) {
|
||||
if (k >= ninst(i)) continue;
|
||||
InstanceCpu ic;
|
||||
InstanceInfo ic;
|
||||
ic.mesh_id = uint32_t(i); // authoritative
|
||||
ic.object_id = obj++;
|
||||
ic.model_id = 1;
|
||||
@@ -114,7 +114,7 @@ struct InstSig {
|
||||
}
|
||||
};
|
||||
|
||||
InstSig sigFor(const SidecarData& sd, const InstanceCpu& inst) {
|
||||
InstSig sigFor(const SidecarData& sd, const InstanceInfo& inst) {
|
||||
const MeshInfo& m = sd.meshes.at(inst.mesh_id);
|
||||
InstSig s{};
|
||||
s.verts.assign(sd.vertices.begin() + m.vbo_byte_offset,
|
||||
|
||||
@@ -84,7 +84,6 @@ SidecarData buildFixture() {
|
||||
sd.elements[i].object_id = uint32_t(100 + i);
|
||||
sd.elements[i].model_id = 1;
|
||||
sd.elements[i].ifc_id = int32_t(1000 + i);
|
||||
sd.elements[i].parent_id = (i == 0) ? -1 : int32_t(100);
|
||||
}
|
||||
// v16 stores geometry per-chunk (compressed); a fixture with geometry needs
|
||||
// a chunk TOC covering its meshes (one chunk per mesh here).
|
||||
@@ -113,8 +112,8 @@ TEST_CASE("readSidecarMetadataOnly returns metadata, skips bulk geometry",
|
||||
// Chunk TOC carries compressed blob locators for each chunk.
|
||||
REQUIRE(meta->meta.chunks.size() == sd.chunks.size());
|
||||
REQUIRE(meta->meta.chunks[0].v_comp_size > 0);
|
||||
// The deferred (property) block locator is recorded for on-demand fetch.
|
||||
REQUIRE(meta->deferred_comp_size > 0);
|
||||
// The element metadata block locator is recorded for on-demand fetch.
|
||||
REQUIRE(meta->element_metadata_comp_size > 0);
|
||||
|
||||
// Metadata round-trips.
|
||||
REQUIRE(meta->meta.meshes.size() == sd.meshes.size());
|
||||
@@ -197,37 +196,37 @@ TEST_CASE("parseSidecarHead validates magic / version, reads geom length", "[str
|
||||
REQUIRE_FALSE(parseSidecarHead(bad, sizeof(bad), got));
|
||||
}
|
||||
|
||||
TEST_CASE("v16 deferred block: fetch via locator, decompress, parse", "[streaming]") {
|
||||
fs::path dir = makeScratchDir("v16def");
|
||||
TEST_CASE("v16 element metadata block: fetch via locator, decompress, parse", "[streaming]") {
|
||||
fs::path dir = makeScratchDir("v16element");
|
||||
fs::path ifc = dir / "model.ifc";
|
||||
SidecarData sd = buildFixture();
|
||||
REQUIRE(writeSidecar(ifc.string(), sd));
|
||||
|
||||
auto meta = readSidecarMetadataOnly(ifc.string());
|
||||
REQUIRE(meta.has_value());
|
||||
REQUIRE(meta->meta.meshes.size() == sd.meshes.size()); // critical
|
||||
REQUIRE(meta->meta.meshes.size() == sd.meshes.size()); // geometry metadata
|
||||
REQUIRE(meta->meta.chunks.size() == sd.chunks.size());
|
||||
REQUIRE(meta->meta.elements.size() == sd.elements.size()); // desktop reads deferred too
|
||||
REQUIRE(meta->deferred_comp_size > 0);
|
||||
REQUIRE(meta->meta.elements.size() == sd.elements.size()); // desktop reads element metadata too
|
||||
REQUIRE(meta->element_metadata_comp_size > 0);
|
||||
|
||||
// The on-demand path (web) fetches the compressed deferred frame via the
|
||||
// The on-demand path (web) fetches the compressed element metadata frame via the
|
||||
// recorded locator and decompresses it — verify that round-trips.
|
||||
FILE* f = std::fopen((dir / "model.ifcview").string().c_str(), "rb");
|
||||
REQUIRE(f);
|
||||
std::vector<uint8_t> cz(size_t(meta->deferred_comp_size));
|
||||
std::fseek(f, long(meta->deferred_comp_offset), SEEK_SET);
|
||||
std::vector<uint8_t> cz(size_t(meta->element_metadata_comp_size));
|
||||
std::fseek(f, long(meta->element_metadata_comp_offset), SEEK_SET);
|
||||
REQUIRE(std::fread(cz.data(), 1, cz.size(), f) == cz.size());
|
||||
std::fclose(f);
|
||||
|
||||
std::vector<uint8_t> raw(size_t(meta->deferred_raw_size));
|
||||
std::vector<uint8_t> raw(size_t(meta->element_metadata_raw_size));
|
||||
REQUIRE(SidecarCompress::decompress(cz.data(), cz.size(), raw.data(), raw.size()));
|
||||
SidecarData d;
|
||||
REQUIRE(parseSidecarDeferred(raw.data(), raw.size(), d));
|
||||
REQUIRE(parseSidecarElementMetadata(raw.data(), raw.size(), d));
|
||||
REQUIRE(d.elements.size() == sd.elements.size());
|
||||
REQUIRE(d.string_table == sd.string_table);
|
||||
|
||||
SidecarData chopped;
|
||||
REQUIRE_FALSE(parseSidecarDeferred(raw.data(), raw.size() - 1, chopped));
|
||||
REQUIRE_FALSE(parseSidecarElementMetadata(raw.data(), raw.size() - 1, chopped));
|
||||
}
|
||||
|
||||
TEST_CASE("planSidecarReadRanges coalesces adjacent ranges, keeps far ones split",
|
||||
|
||||
Reference in New Issue
Block a user