mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-19 11:43:53 +00:00
Merge branch 'ifcviewer-wgpu' of https://github.com/IfcOpenShell/IfcOpenShell into ifcviewer-wgpu
This commit is contained in:
@@ -186,6 +186,16 @@ AppSettings::NavPreset AppSettings::navPreset() const {
|
||||
return nav_preset_;
|
||||
}
|
||||
|
||||
const char* AppSettings::navPresetName(NavPreset preset) {
|
||||
switch (preset) {
|
||||
case NavPreset::Rhino: return "rhino";
|
||||
case NavPreset::Revit: return "revit";
|
||||
case NavPreset::Web: return "web";
|
||||
case NavPreset::Blender: break;
|
||||
}
|
||||
return "blender";
|
||||
}
|
||||
|
||||
void AppSettings::setNavPreset(NavPreset value) {
|
||||
if (nav_preset_ == value) return;
|
||||
nav_preset_ = value;
|
||||
@@ -220,7 +230,7 @@ void AppSettings::load() {
|
||||
static_cast<int>(NavPreset::Blender)).toInt();
|
||||
// Clamp to known values so a stale config doesn't drop us into
|
||||
// an undefined preset slot.
|
||||
if (raw < 0 || raw > static_cast<int>(NavPreset::Revit)) {
|
||||
if (raw < 0 || raw > static_cast<int>(NavPreset::Web)) {
|
||||
nav_preset_ = NavPreset::Blender;
|
||||
} else {
|
||||
nav_preset_ = static_cast<NavPreset>(raw);
|
||||
|
||||
@@ -37,13 +37,18 @@ public:
|
||||
// Blender — Orbit MMB, Pan Shift+MMB (current default)
|
||||
// Rhino — Orbit RMB, Pan Shift+RMB
|
||||
// Revit — Orbit Shift+MMB, Pan MMB
|
||||
// Web — Orbit LMB, Pan MMB, Select RMB
|
||||
enum class NavPreset {
|
||||
Blender = 0,
|
||||
Rhino = 1,
|
||||
Revit = 2,
|
||||
Web = 3,
|
||||
};
|
||||
Q_ENUM(NavPreset)
|
||||
|
||||
// Preset → the lowercase name ViewportCore::setNavPreset / applyNavPreset take.
|
||||
static const char* navPresetName(NavPreset preset);
|
||||
|
||||
static AppSettings& instance();
|
||||
|
||||
QString geometryLibrary() const;
|
||||
|
||||
@@ -41,8 +41,8 @@ void BufferPool::configure(WGPUInstance instance, WGPUDevice device,
|
||||
}
|
||||
|
||||
void BufferPool::destroy() {
|
||||
for (auto& sp : sub_pools_) {
|
||||
if (sp.buffer) wgpuBufferRelease(sp.buffer);
|
||||
for (auto& sub_pool : sub_pools_) {
|
||||
if (sub_pool.buffer) wgpuBufferRelease(sub_pool.buffer);
|
||||
}
|
||||
sub_pools_.clear();
|
||||
device_ = nullptr;
|
||||
@@ -140,7 +140,7 @@ bool BufferPool::addSubBuffer() {
|
||||
WGPUBuffer buf = wgpuDeviceCreateBuffer(device_, &desc);
|
||||
|
||||
struct PopResult { bool done = false; bool error = false; };
|
||||
auto pop = [&](PopResult& pr) {
|
||||
auto pop = [&](PopResult& pop_result) {
|
||||
WGPUPopErrorScopeCallbackInfo pcb = {};
|
||||
pcb.mode = WGPUCallbackMode_AllowProcessEvents;
|
||||
pcb.callback = [](WGPUPopErrorScopeStatus, WGPUErrorType type,
|
||||
@@ -149,9 +149,9 @@ bool BufferPool::addSubBuffer() {
|
||||
p->done = true;
|
||||
p->error = (type != WGPUErrorType_NoError);
|
||||
};
|
||||
pcb.userdata1 = ≺
|
||||
pcb.userdata1 = &pop_result;
|
||||
wgpuDevicePopErrorScope(device_, pcb);
|
||||
while (!pr.done) wgpuInstanceProcessEvents(instance_);
|
||||
while (!pop_result.done) wgpuInstanceProcessEvents(instance_);
|
||||
};
|
||||
PopResult oom_pop, validation_pop;
|
||||
pop(oom_pop);
|
||||
@@ -205,11 +205,12 @@ void BufferPool::resolveProvisionalGrowth(bool failed) {
|
||||
(unsigned long long)(total_capacity_bytes() / (1024 * 1024)),
|
||||
sub_pools_.size());
|
||||
} else {
|
||||
sub_pools_[i].provisional = false;
|
||||
SubPool& sub_pool = sub_pools_[i];
|
||||
sub_pool.provisional = false;
|
||||
std::fprintf(stderr,
|
||||
"[wgpu pool] added sub-buffer %zu (%llu MB); pool total now %llu MB\n",
|
||||
i,
|
||||
(unsigned long long)(sub_pools_[i].capacity / (1024 * 1024)),
|
||||
(unsigned long long)(sub_pool.capacity / (1024 * 1024)),
|
||||
(unsigned long long)(total_capacity_bytes() / (1024 * 1024)));
|
||||
}
|
||||
return;
|
||||
@@ -225,34 +226,34 @@ BufferPool::Slice BufferPool::alloc(uint64_t size, uint64_t align) {
|
||||
// adding another sub-buffer and retry once.
|
||||
for (int attempt = 0; attempt < 2; ++attempt) {
|
||||
for (size_t sp_idx = 0; sp_idx < sub_pools_.size(); ++sp_idx) {
|
||||
SubPool& sp = sub_pools_[sp_idx];
|
||||
SubPool& sub_pool = sub_pools_[sp_idx];
|
||||
// Web: never allocate out of a sub-buffer still awaiting OOM
|
||||
// validation — its handle may be a Dawn error buffer.
|
||||
if (sp.provisional) continue;
|
||||
for (size_t i = 0; i < sp.free_ranges.size(); ++i) {
|
||||
const FreeRange& r = sp.free_ranges[i];
|
||||
const uint64_t aligned = (r.offset + (align - 1)) & ~(align - 1);
|
||||
const uint64_t pad = aligned - r.offset;
|
||||
if (pad >= r.size) continue;
|
||||
if (size > r.size - pad) continue;
|
||||
if (sub_pool.provisional) continue;
|
||||
for (size_t i = 0; i < sub_pool.free_ranges.size(); ++i) {
|
||||
const FreeRange& free_range = sub_pool.free_ranges[i];
|
||||
const uint64_t aligned = (free_range.offset + (align - 1)) & ~(align - 1);
|
||||
const uint64_t alignment_padding = aligned - free_range.offset;
|
||||
if (alignment_padding >= free_range.size) continue;
|
||||
if (size > free_range.size - alignment_padding) continue;
|
||||
|
||||
const uint64_t post_off = aligned + size;
|
||||
const uint64_t post_size = (r.offset + r.size) - post_off;
|
||||
const uint64_t post_size = (free_range.offset + free_range.size) - post_off;
|
||||
|
||||
if (pad == 0 && post_size == 0) {
|
||||
sp.free_ranges.erase(sp.free_ranges.begin() + i);
|
||||
} else if (pad == 0) {
|
||||
sp.free_ranges[i] = {post_off, post_size};
|
||||
if (alignment_padding == 0 && post_size == 0) {
|
||||
sub_pool.free_ranges.erase(sub_pool.free_ranges.begin() + i);
|
||||
} else if (alignment_padding == 0) {
|
||||
sub_pool.free_ranges[i] = {post_off, post_size};
|
||||
} else if (post_size == 0) {
|
||||
sp.free_ranges[i] = {r.offset, pad};
|
||||
sub_pool.free_ranges[i] = {free_range.offset, alignment_padding};
|
||||
} else {
|
||||
sp.free_ranges[i] = {r.offset, pad};
|
||||
sp.free_ranges.insert(sp.free_ranges.begin() + i + 1,
|
||||
sub_pool.free_ranges[i] = {free_range.offset, alignment_padding};
|
||||
sub_pool.free_ranges.insert(sub_pool.free_ranges.begin() + i + 1,
|
||||
{post_off, post_size});
|
||||
}
|
||||
|
||||
sp.used += size;
|
||||
out.buffer = sp.buffer;
|
||||
sub_pool.used += size;
|
||||
out.buffer = sub_pool.buffer;
|
||||
out.offset = aligned;
|
||||
out.size = size;
|
||||
out.sub_idx = int(sp_idx);
|
||||
@@ -270,50 +271,56 @@ BufferPool::Slice BufferPool::alloc(uint64_t size, uint64_t align) {
|
||||
void BufferPool::free(const Slice& s) {
|
||||
if (!s.valid()) return;
|
||||
if (s.sub_idx < 0 || size_t(s.sub_idx) >= sub_pools_.size()) return;
|
||||
SubPool& sp = sub_pools_[size_t(s.sub_idx)];
|
||||
assert(s.offset + s.size <= sp.capacity);
|
||||
SubPool& sub_pool = sub_pools_[size_t(s.sub_idx)];
|
||||
assert(s.offset + s.size <= sub_pool.capacity);
|
||||
|
||||
size_t i = 0;
|
||||
while (i < sp.free_ranges.size() && sp.free_ranges[i].offset < s.offset) ++i;
|
||||
sp.free_ranges.insert(sp.free_ranges.begin() + i, {s.offset, s.size});
|
||||
sp.used -= s.size;
|
||||
while (i < sub_pool.free_ranges.size() && sub_pool.free_ranges[i].offset < s.offset) ++i;
|
||||
sub_pool.free_ranges.insert(sub_pool.free_ranges.begin() + i, {s.offset, s.size});
|
||||
sub_pool.used -= s.size;
|
||||
|
||||
if (i + 1 < sp.free_ranges.size()
|
||||
&& sp.free_ranges[i].offset + sp.free_ranges[i].size == sp.free_ranges[i + 1].offset) {
|
||||
sp.free_ranges[i].size += sp.free_ranges[i + 1].size;
|
||||
sp.free_ranges.erase(sp.free_ranges.begin() + i + 1);
|
||||
if (i + 1 < sub_pool.free_ranges.size()
|
||||
&& sub_pool.free_ranges[i].offset + sub_pool.free_ranges[i].size
|
||||
== sub_pool.free_ranges[i + 1].offset) {
|
||||
sub_pool.free_ranges[i].size += sub_pool.free_ranges[i + 1].size;
|
||||
sub_pool.free_ranges.erase(sub_pool.free_ranges.begin() + i + 1);
|
||||
}
|
||||
if (i > 0
|
||||
&& sp.free_ranges[i - 1].offset + sp.free_ranges[i - 1].size == sp.free_ranges[i].offset) {
|
||||
sp.free_ranges[i - 1].size += sp.free_ranges[i].size;
|
||||
sp.free_ranges.erase(sp.free_ranges.begin() + i);
|
||||
&& sub_pool.free_ranges[i - 1].offset + sub_pool.free_ranges[i - 1].size
|
||||
== sub_pool.free_ranges[i].offset) {
|
||||
sub_pool.free_ranges[i - 1].size += sub_pool.free_ranges[i].size;
|
||||
sub_pool.free_ranges.erase(sub_pool.free_ranges.begin() + i);
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t BufferPool::total_capacity_bytes() const {
|
||||
uint64_t s = 0;
|
||||
uint64_t total_capacity = 0;
|
||||
// Skip provisional sub-pools (web, awaiting OOM validation) — their
|
||||
// capacity isn't usable yet, so counting it would mislead the
|
||||
// evictor's "is there room?" heuristics.
|
||||
for (const auto& sp : sub_pools_) if (!sp.provisional) s += sp.capacity;
|
||||
return s;
|
||||
for (const auto& sub_pool : sub_pools_) {
|
||||
if (!sub_pool.provisional) total_capacity += sub_pool.capacity;
|
||||
}
|
||||
return total_capacity;
|
||||
}
|
||||
|
||||
uint64_t BufferPool::total_used_bytes() const {
|
||||
uint64_t s = 0;
|
||||
for (const auto& sp : sub_pools_) if (!sp.provisional) s += sp.used;
|
||||
return s;
|
||||
uint64_t total_used = 0;
|
||||
for (const auto& sub_pool : sub_pools_) {
|
||||
if (!sub_pool.provisional) total_used += sub_pool.used;
|
||||
}
|
||||
return total_used;
|
||||
}
|
||||
|
||||
uint64_t BufferPool::largest_free_run_bytes() const {
|
||||
uint64_t m = 0;
|
||||
for (const auto& sp : sub_pools_) {
|
||||
if (sp.provisional) continue;
|
||||
for (const auto& r : sp.free_ranges) {
|
||||
if (r.size > m) m = r.size;
|
||||
uint64_t largest_free_run = 0;
|
||||
for (const auto& sub_pool : sub_pools_) {
|
||||
if (sub_pool.provisional) continue;
|
||||
for (const auto& free_range : sub_pool.free_ranges) {
|
||||
if (free_range.size > largest_free_run) largest_free_run = free_range.size;
|
||||
}
|
||||
}
|
||||
return m;
|
||||
return largest_free_run;
|
||||
}
|
||||
|
||||
void BufferPool::addSubBufferForTesting(WGPUBuffer fake_buffer, uint64_t capacity) {
|
||||
|
||||
@@ -146,6 +146,7 @@ set(IFCVIEWER_CORE_SOURCES
|
||||
SidecarCompress.cpp
|
||||
StreamingLoader.cpp
|
||||
StreamingThread.cpp
|
||||
SectionGizmoRenderer.cpp
|
||||
ViewportCore.cpp
|
||||
)
|
||||
# Web needs a zstd DECODER (Emscripten has no zstd port; the desktop links the
|
||||
@@ -195,6 +196,7 @@ set(IFCVIEWER_CORE_HEADERS
|
||||
ModelGpuData.h
|
||||
FrameStats.h
|
||||
OverlayFrame.h
|
||||
SectionGizmoRenderer.h
|
||||
SectionPlane.h
|
||||
SelectionState.h
|
||||
SidecarCache.h
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -42,29 +42,29 @@ struct MaterialInfo {
|
||||
};
|
||||
|
||||
static MaterialInfo materialFromStyle(const ifcopenshell::geometry::taxonomy::style::ptr& style) {
|
||||
MaterialInfo m;
|
||||
if (!style) return m;
|
||||
MaterialInfo material;
|
||||
if (!style) return material;
|
||||
const auto& color = style->get_color();
|
||||
if (color) {
|
||||
m.r = static_cast<float>(color.r());
|
||||
m.g = static_cast<float>(color.g());
|
||||
m.b = static_cast<float>(color.b());
|
||||
material.r = static_cast<float>(color.r());
|
||||
material.g = static_cast<float>(color.g());
|
||||
material.b = static_cast<float>(color.b());
|
||||
}
|
||||
if (!std::isnan(style->transparency)) {
|
||||
m.a = 1.0f - static_cast<float>(style->transparency);
|
||||
material.a = 1.0f - static_cast<float>(style->transparency);
|
||||
}
|
||||
return m;
|
||||
return material;
|
||||
}
|
||||
|
||||
static inline uint32_t packRGBA8(const MaterialInfo& m) {
|
||||
auto to_byte = [](float v) -> uint32_t {
|
||||
float c = std::clamp(v, 0.0f, 1.0f);
|
||||
return static_cast<uint32_t>(c * 255.0f + 0.5f);
|
||||
static inline uint32_t packRGBA8(const MaterialInfo& material) {
|
||||
auto to_byte = [](float channel_value) -> uint32_t {
|
||||
float clamped_value = std::clamp(channel_value, 0.0f, 1.0f);
|
||||
return static_cast<uint32_t>(clamped_value * 255.0f + 0.5f);
|
||||
};
|
||||
uint32_t r = to_byte(m.r);
|
||||
uint32_t g = to_byte(m.g);
|
||||
uint32_t b = to_byte(m.b);
|
||||
uint32_t a = to_byte(m.a);
|
||||
uint32_t r = to_byte(material.r);
|
||||
uint32_t g = to_byte(material.g);
|
||||
uint32_t b = to_byte(material.b);
|
||||
uint32_t a = to_byte(material.a);
|
||||
// Little-endian byte layout [r,g,b,a] for GL_UNSIGNED_BYTE * 4 normalized.
|
||||
return r | (g << 8) | (b << 16) | (a << 24);
|
||||
}
|
||||
@@ -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,16 +184,16 @@ 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 amin[3] = { std::numeric_limits<float>::max(),
|
||||
std::numeric_limits<float>::max(),
|
||||
std::numeric_limits<float>::max() };
|
||||
float amax[3] = { -std::numeric_limits<float>::max(),
|
||||
-std::numeric_limits<float>::max(),
|
||||
-std::numeric_limits<float>::max() };
|
||||
float local_aabb_min[3] = { std::numeric_limits<float>::max(),
|
||||
std::numeric_limits<float>::max(),
|
||||
std::numeric_limits<float>::max() };
|
||||
float local_aabb_max[3] = { -std::numeric_limits<float>::max(),
|
||||
-std::numeric_limits<float>::max(),
|
||||
-std::numeric_limits<float>::max() };
|
||||
|
||||
auto emit_vertex = [&](uint32_t orig_idx, int mat_id) -> uint32_t {
|
||||
const uint64_t key = make_key(orig_idx, mat_id);
|
||||
@@ -201,28 +201,31 @@ 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);
|
||||
if (px < amin[0]) amin[0] = px; if (px > amax[0]) amax[0] = px;
|
||||
if (py < amin[1]) amin[1] = py; if (py > amax[1]) amax[1] = py;
|
||||
if (pz < amin[2]) amin[2] = pz; if (pz > amax[2]) amax[2] = 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;
|
||||
if (py > local_aabb_max[1]) local_aabb_max[1] = py;
|
||||
if (pz < local_aabb_min[2]) local_aabb_min[2] = pz;
|
||||
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;
|
||||
@@ -232,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;
|
||||
@@ -240,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()) {
|
||||
for (int a = 0; a < 3; ++a) amin[a] = amax[a] = 0.0f;
|
||||
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] = amin[a];
|
||||
chunk.local_aabb_max[a] = amax[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
|
||||
@@ -527,7 +530,6 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
emit errorOccurred(QString("Failed to create geometry iterator: %1").arg(e.what()));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!iterator->initialize()) {
|
||||
// No geometry survived this context for the remaining ids.
|
||||
// Subsequent contexts will pick them up; nothing to emit.
|
||||
@@ -560,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));
|
||||
@@ -602,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);
|
||||
MeshAabb ma;
|
||||
StreamedMesh streamed_mesh =
|
||||
buildStreamedMesh(model_id_, local_mesh_id, tri_elem, offset);
|
||||
MeshAabb mesh_aabb;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
ma.lmin[a] = mesh_chunk.local_aabb_min[a];
|
||||
ma.lmax[a] = mesh_chunk.local_aabb_max[a];
|
||||
ma.offset[a] = offset[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];
|
||||
}
|
||||
ma.has_offset = (offset.squaredNorm() > 0.0);
|
||||
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] = ma;
|
||||
if (!mesh_chunk.indices.empty()) {
|
||||
emit meshReady(std::move(mesh_chunk));
|
||||
mesh_aabbs[local_mesh_id] = mesh_aabb;
|
||||
if (!streamed_mesh.indices.empty()) {
|
||||
emit meshReady(std::move(streamed_mesh));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -626,14 +627,14 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
Eigen::Matrix4d mat_d =
|
||||
tri_elem->transformation().data()->ccomponents();
|
||||
if (mesh_aabbs[local_mesh_id].has_offset) {
|
||||
const Eigen::Vector3d off(
|
||||
const Eigen::Vector3d mesh_rebase_offset(
|
||||
mesh_aabbs[local_mesh_id].offset[0],
|
||||
mesh_aabbs[local_mesh_id].offset[1],
|
||||
mesh_aabbs[local_mesh_id].offset[2]);
|
||||
mat_d.block<3, 1>(0, 3) += mat_d.block<3, 3>(0, 0) * off;
|
||||
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;
|
||||
@@ -642,25 +643,25 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
inst.transform[i] = mat_d.data()[i];
|
||||
}
|
||||
|
||||
const MeshAabb& ma = mesh_aabbs[local_mesh_id];
|
||||
const MeshAabb& mesh_aabb = mesh_aabbs[local_mesh_id];
|
||||
float mat_f[16];
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
mat_f[i] = static_cast<float>(inst.transform[i]);
|
||||
}
|
||||
worldAabbFromLocal(ma.lmin, ma.lmax, mat_f,
|
||||
worldAabbFromLocal(mesh_aabb.lmin, mesh_aabb.lmax, mat_f,
|
||||
inst.world_aabb_min, inst.world_aabb_max);
|
||||
|
||||
emit instanceReady(std::move(inst));
|
||||
total_shapes++;
|
||||
yielded_count++;
|
||||
|
||||
const int p = total_count > 0
|
||||
const int progress_percent = total_count > 0
|
||||
? static_cast<int>((100 * yielded_count) / total_count)
|
||||
: 100;
|
||||
if (p != last_emitted_progress) {
|
||||
last_emitted_progress = p;
|
||||
progress_ = p;
|
||||
emit progressChanged(p);
|
||||
if (progress_percent != last_emitted_progress) {
|
||||
last_emitted_progress = progress_percent;
|
||||
progress_ = progress_percent;
|
||||
emit progressChanged(progress_percent);
|
||||
}
|
||||
} while (iterator->next());
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -79,16 +79,16 @@ bool findInstanceInModels(
|
||||
const std::unordered_map<uint32_t, ModelGpuData>& models,
|
||||
InstanceLookup& out) {
|
||||
if (object_id == 0) return false;
|
||||
for (const auto& [mid, m] : models) {
|
||||
auto it = m.object_id_to_instance.find(object_id);
|
||||
if (it == m.object_id_to_instance.end()) continue;
|
||||
const uint32_t inst_idx = it->second;
|
||||
if (inst_idx >= m.instances.size()) continue;
|
||||
const InstanceCpu& inst = m.instances[inst_idx];
|
||||
out.model_id = mid;
|
||||
out.mesh_id = inst.mesh_id;
|
||||
for (const auto& [model_id, model_data] : models) {
|
||||
auto it = model_data.object_id_to_instance.find(object_id);
|
||||
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 InstanceInfo& instance = model_data.instances[instance_index];
|
||||
out.model_id = model_id;
|
||||
out.mesh_id = instance.mesh_id;
|
||||
std::memcpy(out.placement_transformation,
|
||||
inst.placement_transformation,
|
||||
instance.placement_transformation,
|
||||
sizeof(out.placement_transformation));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -87,36 +87,6 @@ void packAxisUniform(uint8_t* dst,
|
||||
std::memcpy(dst + 92, &viewport_h, sizeof(float));
|
||||
}
|
||||
|
||||
// Pack the section uniform's 256-byte slot. Layout matches WGSL
|
||||
// SectionUniforms: mat4 + 4×(vec3 + scalar pad) + vec4 + vec2 + 8 B pad
|
||||
// = 160 B used, padded to 256.
|
||||
void packSectionUniform(uint8_t* dst,
|
||||
const Eigen::Matrix4f& mvp,
|
||||
const Eigen::Vector3f& origin, float half_size,
|
||||
const Eigen::Vector3f& tangent, float line_width_px,
|
||||
const Eigen::Vector3f& bitangent,
|
||||
const Eigen::Vector3f& normal,
|
||||
float r, float g, float b, float a,
|
||||
float viewport_w, float viewport_h) {
|
||||
std::memset(dst, 0, 256);
|
||||
std::memcpy(dst, mvp.data(), 16 * sizeof(float));
|
||||
auto put_vec3_pad = [&](size_t off, const Eigen::Vector3f& v, float pad_val) {
|
||||
float vx = v.x(), vy = v.y(), vz = v.z();
|
||||
std::memcpy(dst + off + 0, &vx, sizeof(float));
|
||||
std::memcpy(dst + off + 4, &vy, sizeof(float));
|
||||
std::memcpy(dst + off + 8, &vz, sizeof(float));
|
||||
std::memcpy(dst + off + 12, &pad_val, sizeof(float));
|
||||
};
|
||||
put_vec3_pad(64, origin, half_size);
|
||||
put_vec3_pad(80, tangent, line_width_px);
|
||||
put_vec3_pad(96, bitangent, 0.0f);
|
||||
put_vec3_pad(112, normal, 0.0f);
|
||||
float tint[4] = { r, g, b, a };
|
||||
std::memcpy(dst + 128, tint, sizeof(tint));
|
||||
std::memcpy(dst + 144, &viewport_w, sizeof(float));
|
||||
std::memcpy(dst + 148, &viewport_h, sizeof(float));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -185,46 +155,6 @@ fn vs_main(@location(0) start: vec3<f32>,
|
||||
}
|
||||
)WGSL";
|
||||
|
||||
static const std::string SECTION_WGSL = std::string(THICK_LINE_HELPERS_WGSL) + R"WGSL(
|
||||
struct SectionUniforms {
|
||||
mvp: mat4x4<f32>,
|
||||
origin: vec3<f32>,
|
||||
half_size: f32,
|
||||
tangent: vec3<f32>,
|
||||
line_width_px: f32,
|
||||
bitangent: vec3<f32>,
|
||||
_pad1: f32,
|
||||
normal: vec3<f32>,
|
||||
_pad2: f32,
|
||||
tint: vec4<f32>,
|
||||
viewport_size: vec2<f32>,
|
||||
_pad3: vec2<f32>,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> u: SectionUniforms;
|
||||
|
||||
fn plane_to_world(p: vec3<f32>) -> vec3<f32> {
|
||||
return u.origin + (u.tangent * p.x + u.bitangent * p.y + u.normal * p.z)
|
||||
* u.half_size;
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@location(0) start_local: vec3<f32>,
|
||||
@location(1) end_local: vec3<f32>,
|
||||
@location(2) col: vec3<f32>,
|
||||
@location(3) t: f32,
|
||||
@location(4) side: f32) -> VsOut {
|
||||
let p_start = u.mvp * vec4<f32>(plane_to_world(start_local), 1.0);
|
||||
let p_end = u.mvp * vec4<f32>(plane_to_world(end_local), 1.0);
|
||||
var out: VsOut;
|
||||
out.clip_pos = thick_line_clip(p_start, p_end, t, side,
|
||||
u.viewport_size, u.line_width_px);
|
||||
out.color = vec4<f32>(col * u.tint.xyz, u.tint.w);
|
||||
out.side_t = side;
|
||||
return out;
|
||||
}
|
||||
)WGSL";
|
||||
|
||||
static const std::string MARQUEE_WGSL = std::string(THICK_LINE_HELPERS_WGSL) + R"WGSL(
|
||||
struct MarqueeUniforms {
|
||||
rect_min: vec2<f32>,
|
||||
@@ -454,7 +384,7 @@ bool OverlayRenderer::init(WGPUInstance instance, WGPUDevice device,
|
||||
surface_format_ = surface_format;
|
||||
sample_count_ = sample_count;
|
||||
if (!buildAxisIndicator()) return false;
|
||||
if (!buildSectionVisualizer()) return false;
|
||||
// Section-plane gizmos moved to the shared SectionGizmoRenderer (ViewportCore).
|
||||
if (!buildMarquee()) return false;
|
||||
if (!buildOverlayLines()) return false;
|
||||
if (!buildOverlayPoints()) return false;
|
||||
@@ -476,13 +406,6 @@ void OverlayRenderer::destroy() {
|
||||
if (axis_vertex_buffer_) { wgpuBufferRelease(axis_vertex_buffer_); axis_vertex_buffer_ = nullptr; }
|
||||
|
||||
// Section visualizer
|
||||
if (section_bind_group_) { wgpuBindGroupRelease(section_bind_group_); section_bind_group_ = nullptr; }
|
||||
if (section_pipeline_) { wgpuRenderPipelineRelease(section_pipeline_); section_pipeline_ = nullptr; }
|
||||
if (section_shader_module_) { wgpuShaderModuleRelease(section_shader_module_); section_shader_module_ = nullptr; }
|
||||
if (section_pipeline_layout_) { wgpuPipelineLayoutRelease(section_pipeline_layout_); section_pipeline_layout_ = nullptr; }
|
||||
if (section_bgl_) { wgpuBindGroupLayoutRelease(section_bgl_); section_bgl_ = nullptr; }
|
||||
if (section_uniform_buffer_) { wgpuBufferRelease(section_uniform_buffer_); section_uniform_buffer_ = nullptr; }
|
||||
if (section_vertex_buffer_) { wgpuBufferRelease(section_vertex_buffer_); section_vertex_buffer_ = nullptr; }
|
||||
|
||||
// Marquee
|
||||
if (marquee_bind_group_) { wgpuBindGroupRelease(marquee_bind_group_); marquee_bind_group_ = nullptr; }
|
||||
@@ -825,198 +748,6 @@ void OverlayRenderer::encodeCornerAxis(WGPUCommandEncoder enc,
|
||||
wgpuRenderPassEncoderRelease(pass);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Section plane visualizer
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
bool OverlayRenderer::buildSectionVisualizer() {
|
||||
struct Seg {
|
||||
std::array<float, 3> s, e;
|
||||
std::array<float, 3> c;
|
||||
};
|
||||
static constexpr std::array<float, 3> kSectionRed = {1.000f, 0.200f, 0.322f};
|
||||
static const Seg segs[] = {
|
||||
// ---- quad outline ----
|
||||
{ {-1, -1, 0}, { 1, -1, 0}, kSectionRed },
|
||||
{ { 1, -1, 0}, { 1, 1, 0}, kSectionRed },
|
||||
{ { 1, 1, 0}, {-1, 1, 0}, kSectionRed },
|
||||
{ {-1, 1, 0}, {-1, -1, 0}, kSectionRed },
|
||||
// ---- arrow shaft along +n ----
|
||||
{ { 0, 0, 0}, { 0, 0, 1}, kSectionRed },
|
||||
// ---- arrow head: 4 diagonals from tip to ring at z = 0.78 ----
|
||||
{ { 0, 0, 1}, {-0.18f, 0, 0.78f}, kSectionRed },
|
||||
{ { 0, 0, 1}, { 0.18f, 0, 0.78f}, kSectionRed },
|
||||
{ { 0, 0, 1}, { 0, -0.18f, 0.78f}, kSectionRed },
|
||||
{ { 0, 0, 1}, { 0, 0.18f, 0.78f}, kSectionRed },
|
||||
};
|
||||
std::vector<float> verts;
|
||||
verts.reserve(std::size(segs) * 6 * 11);
|
||||
auto push_v = [&](const Seg& s, float t, float side) {
|
||||
verts.insert(verts.end(), { s.s[0], s.s[1], s.s[2],
|
||||
s.e[0], s.e[1], s.e[2],
|
||||
s.c[0], s.c[1], s.c[2],
|
||||
t, side });
|
||||
};
|
||||
for (const auto& s : segs) {
|
||||
push_v(s, 0.f, -1.f); push_v(s, 0.f, +1.f); push_v(s, 1.f, -1.f);
|
||||
push_v(s, 1.f, -1.f); push_v(s, 0.f, +1.f); push_v(s, 1.f, +1.f);
|
||||
}
|
||||
{
|
||||
WGPUBufferDescriptor bdesc = {};
|
||||
bdesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst;
|
||||
bdesc.size = verts.size() * sizeof(float);
|
||||
bdesc.label = svFromCStr("ifcviewer-wgpu.section_gizmo_vbo");
|
||||
section_vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc);
|
||||
wgpuQueueWriteBuffer(queue_, section_vertex_buffer_, 0,
|
||||
verts.data(), verts.size() * sizeof(float));
|
||||
}
|
||||
{
|
||||
WGPUBufferDescriptor bdesc = {};
|
||||
bdesc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
|
||||
bdesc.size = uint64_t(kMaxSectionPlanes) * kSectionUniformSlotSize;
|
||||
bdesc.label = svFromCStr("ifcviewer-wgpu.section_uniforms");
|
||||
section_uniform_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc);
|
||||
}
|
||||
{
|
||||
WGPUBindGroupLayoutEntry entry = {};
|
||||
entry.binding = 0;
|
||||
entry.visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment;
|
||||
entry.buffer.type = WGPUBufferBindingType_Uniform;
|
||||
entry.buffer.hasDynamicOffset = 1;
|
||||
entry.buffer.minBindingSize = 160;
|
||||
WGPUBindGroupLayoutDescriptor bgl_desc = {};
|
||||
bgl_desc.entryCount = 1;
|
||||
bgl_desc.entries = &entry;
|
||||
bgl_desc.label = svFromCStr("ifcviewer-wgpu.section_bgl");
|
||||
section_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc);
|
||||
}
|
||||
{
|
||||
WGPUPipelineLayoutDescriptor pl_desc = {};
|
||||
pl_desc.bindGroupLayoutCount = 1;
|
||||
pl_desc.bindGroupLayouts = §ion_bgl_;
|
||||
pl_desc.label = svFromCStr("ifcviewer-wgpu.section_pipeline_layout");
|
||||
section_pipeline_layout_ = wgpuDeviceCreatePipelineLayout(device_, &pl_desc);
|
||||
}
|
||||
{
|
||||
WGPUBindGroupEntry entry = {};
|
||||
entry.binding = 0;
|
||||
entry.buffer = section_uniform_buffer_;
|
||||
entry.offset = 0;
|
||||
entry.size = kSectionUniformSlotSize;
|
||||
WGPUBindGroupDescriptor bg_desc = {};
|
||||
bg_desc.layout = section_bgl_;
|
||||
bg_desc.entryCount = 1;
|
||||
bg_desc.entries = &entry;
|
||||
bg_desc.label = svFromCStr("ifcviewer-wgpu.section_bind_group");
|
||||
section_bind_group_ = wgpuDeviceCreateBindGroup(device_, &bg_desc);
|
||||
}
|
||||
{
|
||||
WGPUShaderSourceWGSL wgsl_src = {};
|
||||
wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL;
|
||||
wgsl_src.code = svFromCStr(SECTION_WGSL.c_str());
|
||||
WGPUShaderModuleDescriptor sm_desc = {};
|
||||
sm_desc.nextInChain = &wgsl_src.chain;
|
||||
sm_desc.label = svFromCStr("ifcviewer-wgpu.section_wgsl");
|
||||
section_shader_module_ = wgpuDeviceCreateShaderModule(device_, &sm_desc);
|
||||
}
|
||||
|
||||
WGPUVertexAttribute attribs[5] = {};
|
||||
WGPUVertexBufferLayout vbl = thickLineVertexLayout(attribs);
|
||||
|
||||
WGPUBlendState blend = {};
|
||||
blend.color.srcFactor = WGPUBlendFactor_SrcAlpha;
|
||||
blend.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
|
||||
blend.color.operation = WGPUBlendOperation_Add;
|
||||
blend.alpha.srcFactor = WGPUBlendFactor_One;
|
||||
blend.alpha.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
|
||||
blend.alpha.operation = WGPUBlendOperation_Add;
|
||||
|
||||
WGPUColorTargetState ct = {};
|
||||
ct.format = surface_format_;
|
||||
ct.blend = &blend;
|
||||
ct.writeMask = WGPUColorWriteMask_All;
|
||||
|
||||
WGPUFragmentState frag = {};
|
||||
frag.module = section_shader_module_;
|
||||
frag.entryPoint = svFromCStr("fs_main");
|
||||
frag.targetCount = 1;
|
||||
frag.targets = &ct;
|
||||
|
||||
WGPUDepthStencilState depth = {};
|
||||
depth.format = WGPUTextureFormat_Depth32Float;
|
||||
depth.depthWriteEnabled = WGPUOptionalBool_False;
|
||||
depth.depthCompare = WGPUCompareFunction_LessEqual;
|
||||
depth.stencilFront.compare = WGPUCompareFunction_Always;
|
||||
depth.stencilBack.compare = WGPUCompareFunction_Always;
|
||||
|
||||
WGPURenderPipelineDescriptor rp_desc = {};
|
||||
rp_desc.layout = section_pipeline_layout_;
|
||||
rp_desc.label = svFromCStr("ifcviewer-wgpu.section_pipeline");
|
||||
rp_desc.vertex.module = section_shader_module_;
|
||||
rp_desc.vertex.entryPoint = svFromCStr("vs_main");
|
||||
rp_desc.vertex.bufferCount = 1;
|
||||
rp_desc.vertex.buffers = &vbl;
|
||||
rp_desc.fragment = &frag;
|
||||
rp_desc.depthStencil = &depth;
|
||||
rp_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
|
||||
rp_desc.primitive.cullMode = WGPUCullMode_None;
|
||||
rp_desc.multisample.count = uint32_t(sample_count_);
|
||||
rp_desc.multisample.mask = 0xFFFFFFFFu;
|
||||
section_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp_desc);
|
||||
|
||||
return section_pipeline_ != nullptr;
|
||||
}
|
||||
|
||||
void OverlayRenderer::encodeSectionGizmos(WGPURenderPassEncoder pass,
|
||||
const OverlayFrame& f,
|
||||
const std::vector<SectionPlane>& planes) {
|
||||
if (!section_pipeline_ || planes.empty()) return;
|
||||
|
||||
wgpuRenderPassEncoderSetPipeline(pass, section_pipeline_);
|
||||
wgpuRenderPassEncoderSetVertexBuffer(pass, 0, section_vertex_buffer_, 0,
|
||||
WGPU_WHOLE_SIZE);
|
||||
|
||||
const int n = std::min<int>(int(planes.size()), kMaxSectionPlanes);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const SectionPlane& p = planes[i];
|
||||
|
||||
// Stable in-plane basis: pick the world axis least parallel to n
|
||||
// so the cross-product stays well-conditioned at any orientation.
|
||||
Eigen::Vector3f nn = p.n.normalized();
|
||||
const float ax = std::abs(nn.x()), ay = std::abs(nn.y()), az = std::abs(nn.z());
|
||||
Eigen::Vector3f seed = (ax < ay && ax < az) ? Eigen::Vector3f(1, 0, 0)
|
||||
: (ay < az) ? Eigen::Vector3f(0, 1, 0)
|
||||
: Eigen::Vector3f(0, 0, 1);
|
||||
Eigen::Vector3f tangent = nn.cross(seed);
|
||||
if (tangent.squaredNorm() < 1e-12f) tangent = Eigen::Vector3f(1, 0, 0);
|
||||
tangent.normalize();
|
||||
Eigen::Vector3f bitangent = nn.cross(tangent).normalized();
|
||||
|
||||
// Fixed 1 m half-size matches GL's renderSectionPlanes constant.
|
||||
const float half_size = 1.0f;
|
||||
const float dpr = float(std::max(1, f.device_pixel_ratio));
|
||||
const float line_w = 5.0f * dpr;
|
||||
const float vw = float(f.viewport_w_px);
|
||||
const float vh = float(f.viewport_h_px);
|
||||
|
||||
uint8_t slot[256];
|
||||
// Neutral tint — actual colours come from the per-vertex VBO
|
||||
// (red quad outline + red arrow). Tint stays available for a
|
||||
// future "selected" multiplier.
|
||||
packSectionUniform(slot, f.view_proj, p.origin, half_size,
|
||||
tangent, line_w, bitangent, nn,
|
||||
1.0f, 1.0f, 1.0f, 1.0f,
|
||||
vw, vh);
|
||||
const uint32_t slot_offset = uint32_t(i) * kSectionUniformSlotSize;
|
||||
wgpuQueueWriteBuffer(queue_, section_uniform_buffer_,
|
||||
slot_offset, slot, sizeof(slot));
|
||||
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, section_bind_group_,
|
||||
1, &slot_offset);
|
||||
wgpuRenderPassEncoderDraw(pass, 54, 1, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Marquee
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -64,12 +64,8 @@ public:
|
||||
const OverlayFrame& f,
|
||||
bool visible);
|
||||
|
||||
// Per-plane wireframe gizmo (2 × 2 m quad outline + arrow shaft +
|
||||
// arrow head). Drawn at each plane's origin in its local basis;
|
||||
// colour comes from Bonsai's decorator_color_error.
|
||||
void encodeSectionGizmos(WGPURenderPassEncoder pass,
|
||||
const OverlayFrame& f,
|
||||
const std::vector<SectionPlane>& planes);
|
||||
// Section-plane gizmos moved to the shared SectionGizmoRenderer (drawn by
|
||||
// ViewportCore for both desktop + web).
|
||||
|
||||
// Replace the highlight-triangle list. `world_xyz` is 3 floats per
|
||||
// vertex, 3 vertices per triangle, in world space (post-composed-
|
||||
@@ -175,7 +171,6 @@ public:
|
||||
|
||||
private:
|
||||
bool buildAxisIndicator();
|
||||
bool buildSectionVisualizer();
|
||||
bool buildMarquee();
|
||||
bool buildOverlayLines();
|
||||
bool buildOverlayPoints();
|
||||
@@ -218,16 +213,6 @@ private:
|
||||
WGPUBindGroup axis_bind_group_ = nullptr;
|
||||
static constexpr uint32_t kAxisUniformSlotSize = 256;
|
||||
|
||||
// ---- Section plane gizmos (1 pipeline, dynamic offset per plane) ----
|
||||
WGPUShaderModule section_shader_module_ = nullptr;
|
||||
WGPUBindGroupLayout section_bgl_ = nullptr;
|
||||
WGPUPipelineLayout section_pipeline_layout_ = nullptr;
|
||||
WGPURenderPipeline section_pipeline_ = nullptr;
|
||||
WGPUBuffer section_vertex_buffer_ = nullptr;
|
||||
WGPUBuffer section_uniform_buffer_ = nullptr;
|
||||
WGPUBindGroup section_bind_group_ = nullptr;
|
||||
static constexpr uint32_t kSectionUniformSlotSize = 256;
|
||||
|
||||
// ---- Marquee (fill + outline pipelines, one uniform buffer) ----
|
||||
WGPUShaderModule marquee_shader_module_ = nullptr;
|
||||
WGPUBindGroupLayout marquee_bgl_ = nullptr;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "SectionGizmoRenderer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kMaxPlanes = 6; // matches kMaxSectionPlanes
|
||||
constexpr uint32_t kSectionUniformSlot = 256; // dynamic-offset slot stride
|
||||
|
||||
WGPUStringView svFromCStr(const char* s) {
|
||||
WGPUStringView v;
|
||||
v.data = s;
|
||||
v.length = s ? std::strlen(s) : 0;
|
||||
return v;
|
||||
}
|
||||
|
||||
// Thick-line rendering helper (shared shape with OverlayRenderer's other
|
||||
// overlays) + the section-gizmo vertex/fragment shaders. Each line segment is
|
||||
// expanded to a screen-space-thick, anti-aliased quad.
|
||||
static const std::string SECTION_GIZMO_WGSL = std::string(R"WGSL(
|
||||
struct VsOut {
|
||||
@builtin(position) clip_pos: vec4<f32>,
|
||||
@location(0) color: vec4<f32>,
|
||||
@location(1) side_t: f32,
|
||||
};
|
||||
|
||||
fn thick_line_clip(p_start: vec4<f32>, p_end: vec4<f32>,
|
||||
t: f32, side: f32,
|
||||
viewport_size: vec2<f32>,
|
||||
line_width_px: f32) -> vec4<f32> {
|
||||
let p_here = mix(p_start, p_end, t);
|
||||
let s_start = (p_start.xy / p_start.w) * viewport_size * 0.5;
|
||||
let s_end = (p_end.xy / p_end.w ) * viewport_size * 0.5;
|
||||
let dir = normalize(s_end - s_start);
|
||||
let perp = vec2<f32>(-dir.y, dir.x);
|
||||
let off_pixels = perp * (line_width_px * 0.5) * side;
|
||||
let off_ndc = off_pixels * 2.0 / viewport_size;
|
||||
return vec4<f32>(p_here.xy + off_ndc * p_here.w, p_here.zw);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
||||
let d = abs(in.side_t);
|
||||
let aa = fwidth(in.side_t);
|
||||
let coverage = 1.0 - smoothstep(1.0 - aa, 1.0, d);
|
||||
return vec4<f32>(in.color.xyz, in.color.w * coverage);
|
||||
}
|
||||
|
||||
struct SectionUniforms {
|
||||
mvp: mat4x4<f32>,
|
||||
origin: vec3<f32>,
|
||||
half_size: f32,
|
||||
tangent: vec3<f32>,
|
||||
line_width_px: f32,
|
||||
bitangent: vec3<f32>,
|
||||
_pad1: f32,
|
||||
normal: vec3<f32>,
|
||||
_pad2: f32,
|
||||
tint: vec4<f32>,
|
||||
viewport_size: vec2<f32>,
|
||||
_pad3: vec2<f32>,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> u: SectionUniforms;
|
||||
|
||||
fn plane_to_world(p: vec3<f32>) -> vec3<f32> {
|
||||
return u.origin + (u.tangent * p.x + u.bitangent * p.y + u.normal * p.z)
|
||||
* u.half_size;
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@location(0) start_local: vec3<f32>,
|
||||
@location(1) end_local: vec3<f32>,
|
||||
@location(2) col: vec3<f32>,
|
||||
@location(3) t: f32,
|
||||
@location(4) side: f32) -> VsOut {
|
||||
let p_start = u.mvp * vec4<f32>(plane_to_world(start_local), 1.0);
|
||||
let p_end = u.mvp * vec4<f32>(plane_to_world(end_local), 1.0);
|
||||
var out: VsOut;
|
||||
out.clip_pos = thick_line_clip(p_start, p_end, t, side,
|
||||
u.viewport_size, u.line_width_px);
|
||||
out.color = vec4<f32>(col * u.tint.xyz, u.tint.w);
|
||||
out.side_t = side;
|
||||
return out;
|
||||
}
|
||||
)WGSL");
|
||||
|
||||
// Pack the 256-byte dynamic-offset slot. Layout matches SectionUniforms above:
|
||||
// mat4 + 4×(vec3 + scalar) + vec4 + vec2 + pad = 160 B used, padded to 256.
|
||||
void packSectionUniform(uint8_t* dst,
|
||||
const Eigen::Matrix4f& mvp,
|
||||
const Eigen::Vector3f& origin, float half_size,
|
||||
const Eigen::Vector3f& tangent, float line_width_px,
|
||||
const Eigen::Vector3f& bitangent,
|
||||
const Eigen::Vector3f& normal,
|
||||
float r, float g, float b, float a,
|
||||
float viewport_w, float viewport_h) {
|
||||
std::memset(dst, 0, 256);
|
||||
std::memcpy(dst, mvp.data(), 16 * sizeof(float));
|
||||
auto put_vec3_pad = [&](size_t off, const Eigen::Vector3f& v, float pad_val) {
|
||||
float vx = v.x(), vy = v.y(), vz = v.z();
|
||||
std::memcpy(dst + off + 0, &vx, sizeof(float));
|
||||
std::memcpy(dst + off + 4, &vy, sizeof(float));
|
||||
std::memcpy(dst + off + 8, &vz, sizeof(float));
|
||||
std::memcpy(dst + off + 12, &pad_val, sizeof(float));
|
||||
};
|
||||
put_vec3_pad(64, origin, half_size);
|
||||
put_vec3_pad(80, tangent, line_width_px);
|
||||
put_vec3_pad(96, bitangent, 0.0f);
|
||||
put_vec3_pad(112, normal, 0.0f);
|
||||
float tint[4] = { r, g, b, a };
|
||||
std::memcpy(dst + 128, tint, sizeof(tint));
|
||||
std::memcpy(dst + 144, &viewport_w, sizeof(float));
|
||||
std::memcpy(dst + 148, &viewport_h, sizeof(float));
|
||||
}
|
||||
|
||||
// Stable in-plane basis: pick the world axis least parallel to n so the
|
||||
// cross-product stays well-conditioned at any orientation.
|
||||
void planeBasis(const Eigen::Vector3f& n_in,
|
||||
Eigen::Vector3f& nn, Eigen::Vector3f& tangent, Eigen::Vector3f& bitangent) {
|
||||
nn = n_in.normalized();
|
||||
const float ax = std::abs(nn.x()), ay = std::abs(nn.y()), az = std::abs(nn.z());
|
||||
Eigen::Vector3f seed = (ax < ay && ax < az) ? Eigen::Vector3f(1, 0, 0)
|
||||
: (ay < az) ? Eigen::Vector3f(0, 1, 0)
|
||||
: Eigen::Vector3f(0, 0, 1);
|
||||
tangent = nn.cross(seed);
|
||||
if (tangent.squaredNorm() < 1e-12f) tangent = Eigen::Vector3f(1, 0, 0);
|
||||
tangent.normalize();
|
||||
bitangent = nn.cross(tangent).normalized();
|
||||
}
|
||||
|
||||
bool projectWorldToLogicalScreen(const Eigen::Matrix4f& vp, const Eigen::Vector3f& world,
|
||||
int win_w, int win_h, Eigen::Vector2f& out) {
|
||||
const Eigen::Vector4f clip = vp * Eigen::Vector4f(world.x(), world.y(), world.z(), 1.0f);
|
||||
if (clip.w() <= 0.0f) return false;
|
||||
const float invw = 1.0f / clip.w();
|
||||
out = Eigen::Vector2f((clip.x() * invw * 0.5f + 0.5f) * float(win_w),
|
||||
(1.0f - (clip.y() * invw * 0.5f + 0.5f)) * float(win_h));
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SectionGizmoRenderer::~SectionGizmoRenderer() { destroy(); }
|
||||
|
||||
bool SectionGizmoRenderer::init(WGPUDevice device, WGPUQueue queue,
|
||||
WGPUTextureFormat color_format, int sample_count) {
|
||||
device_ = device;
|
||||
queue_ = queue;
|
||||
if (!device_ || !queue_) return false;
|
||||
|
||||
// ---- Gizmo geometry: 9 line segments (quad outline + normal arrow) ----
|
||||
struct Seg { std::array<float, 3> s, e, c; };
|
||||
static constexpr std::array<float, 3> kRed = { 1.000f, 0.200f, 0.322f };
|
||||
static const Seg segs[] = {
|
||||
{ {-1, -1, 0}, { 1, -1, 0}, kRed }, // quad outline
|
||||
{ { 1, -1, 0}, { 1, 1, 0}, kRed },
|
||||
{ { 1, 1, 0}, {-1, 1, 0}, kRed },
|
||||
{ {-1, 1, 0}, {-1, -1, 0}, kRed },
|
||||
{ { 0, 0, 0}, { 0, 0, 1}, kRed }, // arrow shaft along +n
|
||||
{ { 0, 0, 1}, {-0.18f, 0, 0.78f}, kRed }, // arrow head
|
||||
{ { 0, 0, 1}, { 0.18f, 0, 0.78f}, kRed },
|
||||
{ { 0, 0, 1}, { 0, -0.18f, 0.78f}, kRed },
|
||||
{ { 0, 0, 1}, { 0, 0.18f, 0.78f}, kRed },
|
||||
};
|
||||
std::vector<float> verts;
|
||||
verts.reserve(std::size(segs) * 6 * 11);
|
||||
auto push_v = [&](const Seg& s, float t, float side) {
|
||||
verts.insert(verts.end(), { s.s[0], s.s[1], s.s[2], s.e[0], s.e[1], s.e[2],
|
||||
s.c[0], s.c[1], s.c[2], t, side });
|
||||
};
|
||||
for (const auto& s : segs) {
|
||||
push_v(s, 0.f, -1.f); push_v(s, 0.f, +1.f); push_v(s, 1.f, -1.f);
|
||||
push_v(s, 1.f, -1.f); push_v(s, 0.f, +1.f); push_v(s, 1.f, +1.f);
|
||||
}
|
||||
vertex_count_ = int(std::size(segs)) * 6;
|
||||
|
||||
WGPUBufferDescriptor vb = {};
|
||||
vb.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst;
|
||||
vb.size = verts.size() * sizeof(float);
|
||||
vb.label = svFromCStr("ifcviewer-wgpu.section_gizmo_vbo");
|
||||
vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &vb);
|
||||
wgpuQueueWriteBuffer(queue_, vertex_buffer_, 0, verts.data(), verts.size() * sizeof(float));
|
||||
|
||||
WGPUBufferDescriptor ub = {};
|
||||
ub.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
|
||||
ub.size = uint64_t(kMaxPlanes) * kSectionUniformSlot;
|
||||
ub.label = svFromCStr("ifcviewer-wgpu.section_gizmo_uniforms");
|
||||
uniform_buffer_ = wgpuDeviceCreateBuffer(device_, &ub);
|
||||
|
||||
WGPUBindGroupLayoutEntry ble = {};
|
||||
ble.binding = 0;
|
||||
ble.visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment;
|
||||
ble.buffer.type = WGPUBufferBindingType_Uniform;
|
||||
ble.buffer.hasDynamicOffset = 1;
|
||||
ble.buffer.minBindingSize = 160;
|
||||
WGPUBindGroupLayoutDescriptor bgl_desc = {};
|
||||
bgl_desc.entryCount = 1;
|
||||
bgl_desc.entries = &ble;
|
||||
bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc);
|
||||
|
||||
WGPUPipelineLayoutDescriptor pl_desc = {};
|
||||
pl_desc.bindGroupLayoutCount = 1;
|
||||
pl_desc.bindGroupLayouts = &bgl_;
|
||||
layout_ = wgpuDeviceCreatePipelineLayout(device_, &pl_desc);
|
||||
|
||||
WGPUBindGroupEntry bge = {};
|
||||
bge.binding = 0;
|
||||
bge.buffer = uniform_buffer_;
|
||||
bge.offset = 0;
|
||||
bge.size = kSectionUniformSlot;
|
||||
WGPUBindGroupDescriptor bg_desc = {};
|
||||
bg_desc.layout = bgl_;
|
||||
bg_desc.entryCount = 1;
|
||||
bg_desc.entries = &bge;
|
||||
bind_group_ = wgpuDeviceCreateBindGroup(device_, &bg_desc);
|
||||
|
||||
WGPUShaderSourceWGSL wgsl = {};
|
||||
wgsl.chain.sType = WGPUSType_ShaderSourceWGSL;
|
||||
wgsl.code = svFromCStr(SECTION_GIZMO_WGSL.c_str());
|
||||
WGPUShaderModuleDescriptor sm_desc = {};
|
||||
sm_desc.nextInChain = &wgsl.chain;
|
||||
shader_ = wgpuDeviceCreateShaderModule(device_, &sm_desc);
|
||||
|
||||
// Vertex layout: start_local vec3, end_local vec3, col vec3, t f32, side f32.
|
||||
WGPUVertexAttribute attribs[5] = {};
|
||||
attribs[0].format = WGPUVertexFormat_Float32x3; attribs[0].offset = 0; attribs[0].shaderLocation = 0;
|
||||
attribs[1].format = WGPUVertexFormat_Float32x3; attribs[1].offset = 12; attribs[1].shaderLocation = 1;
|
||||
attribs[2].format = WGPUVertexFormat_Float32x3; attribs[2].offset = 24; attribs[2].shaderLocation = 2;
|
||||
attribs[3].format = WGPUVertexFormat_Float32; attribs[3].offset = 36; attribs[3].shaderLocation = 3;
|
||||
attribs[4].format = WGPUVertexFormat_Float32; attribs[4].offset = 40; attribs[4].shaderLocation = 4;
|
||||
WGPUVertexBufferLayout vbl = {};
|
||||
vbl.arrayStride = 44;
|
||||
vbl.stepMode = WGPUVertexStepMode_Vertex;
|
||||
vbl.attributeCount = 5;
|
||||
vbl.attributes = attribs;
|
||||
|
||||
WGPUBlendState blend = {};
|
||||
blend.color.srcFactor = WGPUBlendFactor_SrcAlpha;
|
||||
blend.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
|
||||
blend.color.operation = WGPUBlendOperation_Add;
|
||||
blend.alpha.srcFactor = WGPUBlendFactor_One;
|
||||
blend.alpha.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
|
||||
blend.alpha.operation = WGPUBlendOperation_Add;
|
||||
WGPUColorTargetState ct = {};
|
||||
ct.format = color_format;
|
||||
ct.blend = &blend;
|
||||
ct.writeMask = WGPUColorWriteMask_All;
|
||||
|
||||
WGPUFragmentState frag = {};
|
||||
frag.module = shader_;
|
||||
frag.entryPoint = svFromCStr("fs_main");
|
||||
frag.targetCount = 1;
|
||||
frag.targets = &ct;
|
||||
|
||||
// Depth-test against geometry (LessEqual) but don't write depth.
|
||||
WGPUDepthStencilState depth = {};
|
||||
depth.format = WGPUTextureFormat_Depth32Float;
|
||||
depth.depthWriteEnabled = WGPUOptionalBool_False;
|
||||
depth.depthCompare = WGPUCompareFunction_LessEqual;
|
||||
depth.stencilFront.compare = WGPUCompareFunction_Always;
|
||||
depth.stencilBack.compare = WGPUCompareFunction_Always;
|
||||
|
||||
WGPURenderPipelineDescriptor rp = {};
|
||||
rp.layout = layout_;
|
||||
rp.label = svFromCStr("ifcviewer-wgpu.section_gizmo_pipeline");
|
||||
rp.vertex.module = shader_;
|
||||
rp.vertex.entryPoint = svFromCStr("vs_main");
|
||||
rp.vertex.bufferCount = 1;
|
||||
rp.vertex.buffers = &vbl;
|
||||
rp.fragment = &frag;
|
||||
rp.depthStencil = &depth;
|
||||
rp.primitive.topology = WGPUPrimitiveTopology_TriangleList;
|
||||
rp.primitive.cullMode = WGPUCullMode_None;
|
||||
rp.multisample.count = uint32_t(sample_count);
|
||||
rp.multisample.mask = 0xFFFFFFFFu;
|
||||
pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp);
|
||||
return pipeline_ != nullptr;
|
||||
}
|
||||
|
||||
void SectionGizmoRenderer::encode(WGPURenderPassEncoder pass, const Eigen::Matrix4f& view_proj,
|
||||
const std::vector<SectionPlane>& planes,
|
||||
int viewport_w_px, int viewport_h_px, int device_pixel_ratio) {
|
||||
if (!pipeline_ || planes.empty()) return;
|
||||
wgpuRenderPassEncoderSetPipeline(pass, pipeline_);
|
||||
wgpuRenderPassEncoderSetVertexBuffer(pass, 0, vertex_buffer_, 0, WGPU_WHOLE_SIZE);
|
||||
|
||||
const float dpr = float(std::max(1, device_pixel_ratio));
|
||||
const float line_w = 5.0f * dpr;
|
||||
const float vw = float(viewport_w_px);
|
||||
const float vh = float(viewport_h_px);
|
||||
const int n = std::min<int>(int(planes.size()), kMaxPlanes);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const SectionPlane& p = planes[i];
|
||||
Eigen::Vector3f nn, tangent, bitangent;
|
||||
planeBasis(p.n, nn, tangent, bitangent);
|
||||
// Fixed 1 m gizmo (matches the desktop OverlayRenderer / GL constant).
|
||||
// NOT visual_radius: the normal is flipped toward the camera, so a large
|
||||
// arrow would shoot past the eye (clip.w<0) and vanish.
|
||||
const float half = 1.0f;
|
||||
|
||||
uint8_t slot[256];
|
||||
packSectionUniform(slot, view_proj, p.origin, half, tangent, line_w,
|
||||
bitangent, nn, 1.0f, 1.0f, 1.0f, 1.0f, vw, vh);
|
||||
const uint32_t slot_offset = uint32_t(i) * kSectionUniformSlot;
|
||||
wgpuQueueWriteBuffer(queue_, uniform_buffer_, slot_offset, slot, sizeof(slot));
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 1, &slot_offset);
|
||||
wgpuRenderPassEncoderDraw(pass, uint32_t(vertex_count_), 1, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
int SectionGizmoRenderer::hitTest(int x, int y, const std::vector<SectionPlane>& planes,
|
||||
const Eigen::Matrix4f& view, const Eigen::Matrix4f& proj,
|
||||
int viewport_w_px, int viewport_h_px, float tolerance_px) {
|
||||
const Eigen::Matrix4f vp = proj * view;
|
||||
const Eigen::Vector2f q{ float(x), float(y) };
|
||||
int best_i = -1;
|
||||
float best_d = tolerance_px;
|
||||
const int n = std::min<int>(int(planes.size()), kMaxPlanes);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
const SectionPlane& p = planes[i];
|
||||
// The arrow runs origin → origin + n * 1 m (visual radius scales the
|
||||
// gizmo, but hit-test the unit arrow to mirror the desktop).
|
||||
Eigen::Vector2f s_origin, s_tip;
|
||||
if (!projectWorldToLogicalScreen(vp, p.origin, viewport_w_px, viewport_h_px, s_origin)) continue;
|
||||
if (!projectWorldToLogicalScreen(vp, p.origin + p.n * 1.0f, viewport_w_px, viewport_h_px, s_tip)) continue;
|
||||
const Eigen::Vector2f ab = s_tip - s_origin;
|
||||
const float ab_len2 = ab.squaredNorm();
|
||||
if (ab_len2 < 1e-3f) continue;
|
||||
float t = (q - s_origin).dot(ab) / ab_len2;
|
||||
t = std::clamp(t, 0.0f, 1.0f);
|
||||
const Eigen::Vector2f proj_pt = s_origin + ab * t;
|
||||
const float d = (q - proj_pt).norm();
|
||||
if (d < best_d) { best_d = d; best_i = i; }
|
||||
}
|
||||
return best_i;
|
||||
}
|
||||
|
||||
void SectionGizmoRenderer::destroy() {
|
||||
if (pipeline_) { wgpuRenderPipelineRelease(pipeline_); pipeline_ = nullptr; }
|
||||
if (layout_) { wgpuPipelineLayoutRelease(layout_); layout_ = nullptr; }
|
||||
if (bgl_) { wgpuBindGroupLayoutRelease(bgl_); bgl_ = nullptr; }
|
||||
if (bind_group_) { wgpuBindGroupRelease(bind_group_); bind_group_ = nullptr; }
|
||||
if (vertex_buffer_) { wgpuBufferRelease(vertex_buffer_); vertex_buffer_ = nullptr; }
|
||||
if (uniform_buffer_) { wgpuBufferRelease(uniform_buffer_); uniform_buffer_ = nullptr; }
|
||||
if (shader_) { wgpuShaderModuleRelease(shader_); shader_ = nullptr; }
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef SECTIONGIZMORENDERER_H
|
||||
#define SECTIONGIZMORENDERER_H
|
||||
|
||||
#include <webgpu/webgpu.h>
|
||||
|
||||
#include <Eigen/Dense>
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "SectionPlane.h"
|
||||
|
||||
// Qt-free renderer for the section-plane gizmo — a red quad outline plus a
|
||||
// normal arrow, drawn with anti-aliased thick lines. Lifted out of the
|
||||
// Qt-coupled OverlayRenderer so BOTH the desktop and web builds draw one
|
||||
// identical gizmo from a single place (ViewportCore::render calls it on both).
|
||||
//
|
||||
// The gizmo is plane-local geometry scaled by each plane's visual radius and
|
||||
// oriented by a stable tangent/bitangent basis derived from the plane normal.
|
||||
class SectionGizmoRenderer {
|
||||
public:
|
||||
SectionGizmoRenderer() = default;
|
||||
~SectionGizmoRenderer();
|
||||
SectionGizmoRenderer(const SectionGizmoRenderer&) = delete;
|
||||
SectionGizmoRenderer& operator=(const SectionGizmoRenderer&) = delete;
|
||||
|
||||
// Create the pipeline, gizmo VBO, and per-plane uniform buffer. `color_format`
|
||||
// is the render target's format; `sample_count` the MSAA count. Returns false
|
||||
// (and leaves the renderer inert) if pipeline creation fails.
|
||||
bool init(WGPUDevice device, WGPUQueue queue,
|
||||
WGPUTextureFormat color_format, int sample_count);
|
||||
void destroy();
|
||||
bool ready() const { return pipeline_ != nullptr; }
|
||||
|
||||
// Draw one gizmo per plane into an already-open render pass (the main pass).
|
||||
void encode(WGPURenderPassEncoder pass, const Eigen::Matrix4f& view_proj,
|
||||
const std::vector<SectionPlane>& planes,
|
||||
int viewport_w_px, int viewport_h_px, int device_pixel_ratio);
|
||||
|
||||
// Screen-space hit test: index of the plane whose gizmo (arrow segment,
|
||||
// origin→origin+normal) the (x, y) logical-pixel point lies within
|
||||
// `tolerance_px` of, or -1. Pure math — no GPU. Nearest wins.
|
||||
static int hitTest(int x, int y, const std::vector<SectionPlane>& planes,
|
||||
const Eigen::Matrix4f& view, const Eigen::Matrix4f& proj,
|
||||
int viewport_w_px, int viewport_h_px,
|
||||
float tolerance_px = 12.0f);
|
||||
|
||||
private:
|
||||
WGPUDevice device_ = nullptr;
|
||||
WGPUQueue queue_ = nullptr;
|
||||
WGPURenderPipeline pipeline_ = nullptr;
|
||||
WGPUPipelineLayout layout_ = nullptr;
|
||||
WGPUBindGroupLayout bgl_ = nullptr;
|
||||
WGPUBindGroup bind_group_ = nullptr;
|
||||
WGPUBuffer vertex_buffer_ = nullptr;
|
||||
WGPUBuffer uniform_buffer_ = nullptr;
|
||||
WGPUShaderModule shader_ = nullptr;
|
||||
int vertex_count_ = 0;
|
||||
};
|
||||
|
||||
#endif // SECTIONGIZMORENDERER_H
|
||||
@@ -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,10 +53,10 @@ 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* v = 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 (v[a] < bmin[a]) bmin[a] = v[a];
|
||||
if (v[a] > bmax[a]) bmax[a] = v[a];
|
||||
if (vertex[a] < bmin[a]) bmin[a] = vertex[a];
|
||||
if (vertex[a] > bmax[a]) bmax[a] = vertex[a];
|
||||
}
|
||||
}
|
||||
float extent_recip[3];
|
||||
@@ -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,32 +92,32 @@ 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 inst;
|
||||
inst.mesh_id = chunk.local_mesh_id;
|
||||
inst.object_id = chunk.object_id;
|
||||
inst.color_override_rgba8 = chunk.color_override_rgba8;
|
||||
inst.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(inst.placement_transformation, chunk.transform,
|
||||
sizeof(inst.placement_transformation));
|
||||
std::memcpy(instance.placement_transformation, instance_record.transform,
|
||||
sizeof(instance.placement_transformation));
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
inst.transform[i] = static_cast<float>(chunk.transform[i]);
|
||||
instance.transform[i] = static_cast<float>(instance_record.transform[i]);
|
||||
}
|
||||
std::memcpy(inst.world_aabb_min, chunk.world_aabb_min, sizeof(inst.world_aabb_min));
|
||||
std::memcpy(inst.world_aabb_max, chunk.world_aabb_max, sizeof(inst.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(inst);
|
||||
sidecar_data_.instances.push_back(instance);
|
||||
}
|
||||
|
||||
SidecarData SidecarBuilder::finalize(const ModelGeoref& georef,
|
||||
@@ -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
|
||||
|
||||
|
||||
+142
-107
@@ -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]
|
||||
|
||||
@@ -59,50 +59,61 @@ static constexpr int kSidecarZstdLevel = 19;
|
||||
|
||||
// --- In-memory serialisation (a block is built in RAM, then compressed) ------
|
||||
template<typename T>
|
||||
static void appendVec(std::vector<std::uint8_t>& b, const std::vector<T>& v) {
|
||||
std::uint32_t n = static_cast<std::uint32_t>(v.size());
|
||||
const auto* np = reinterpret_cast<const std::uint8_t*>(&n);
|
||||
b.insert(b.end(), np, np + 4);
|
||||
if (n > 0) {
|
||||
const auto* p = reinterpret_cast<const std::uint8_t*>(v.data());
|
||||
b.insert(b.end(), p, p + std::size_t(sizeof(T)) * n);
|
||||
static void appendVec(std::vector<std::uint8_t>& buffer, const std::vector<T>& values) {
|
||||
std::uint32_t count = static_cast<std::uint32_t>(values.size());
|
||||
const auto* count_bytes = reinterpret_cast<const std::uint8_t*>(&count);
|
||||
buffer.insert(buffer.end(), count_bytes, count_bytes + 4);
|
||||
if (count > 0) {
|
||||
const auto* value_bytes = reinterpret_cast<const std::uint8_t*>(values.data());
|
||||
buffer.insert(buffer.end(), value_bytes, value_bytes + std::size_t(sizeof(T)) * count);
|
||||
}
|
||||
}
|
||||
static void appendBytes(std::vector<std::uint8_t>& b, const void* p, std::size_t n) {
|
||||
const auto* c = static_cast<const std::uint8_t*>(p);
|
||||
b.insert(b.end(), c, c + n);
|
||||
static void appendBytes(std::vector<std::uint8_t>& buffer, const void* data, std::size_t byte_count) {
|
||||
const auto* bytes = static_cast<const std::uint8_t*>(data);
|
||||
buffer.insert(buffer.end(), bytes, bytes + byte_count);
|
||||
}
|
||||
|
||||
// Pull one chunk's geometry out of the whole-model vertex/index arrays into the
|
||||
// chunk-LOCAL layout applyStreamedChunk expects: vertices of its meshes in chunk
|
||||
// order, then indices as LOD0 (per mesh) followed by LOD1 (per mesh).
|
||||
static void extractChunkGeometry(const SidecarData& d, const SidecarChunk& c,
|
||||
static void extractChunkGeometry(const SidecarData& sidecar_data, const SidecarChunk& sidecar_chunk,
|
||||
std::vector<std::uint8_t>& vbytes,
|
||||
std::vector<std::uint8_t>& ibytes) {
|
||||
vbytes.clear();
|
||||
ibytes.clear();
|
||||
const std::uint32_t end = c.first_mesh + c.mesh_count;
|
||||
for (std::uint32_t mi = c.first_mesh; mi < end && mi < d.meshes.size(); ++mi) {
|
||||
const MeshInfo& m = d.meshes[mi];
|
||||
const std::size_t voff = m.vbo_byte_offset;
|
||||
const std::size_t vn = std::size_t(m.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES;
|
||||
if (voff + vn <= d.vertices.size())
|
||||
vbytes.insert(vbytes.end(), d.vertices.begin() + voff,
|
||||
d.vertices.begin() + voff + vn);
|
||||
const std::uint32_t end = sidecar_chunk.first_mesh + sidecar_chunk.mesh_count;
|
||||
for (std::uint32_t mesh_index = sidecar_chunk.first_mesh;
|
||||
mesh_index < end && mesh_index < sidecar_data.meshes.size();
|
||||
++mesh_index) {
|
||||
const MeshInfo& mesh_info = sidecar_data.meshes[mesh_index];
|
||||
const std::size_t vertex_offset = mesh_info.vbo_byte_offset;
|
||||
const std::size_t vertex_byte_count =
|
||||
std::size_t(mesh_info.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES;
|
||||
if (vertex_offset + vertex_byte_count <= sidecar_data.vertices.size())
|
||||
vbytes.insert(vbytes.end(), sidecar_data.vertices.begin() + vertex_offset,
|
||||
sidecar_data.vertices.begin() + vertex_offset + vertex_byte_count);
|
||||
}
|
||||
auto appendIdx = [&](std::size_t first_u32, std::size_t count) {
|
||||
if (first_u32 + count > d.indices.size()) return;
|
||||
const auto* p = reinterpret_cast<const std::uint8_t*>(d.indices.data() + first_u32);
|
||||
ibytes.insert(ibytes.end(), p, p + count * sizeof(std::uint32_t));
|
||||
if (first_u32 + count > sidecar_data.indices.size()) return;
|
||||
const auto* index_bytes =
|
||||
reinterpret_cast<const std::uint8_t*>(sidecar_data.indices.data() + first_u32);
|
||||
ibytes.insert(ibytes.end(), index_bytes, index_bytes + count * sizeof(std::uint32_t));
|
||||
};
|
||||
for (std::uint32_t mi = c.first_mesh; mi < end && mi < d.meshes.size(); ++mi) {
|
||||
const MeshInfo& m = d.meshes[mi];
|
||||
if (m.index_count) appendIdx(m.ebo_byte_offset / sizeof(std::uint32_t), m.index_count);
|
||||
for (std::uint32_t mesh_index = sidecar_chunk.first_mesh;
|
||||
mesh_index < end && mesh_index < sidecar_data.meshes.size();
|
||||
++mesh_index) {
|
||||
const MeshInfo& mesh_info = sidecar_data.meshes[mesh_index];
|
||||
if (mesh_info.index_count) {
|
||||
appendIdx(mesh_info.ebo_byte_offset / sizeof(std::uint32_t), mesh_info.index_count);
|
||||
}
|
||||
}
|
||||
for (std::uint32_t mi = c.first_mesh; mi < end && mi < d.meshes.size(); ++mi) {
|
||||
const MeshInfo& m = d.meshes[mi];
|
||||
if (m.lod1_index_count)
|
||||
appendIdx(m.lod1_ebo_byte_offset / sizeof(std::uint32_t), m.lod1_index_count);
|
||||
for (std::uint32_t mesh_index = sidecar_chunk.first_mesh;
|
||||
mesh_index < end && mesh_index < sidecar_data.meshes.size();
|
||||
++mesh_index) {
|
||||
const MeshInfo& mesh_info = sidecar_data.meshes[mesh_index];
|
||||
if (mesh_info.lod1_index_count) {
|
||||
appendIdx(mesh_info.lod1_ebo_byte_offset / sizeof(std::uint32_t), mesh_info.lod1_index_count);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // !__EMSCRIPTEN__ (bake-only serialisation helpers)
|
||||
@@ -118,14 +129,14 @@ struct SidecarHeader {
|
||||
// foo.ifcdb -> foo.ifcview
|
||||
// foo (no ext) -> foo.ifcview
|
||||
static std::string sidecarPath(const std::string& ifc_path) {
|
||||
std::string p = ifc_path;
|
||||
while (!p.empty() && (p.back() == '/' || p.back() == '\\')) p.pop_back();
|
||||
auto slash = p.find_last_of("/\\");
|
||||
auto dot = p.find_last_of('.');
|
||||
std::string path = ifc_path;
|
||||
while (!path.empty() && (path.back() == '/' || path.back() == '\\')) path.pop_back();
|
||||
auto slash = path.find_last_of("/\\");
|
||||
auto dot = path.find_last_of('.');
|
||||
std::string stem = (dot != std::string::npos &&
|
||||
(slash == std::string::npos || dot > slash))
|
||||
? p.substr(0, dot)
|
||||
: p;
|
||||
? path.substr(0, dot)
|
||||
: path;
|
||||
return stem + ".ifcview";
|
||||
}
|
||||
|
||||
@@ -152,18 +163,18 @@ bool writeSidecar(const std::string& ifc_path, const SidecarData& data) {
|
||||
FILE* f = fopen(path.c_str(), "wb");
|
||||
if (!f) return false;
|
||||
|
||||
auto wr = [&](const void* p, std::size_t n) {
|
||||
return fwrite(p, 1, n, f) == n;
|
||||
auto write_bytes = [&](const void* data, std::size_t byte_count) {
|
||||
return fwrite(data, 1, byte_count, f) == byte_count;
|
||||
};
|
||||
auto wrU64 = [&](std::uint64_t v) { return wr(&v, sizeof(v)); };
|
||||
auto wrU64 = [&](std::uint64_t v) { return write_bytes(&v, sizeof(v)); };
|
||||
auto wrBlock = [&](const std::vector<std::uint8_t>& raw) -> bool {
|
||||
auto z = SidecarCompress::compress(raw.data(), raw.size(), kSidecarZstdLevel);
|
||||
if (raw.size() > 0 && z.empty()) return false; // compress failed
|
||||
return wrU64(z.size()) && wrU64(raw.size()) && (z.empty() || wr(z.data(), z.size()));
|
||||
return wrU64(z.size()) && wrU64(raw.size()) && (z.empty() || write_bytes(z.data(), z.size()));
|
||||
};
|
||||
|
||||
SidecarHeader hdr = { SIDECAR_MAGIC, SIDECAR_VERSION, SIDECAR_ENDIAN };
|
||||
if (!wr(&hdr, sizeof(hdr))) { fclose(f); return false; }
|
||||
if (!write_bytes(&hdr, sizeof(hdr))) { fclose(f); return false; }
|
||||
|
||||
// --- Geometry section: per-chunk zstd(vertex) + zstd(index) frames -------
|
||||
// Offsets in the chunk TOC are relative to the geometry section start, so
|
||||
@@ -174,19 +185,19 @@ bool writeSidecar(const std::string& ifc_path, const SidecarData& data) {
|
||||
|
||||
std::vector<SidecarChunk> chunks = data.chunks; // fill blob offsets below
|
||||
std::vector<std::uint8_t> vraw, iraw;
|
||||
for (auto& c : chunks) {
|
||||
extractChunkGeometry(data, c, vraw, iraw);
|
||||
for (auto& sidecar_chunk : chunks) {
|
||||
extractChunkGeometry(data, sidecar_chunk, vraw, iraw);
|
||||
auto vz = SidecarCompress::compress(vraw.data(), vraw.size(), kSidecarZstdLevel);
|
||||
auto iz = SidecarCompress::compress(iraw.data(), iraw.size(), kSidecarZstdLevel);
|
||||
if ((vraw.size() && vz.empty()) || (iraw.size() && iz.empty())) { fclose(f); return false; }
|
||||
c.v_comp_off = std::uint64_t(ftell(f) - geom_start);
|
||||
c.v_comp_size = vz.size();
|
||||
c.v_raw_size = vraw.size();
|
||||
if (!vz.empty() && !wr(vz.data(), vz.size())) { fclose(f); return false; }
|
||||
c.i_comp_off = std::uint64_t(ftell(f) - geom_start);
|
||||
c.i_comp_size = iz.size();
|
||||
c.i_raw_size = iraw.size();
|
||||
if (!iz.empty() && !wr(iz.data(), iz.size())) { fclose(f); return false; }
|
||||
sidecar_chunk.v_comp_off = std::uint64_t(ftell(f) - geom_start);
|
||||
sidecar_chunk.v_comp_size = vz.size();
|
||||
sidecar_chunk.v_raw_size = vraw.size();
|
||||
if (!vz.empty() && !write_bytes(vz.data(), vz.size())) { fclose(f); return false; }
|
||||
sidecar_chunk.i_comp_off = std::uint64_t(ftell(f) - geom_start);
|
||||
sidecar_chunk.i_comp_size = iz.size();
|
||||
sidecar_chunk.i_raw_size = iraw.size();
|
||||
if (!iz.empty() && !write_bytes(iz.data(), iz.size())) { fclose(f); return false; }
|
||||
}
|
||||
const long geom_end = ftell(f);
|
||||
if (geom_start < 0 || geom_end < 0) { fclose(f); return false; }
|
||||
@@ -194,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> crit;
|
||||
appendVec(crit, data.meshes);
|
||||
appendVec(crit, data.instances);
|
||||
appendBytes(crit, &data.has_coordinate_operation, 4);
|
||||
appendBytes(crit, data.coordinate_operation_meters, sizeof(double) * 16);
|
||||
appendBytes(crit, &data.project_length_to_meters, sizeof(double));
|
||||
appendBytes(crit, &data.map_unit_to_meters, sizeof(double));
|
||||
appendVec(crit, chunks);
|
||||
if (!wrBlock(crit)) { 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> def;
|
||||
appendVec(def, 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(def, &stbl_len, 4);
|
||||
appendBytes(def, data.string_table.data(), stbl_len);
|
||||
if (!wrBlock(def)) { 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;
|
||||
@@ -257,28 +268,30 @@ std::optional<SidecarData> readSidecar(const std::string& ifc_path) {
|
||||
if (hdr.magic != SIDECAR_MAGIC || hdr.version != SIDECAR_VERSION ||
|
||||
hdr.endian != SIDECAR_ENDIAN) return fail();
|
||||
|
||||
auto rd = [&](void* p, std::size_t k) { return fread(p, 1, k, f) == k; };
|
||||
auto rdU64 = [&](std::uint64_t& v) { return rd(&v, sizeof(v)); };
|
||||
auto read_bytes = [&](void* data, std::size_t byte_count) {
|
||||
return fread(data, 1, byte_count, f) == byte_count;
|
||||
};
|
||||
auto rdU64 = [&](std::uint64_t& v) { return read_bytes(&v, sizeof(v)); };
|
||||
|
||||
std::uint64_t geom_bytes = 0;
|
||||
if (!rdU64(geom_bytes)) return fail();
|
||||
std::vector<std::uint8_t> geom(static_cast<std::size_t>(geom_bytes));
|
||||
if (geom_bytes && !rd(geom.data(), geom.size())) return fail();
|
||||
if (geom_bytes && !read_bytes(geom.data(), geom.size())) return fail();
|
||||
|
||||
auto readBlock = [&](std::vector<std::uint8_t>& out) -> bool {
|
||||
std::uint64_t comp = 0, raw = 0;
|
||||
if (!rdU64(comp) || !rdU64(raw)) return false;
|
||||
std::vector<std::uint8_t> z(static_cast<std::size_t>(comp));
|
||||
if (comp && !rd(z.data(), z.size())) return false;
|
||||
if (comp && !read_bytes(z.data(), z.size())) return false;
|
||||
out.assign(std::size_t(raw), 0);
|
||||
return SidecarCompress::decompress(z.data(), z.size(), out.data(), out.size());
|
||||
};
|
||||
std::vector<std::uint8_t> crit, def;
|
||||
if (!readBlock(crit) || !readBlock(def)) 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{ crit.data(), crit.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;
|
||||
@@ -287,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{ def.data(), def.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;
|
||||
@@ -296,46 +309,68 @@ std::optional<SidecarData> readSidecar(const std::string& ifc_path) {
|
||||
|
||||
// Reconstruct the whole-model vertex/index arrays from the per-chunk blobs.
|
||||
std::size_t vsize = 0, isize = 0;
|
||||
for (const auto& m : data.meshes) {
|
||||
for (const auto& mesh_info : data.meshes) {
|
||||
vsize = std::max<std::size_t>(vsize,
|
||||
std::size_t(m.vbo_byte_offset) + std::size_t(m.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES);
|
||||
isize = std::max<std::size_t>(isize, m.ebo_byte_offset / sizeof(std::uint32_t) + m.index_count);
|
||||
if (m.lod1_index_count)
|
||||
isize = std::max<std::size_t>(isize, m.lod1_ebo_byte_offset / sizeof(std::uint32_t) + m.lod1_index_count);
|
||||
std::size_t(mesh_info.vbo_byte_offset) +
|
||||
std::size_t(mesh_info.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES);
|
||||
isize = std::max<std::size_t>(
|
||||
isize, mesh_info.ebo_byte_offset / sizeof(std::uint32_t) + mesh_info.index_count);
|
||||
if (mesh_info.lod1_index_count) {
|
||||
isize = std::max<std::size_t>(
|
||||
isize,
|
||||
mesh_info.lod1_ebo_byte_offset / sizeof(std::uint32_t) + mesh_info.lod1_index_count);
|
||||
}
|
||||
}
|
||||
data.vertices.assign(vsize, 0);
|
||||
data.indices.assign(isize, 0);
|
||||
for (const auto& c : data.chunks) {
|
||||
if (c.v_comp_off + c.v_comp_size > geom.size() ||
|
||||
c.i_comp_off + c.i_comp_size > geom.size()) return std::nullopt;
|
||||
std::vector<std::uint8_t> vraw(static_cast<std::size_t>(c.v_raw_size));
|
||||
std::vector<std::uint8_t> iraw(static_cast<std::size_t>(c.i_raw_size));
|
||||
if (!SidecarCompress::decompress(geom.data() + c.v_comp_off, c.v_comp_size, vraw.data(), vraw.size()) ||
|
||||
!SidecarCompress::decompress(geom.data() + c.i_comp_off, c.i_comp_size, iraw.data(), iraw.size()))
|
||||
for (const auto& sidecar_chunk : data.chunks) {
|
||||
if (sidecar_chunk.v_comp_off + sidecar_chunk.v_comp_size > geom.size() ||
|
||||
sidecar_chunk.i_comp_off + sidecar_chunk.i_comp_size > geom.size()) return std::nullopt;
|
||||
std::vector<std::uint8_t> vraw(static_cast<std::size_t>(sidecar_chunk.v_raw_size));
|
||||
std::vector<std::uint8_t> iraw(static_cast<std::size_t>(sidecar_chunk.i_raw_size));
|
||||
if (!SidecarCompress::decompress(
|
||||
geom.data() + sidecar_chunk.v_comp_off, sidecar_chunk.v_comp_size, vraw.data(), vraw.size()) ||
|
||||
!SidecarCompress::decompress(
|
||||
geom.data() + sidecar_chunk.i_comp_off, sidecar_chunk.i_comp_size, iraw.data(), iraw.size()))
|
||||
return std::nullopt;
|
||||
const auto* iu = reinterpret_cast<const std::uint32_t*>(iraw.data());
|
||||
std::size_t vcur = 0, icur = 0;
|
||||
const std::uint32_t end = c.first_mesh + c.mesh_count;
|
||||
for (std::uint32_t mi = c.first_mesh; mi < end && mi < data.meshes.size(); ++mi) {
|
||||
const MeshInfo& m = data.meshes[mi];
|
||||
const std::size_t vn = std::size_t(m.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES;
|
||||
if (vcur + vn <= vraw.size() && m.vbo_byte_offset + vn <= data.vertices.size())
|
||||
std::memcpy(&data.vertices[m.vbo_byte_offset], vraw.data() + vcur, vn);
|
||||
vcur += vn;
|
||||
const std::uint32_t end = sidecar_chunk.first_mesh + sidecar_chunk.mesh_count;
|
||||
for (std::uint32_t mesh_index = sidecar_chunk.first_mesh;
|
||||
mesh_index < end && mesh_index < data.meshes.size();
|
||||
++mesh_index) {
|
||||
const MeshInfo& mesh_info = data.meshes[mesh_index];
|
||||
const std::size_t vertex_byte_count =
|
||||
std::size_t(mesh_info.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES;
|
||||
if (vcur + vertex_byte_count <= vraw.size() &&
|
||||
mesh_info.vbo_byte_offset + vertex_byte_count <= data.vertices.size()) {
|
||||
std::memcpy(&data.vertices[mesh_info.vbo_byte_offset], vraw.data() + vcur, vertex_byte_count);
|
||||
}
|
||||
vcur += vertex_byte_count;
|
||||
}
|
||||
for (std::uint32_t mi = c.first_mesh; mi < end && mi < data.meshes.size(); ++mi) {
|
||||
const MeshInfo& m = data.meshes[mi];
|
||||
if (!m.index_count) continue;
|
||||
if (icur + m.index_count <= iraw.size() / 4)
|
||||
std::memcpy(&data.indices[m.ebo_byte_offset / sizeof(std::uint32_t)], iu + icur, m.index_count * 4);
|
||||
icur += m.index_count;
|
||||
for (std::uint32_t mesh_index = sidecar_chunk.first_mesh;
|
||||
mesh_index < end && mesh_index < data.meshes.size();
|
||||
++mesh_index) {
|
||||
const MeshInfo& mesh_info = data.meshes[mesh_index];
|
||||
if (!mesh_info.index_count) continue;
|
||||
if (icur + mesh_info.index_count <= iraw.size() / 4) {
|
||||
std::memcpy(&data.indices[mesh_info.ebo_byte_offset / sizeof(std::uint32_t)],
|
||||
iu + icur,
|
||||
mesh_info.index_count * 4);
|
||||
}
|
||||
icur += mesh_info.index_count;
|
||||
}
|
||||
for (std::uint32_t mi = c.first_mesh; mi < end && mi < data.meshes.size(); ++mi) {
|
||||
const MeshInfo& m = data.meshes[mi];
|
||||
if (!m.lod1_index_count) continue;
|
||||
if (icur + m.lod1_index_count <= iraw.size() / 4)
|
||||
std::memcpy(&data.indices[m.lod1_ebo_byte_offset / sizeof(std::uint32_t)], iu + icur, m.lod1_index_count * 4);
|
||||
icur += m.lod1_index_count;
|
||||
for (std::uint32_t mesh_index = sidecar_chunk.first_mesh;
|
||||
mesh_index < end && mesh_index < data.meshes.size();
|
||||
++mesh_index) {
|
||||
const MeshInfo& mesh_info = data.meshes[mesh_index];
|
||||
if (!mesh_info.lod1_index_count) continue;
|
||||
if (icur + mesh_info.lod1_index_count <= iraw.size() / 4) {
|
||||
std::memcpy(&data.indices[mesh_info.lod1_ebo_byte_offset / sizeof(std::uint32_t)],
|
||||
iu + icur,
|
||||
mesh_info.lod1_index_count * 4);
|
||||
}
|
||||
icur += mesh_info.lod1_index_count;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,37 +26,42 @@
|
||||
#include <vector>
|
||||
|
||||
void reorderSidecarByMorton(SidecarData& sd) {
|
||||
const std::size_t n = sd.meshes.size();
|
||||
if (n < 2) return;
|
||||
const std::size_t mesh_count = sd.meshes.size();
|
||||
if (mesh_count < 2) return;
|
||||
|
||||
// Per-mesh centroid + instance count, exactly as the loader computes them
|
||||
// before chunk planning (average of instance world-AABB centres).
|
||||
std::vector<float> cx(n, 0.0f), cy(n, 0.0f), cz(n, 0.0f);
|
||||
std::vector<std::uint32_t> cnt(n, 0);
|
||||
std::vector<float> mesh_centroid_x(mesh_count, 0.0f),
|
||||
mesh_centroid_y(mesh_count, 0.0f),
|
||||
mesh_centroid_z(mesh_count, 0.0f);
|
||||
std::vector<std::uint32_t> mesh_instance_count(mesh_count, 0);
|
||||
for (const auto& inst : sd.instances) {
|
||||
if (inst.mesh_id >= n) continue;
|
||||
cx[inst.mesh_id] += 0.5f * (inst.world_aabb_min[0] + inst.world_aabb_max[0]);
|
||||
cy[inst.mesh_id] += 0.5f * (inst.world_aabb_min[1] + inst.world_aabb_max[1]);
|
||||
cz[inst.mesh_id] += 0.5f * (inst.world_aabb_min[2] + inst.world_aabb_max[2]);
|
||||
++cnt[inst.mesh_id];
|
||||
if (inst.mesh_id >= mesh_count) continue;
|
||||
mesh_centroid_x[inst.mesh_id] += 0.5f * (inst.world_aabb_min[0] + inst.world_aabb_max[0]);
|
||||
mesh_centroid_y[inst.mesh_id] += 0.5f * (inst.world_aabb_min[1] + inst.world_aabb_max[1]);
|
||||
mesh_centroid_z[inst.mesh_id] += 0.5f * (inst.world_aabb_min[2] + inst.world_aabb_max[2]);
|
||||
++mesh_instance_count[inst.mesh_id];
|
||||
}
|
||||
for (std::size_t i = 0; i < n; ++i) {
|
||||
if (cnt[i] > 0) {
|
||||
const float inv = 1.0f / float(cnt[i]);
|
||||
cx[i] *= inv; cy[i] *= inv; cz[i] *= inv;
|
||||
for (std::size_t i = 0; i < mesh_count; ++i) {
|
||||
if (mesh_instance_count[i] > 0) {
|
||||
const float inv = 1.0f / float(mesh_instance_count[i]);
|
||||
mesh_centroid_x[i] *= inv;
|
||||
mesh_centroid_y[i] *= inv;
|
||||
mesh_centroid_z[i] *= inv;
|
||||
}
|
||||
}
|
||||
|
||||
// order[new_id] = old mesh id, in the loader's Morton order.
|
||||
const std::vector<std::uint32_t> order =
|
||||
ChunkPlanner::sortMeshIdsByMorton(n, cx, cy, cz, cnt);
|
||||
ChunkPlanner::sortMeshIdsByMorton(
|
||||
mesh_count, mesh_centroid_x, mesh_centroid_y, mesh_centroid_z, mesh_instance_count);
|
||||
|
||||
// Greedy-pack the sorted order into chunks (the same plan the loader used
|
||||
// to derive). Each chunk is a CONSECUTIVE run of `order`, so once we lay
|
||||
// meshes out in `order` the chunk is a contiguous mesh range — recorded in
|
||||
// the TOC as {first_mesh, mesh_count}.
|
||||
std::vector<std::uint32_t> mesh_vertex_count(n, 0);
|
||||
for (std::size_t i = 0; i < n; ++i) mesh_vertex_count[i] = sd.meshes[i].vertex_count;
|
||||
std::vector<std::uint32_t> mesh_vertex_count(mesh_count, 0);
|
||||
for (std::size_t i = 0; i < mesh_count; ++i) mesh_vertex_count[i] = sd.meshes[i].vertex_count;
|
||||
const std::vector<std::vector<std::uint32_t>> packed = ChunkPlanner::greedyPackChunks(
|
||||
order, mesh_vertex_count, INSTANCED_VERTEX_STRIDE_BYTES,
|
||||
WGPU_CHUNK_VERTEX_BYTES_LIMIT);
|
||||
@@ -74,58 +79,61 @@ void reorderSidecarByMorton(SidecarData& sd) {
|
||||
// MeshInfo.first_instance: the baker leaves it 0 for every mesh and stores
|
||||
// instances ungrouped, so first_instance describes nothing. Grouping here
|
||||
// by mesh_id both reorders instances correctly AND fixes first_instance.
|
||||
std::vector<std::vector<std::uint32_t>> insts_by_mesh(n);
|
||||
for (std::uint32_t ii = 0; ii < sd.instances.size(); ++ii) {
|
||||
const std::uint32_t mid = sd.instances[ii].mesh_id;
|
||||
if (mid < n) insts_by_mesh[mid].push_back(ii);
|
||||
std::vector<std::vector<std::uint32_t>> insts_by_mesh(mesh_count);
|
||||
for (std::uint32_t instance_index = 0; instance_index < sd.instances.size(); ++instance_index) {
|
||||
const std::uint32_t mesh_id = sd.instances[instance_index].mesh_id;
|
||||
if (mesh_id < mesh_count) insts_by_mesh[mesh_id].push_back(instance_index);
|
||||
}
|
||||
|
||||
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(n);
|
||||
std::vector<InstanceCpu> new_instances; new_instances.reserve(sd.instances.size());
|
||||
std::vector<MeshInfo> new_meshes(mesh_count);
|
||||
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.
|
||||
for (std::uint32_t ni = 0; ni < n; ++ni) {
|
||||
const std::uint32_t old = order[ni];
|
||||
const MeshInfo& om = sd.meshes[old];
|
||||
MeshInfo nm = om; // carries AABB; offsets/instance fields overwritten below
|
||||
for (std::uint32_t new_mesh_index = 0; new_mesh_index < mesh_count; ++new_mesh_index) {
|
||||
const std::uint32_t old = order[new_mesh_index];
|
||||
const MeshInfo& old_mesh_info = sd.meshes[old];
|
||||
MeshInfo new_mesh_info = old_mesh_info; // carries AABB; offsets/instance fields overwritten below
|
||||
|
||||
nm.vbo_byte_offset = std::uint32_t(new_vertices.size());
|
||||
const std::size_t vbytes = std::size_t(om.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES;
|
||||
new_mesh_info.vbo_byte_offset = std::uint32_t(new_vertices.size());
|
||||
const std::size_t vbytes = std::size_t(old_mesh_info.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES;
|
||||
new_vertices.insert(new_vertices.end(),
|
||||
sd.vertices.begin() + om.vbo_byte_offset,
|
||||
sd.vertices.begin() + om.vbo_byte_offset + vbytes);
|
||||
sd.vertices.begin() + old_mesh_info.vbo_byte_offset,
|
||||
sd.vertices.begin() + old_mesh_info.vbo_byte_offset + vbytes);
|
||||
|
||||
nm.ebo_byte_offset = std::uint32_t(new_indices.size() * sizeof(std::uint32_t));
|
||||
const std::size_t i0 = om.ebo_byte_offset / sizeof(std::uint32_t);
|
||||
new_mesh_info.ebo_byte_offset = std::uint32_t(new_indices.size() * sizeof(std::uint32_t));
|
||||
const std::size_t i0 = old_mesh_info.ebo_byte_offset / sizeof(std::uint32_t);
|
||||
new_indices.insert(new_indices.end(),
|
||||
sd.indices.begin() + i0,
|
||||
sd.indices.begin() + i0 + om.index_count);
|
||||
sd.indices.begin() + i0 + old_mesh_info.index_count);
|
||||
|
||||
nm.first_instance = std::uint32_t(new_instances.size());
|
||||
nm.instance_count = std::uint32_t(insts_by_mesh[old].size());
|
||||
for (std::uint32_t ii : insts_by_mesh[old]) {
|
||||
InstanceCpu ic = sd.instances[ii];
|
||||
ic.mesh_id = ni;
|
||||
new_instances.push_back(ic);
|
||||
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]) {
|
||||
InstanceInfo instance = sd.instances[instance_index];
|
||||
instance.mesh_id = new_mesh_index;
|
||||
new_instances.push_back(instance);
|
||||
}
|
||||
|
||||
new_meshes[ni] = nm;
|
||||
new_meshes[new_mesh_index] = new_mesh_info;
|
||||
}
|
||||
|
||||
// Pass B: LOD1 indices appended after all LOD0 (same global layout as the
|
||||
// baker), in the new order, so a chunk's LOD1 slice is contiguous too.
|
||||
for (std::uint32_t ni = 0; ni < n; ++ni) {
|
||||
const MeshInfo& om = sd.meshes[order[ni]];
|
||||
MeshInfo& nm = new_meshes[ni];
|
||||
if (om.lod1_index_count == 0) { nm.lod1_ebo_byte_offset = 0; continue; }
|
||||
nm.lod1_ebo_byte_offset = std::uint32_t(new_indices.size() * sizeof(std::uint32_t));
|
||||
const std::size_t l0 = om.lod1_ebo_byte_offset / sizeof(std::uint32_t);
|
||||
for (std::uint32_t new_mesh_index = 0; new_mesh_index < mesh_count; ++new_mesh_index) {
|
||||
const MeshInfo& old_mesh_info = sd.meshes[order[new_mesh_index]];
|
||||
MeshInfo& new_mesh_info = new_meshes[new_mesh_index];
|
||||
if (old_mesh_info.lod1_index_count == 0) {
|
||||
new_mesh_info.lod1_ebo_byte_offset = 0;
|
||||
continue;
|
||||
}
|
||||
new_mesh_info.lod1_ebo_byte_offset = std::uint32_t(new_indices.size() * sizeof(std::uint32_t));
|
||||
const std::size_t l0 = old_mesh_info.lod1_ebo_byte_offset / sizeof(std::uint32_t);
|
||||
new_indices.insert(new_indices.end(),
|
||||
sd.indices.begin() + l0,
|
||||
sd.indices.begin() + l0 + om.lod1_index_count);
|
||||
sd.indices.begin() + l0 + old_mesh_info.lod1_index_count);
|
||||
}
|
||||
|
||||
sd.vertices = std::move(new_vertices);
|
||||
|
||||
@@ -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
|
||||
@@ -52,25 +52,25 @@ struct SidecarHeaderRaw {
|
||||
// walks the metadata tail through one of these so a truncated buffer fails
|
||||
// cleanly (return false) instead of reading out of bounds.
|
||||
struct BufCursor {
|
||||
const uint8_t* p;
|
||||
size_t remaining;
|
||||
const uint8_t* cursor;
|
||||
size_t remaining_bytes;
|
||||
|
||||
bool take(void* dst, size_t bytes) {
|
||||
if (bytes > remaining) return false;
|
||||
std::memcpy(dst, p, bytes);
|
||||
p += bytes;
|
||||
remaining -= bytes;
|
||||
if (bytes > remaining_bytes) return false;
|
||||
std::memcpy(dst, cursor, bytes);
|
||||
cursor += bytes;
|
||||
remaining_bytes -= bytes;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Read a uint32 length prefix followed by length*sizeof(T) elements.
|
||||
template<typename T>
|
||||
bool takeVec(std::vector<T>& v) {
|
||||
bool takeVec(std::vector<T>& values) {
|
||||
uint32_t n;
|
||||
if (!take(&n, 4)) return false;
|
||||
if (uint64_t(n) * sizeof(T) > remaining) return false;
|
||||
v.resize(n);
|
||||
if (n > 0 && !take(v.data(), size_t(n) * sizeof(T))) return false;
|
||||
if (uint64_t(n) * sizeof(T) > remaining_bytes) return false;
|
||||
values.resize(n);
|
||||
if (n > 0 && !take(values.data(), size_t(n) * sizeof(T))) return false;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -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,13 +113,13 @@ 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;
|
||||
if (!c.take(&stbl_len, 4)) return false;
|
||||
if (stbl_len > c.remaining) return false;
|
||||
if (stbl_len > c.remaining_bytes) return false;
|
||||
out.string_table.resize(stbl_len);
|
||||
if (stbl_len > 0 && !c.take(out.string_table.data(), stbl_len)) return false;
|
||||
return true;
|
||||
@@ -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
|
||||
|
||||
@@ -26,23 +26,23 @@ StreamingThread::~StreamingThread() {
|
||||
}
|
||||
|
||||
void StreamingThread::start() {
|
||||
std::unique_lock lk(mu_);
|
||||
std::unique_lock lock(mu_);
|
||||
if (running_) return;
|
||||
shutdown_ = false;
|
||||
running_ = true;
|
||||
lk.unlock();
|
||||
lock.unlock();
|
||||
worker_ = std::thread(&StreamingThread::workerLoop, this);
|
||||
}
|
||||
|
||||
void StreamingThread::stop() {
|
||||
{
|
||||
std::unique_lock lk(mu_);
|
||||
std::unique_lock lock(mu_);
|
||||
if (!running_) return;
|
||||
shutdown_ = true;
|
||||
}
|
||||
cv_.notify_all();
|
||||
if (worker_.joinable()) worker_.join();
|
||||
std::unique_lock lk(mu_);
|
||||
std::unique_lock lock(mu_);
|
||||
running_ = false;
|
||||
requests_.clear();
|
||||
results_.clear();
|
||||
@@ -50,7 +50,7 @@ void StreamingThread::stop() {
|
||||
|
||||
bool StreamingThread::enqueue(Request req) {
|
||||
{
|
||||
std::unique_lock lk(mu_);
|
||||
std::unique_lock lock(mu_);
|
||||
if (!running_ || shutdown_) return false;
|
||||
requests_.push_back(std::move(req));
|
||||
}
|
||||
@@ -59,20 +59,20 @@ bool StreamingThread::enqueue(Request req) {
|
||||
}
|
||||
|
||||
std::vector<StreamingThread::Result> StreamingThread::drainResults() {
|
||||
std::vector<Result> out;
|
||||
std::vector<Result> results;
|
||||
{
|
||||
std::unique_lock lk(mu_);
|
||||
out.reserve(results_.size());
|
||||
std::unique_lock lock(mu_);
|
||||
results.reserve(results_.size());
|
||||
while (!results_.empty()) {
|
||||
out.push_back(std::move(results_.front()));
|
||||
results.push_back(std::move(results_.front()));
|
||||
results_.pop_front();
|
||||
}
|
||||
}
|
||||
return out;
|
||||
return results;
|
||||
}
|
||||
|
||||
std::size_t StreamingThread::inFlightApprox() const {
|
||||
std::unique_lock lk(mu_);
|
||||
std::unique_lock lock(mu_);
|
||||
return requests_.size() + (in_progress_ ? 1u : 0u);
|
||||
}
|
||||
|
||||
@@ -80,8 +80,8 @@ void StreamingThread::workerLoop() {
|
||||
for (;;) {
|
||||
Request req;
|
||||
{
|
||||
std::unique_lock lk(mu_);
|
||||
cv_.wait(lk, [this]() { return shutdown_ || !requests_.empty(); });
|
||||
std::unique_lock lock(mu_);
|
||||
cv_.wait(lock, [this]() { return shutdown_ || !requests_.empty(); });
|
||||
if (shutdown_ && requests_.empty()) return;
|
||||
req = std::move(requests_.front());
|
||||
requests_.pop_front();
|
||||
@@ -94,18 +94,18 @@ void StreamingThread::workerLoop() {
|
||||
// us. The vbytes / idx buffers are allocated here on the worker
|
||||
// thread — they cross back to the main thread when the result
|
||||
// is drained and applied (pool.alloc + queueWriteBuffer).
|
||||
Result res;
|
||||
res.model_id = req.model_id;
|
||||
res.chunk_idx = req.chunk_idx;
|
||||
res.success = readChunkGeometryCompressed(
|
||||
Result result;
|
||||
result.model_id = req.model_id;
|
||||
result.chunk_idx = req.chunk_idx;
|
||||
result.success = readChunkGeometryCompressed(
|
||||
req.file_path, req.geometry_section_offset,
|
||||
req.v_comp_off, req.v_comp_size, req.v_raw_size,
|
||||
req.i_comp_off, req.i_comp_size, req.i_raw_size,
|
||||
res.vbytes, res.idx);
|
||||
result.vbytes, result.idx);
|
||||
|
||||
{
|
||||
std::unique_lock lk(mu_);
|
||||
results_.push_back(std::move(res));
|
||||
std::unique_lock lock(mu_);
|
||||
results_.push_back(std::move(result));
|
||||
in_progress_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+688
-313
File diff suppressed because it is too large
Load Diff
+152
-12
@@ -50,6 +50,7 @@
|
||||
#include "InstanceCompose.h"
|
||||
#include "InstancedGeometry.h"
|
||||
#include "ModelGpuData.h"
|
||||
#include "SectionGizmoRenderer.h"
|
||||
#include "SectionPlane.h"
|
||||
#include "SelectionState.h"
|
||||
#include "SidecarCache.h"
|
||||
@@ -110,7 +111,7 @@ public:
|
||||
// from the mesh-local one. Used by the per-model recompose path
|
||||
// after any of the four federation matrices change. Pure scene
|
||||
// math — no GPU touch.
|
||||
void composeInstanceFromPlacement(InstanceCpu& inst,
|
||||
void composeInstanceFromPlacement(InstanceInfo& inst,
|
||||
const ModelGpuData& m) const;
|
||||
|
||||
// Cross-model object_id lookup. Delegates to
|
||||
@@ -192,6 +193,30 @@ public:
|
||||
enum class StandardView { Front, Back, Left, Right, Top, Bottom };
|
||||
void setStandardView(StandardView view);
|
||||
|
||||
// ---- Navigation mouse bindings (shared, preset-driven) ------------------
|
||||
//
|
||||
// Which mouse button (+ modifier) orbits / pans / selects. Owned by the core
|
||||
// as pure data so BOTH hosts and ALL presets share one source of truth — the
|
||||
// desktop maps these to Qt::MouseButton, the web to DOM button codes. Select
|
||||
// is preset-driven too (not hardcoded to LMB) so a "web" preset can move it
|
||||
// to RMB. Marquee box-select uses the same button as select (drag vs click).
|
||||
enum class MouseBtn { Left, Middle, Right };
|
||||
// Plain (not "None": X11 #defines None to 0L, which would corrupt the token).
|
||||
enum class NavMod { Plain, Shift, Ctrl, Alt };
|
||||
struct NavBindings {
|
||||
MouseBtn orbit; NavMod orbit_mod;
|
||||
MouseBtn pan; NavMod pan_mod;
|
||||
MouseBtn select; NavMod select_mod;
|
||||
};
|
||||
// name: "blender" (default) | "rhino" | "revit" | "web". Unknown → blender.
|
||||
// blender orbit MMB, pan Shift+MMB, select LMB
|
||||
// rhino orbit RMB, pan Shift+RMB, select LMB
|
||||
// revit orbit Shift+MMB, pan MMB, select LMB
|
||||
// web orbit LMB, pan MMB, select RMB (LMB stays free to
|
||||
// orbit-drag; RMB click-selects / drag-marquees, no ambiguity)
|
||||
void setNavPreset(const char* name);
|
||||
const NavBindings& navBindings() const { return nav_bindings_; }
|
||||
|
||||
// Frame the current selection: union the selected objects' world AABBs and
|
||||
// fit the camera to them (same 1.30 padding as the desktop "F" hotkey).
|
||||
// No-op with an empty selection or no resolvable AABBs; returns whether it
|
||||
@@ -401,16 +426,16 @@ public:
|
||||
// `source_label` is a log/identity tag.
|
||||
void loadSidecarMetadataWeb(int source_id, std::string source_label);
|
||||
|
||||
// On-demand fetch of the v15 deferred property block (element tree + string
|
||||
// On-demand fetch of the v15 element metadata block (elements + string
|
||||
// table) for a web-streamed model — what a UI (object tree / selected-name
|
||||
// / search) needs, fetched only when asked so first paint never waits on
|
||||
// it. Populates ModelGpuData.elements/string_table; fires done(ok). At most
|
||||
// one fetch per model.
|
||||
void loadDeferredMetadataWeb(std::uint32_t model_id,
|
||||
std::function<void(bool)> done = {});
|
||||
void loadElementMetadataWeb(std::uint32_t model_id,
|
||||
std::function<void(bool)> done = {});
|
||||
|
||||
// Demo consumer of the deferred fetch: on pick, ensure the owning model's
|
||||
// property block is loaded (loadDeferredMetadataWeb — fetched once, on
|
||||
// Demo consumer of the element metadata fetch: on pick, ensure the owning model's
|
||||
// property block is loaded (loadElementMetadataWeb — fetched once, on
|
||||
// demand), then log the picked object's IFC GUID. The first pick triggers
|
||||
// the network fetch; later picks reuse the cached element table.
|
||||
void logSelectedObjectGuidWeb(std::uint32_t object_id);
|
||||
@@ -447,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) -------------------------
|
||||
@@ -500,6 +525,22 @@ public:
|
||||
// Drop every section plane. No-op when none are active.
|
||||
void clearSectionPlanes();
|
||||
|
||||
// Number of active section planes (0..kMaxSectionPlanes).
|
||||
int sectionPlaneCount() const { return int(section_planes_.size()); }
|
||||
|
||||
// ---- Section gizmo interaction (shared desktop + web) -------------------
|
||||
//
|
||||
// All coords are LOGICAL (CSS) pixels; the core derives the logical viewport
|
||||
// from the host. hitTestSectionGizmo returns the plane index whose gizmo
|
||||
// arrow is under (x,y), or -1. The drag trio slides a plane along its normal:
|
||||
// begin captures the plane origin + press point, update reprojects and moves
|
||||
// it, end finishes.
|
||||
int hitTestSectionGizmo(int x, int y);
|
||||
bool beginSectionDrag(int gizmo_index, int mouse_x, int mouse_y);
|
||||
void updateSectionDrag(int mouse_x, int mouse_y);
|
||||
void endSectionDrag() { section_drag_active_ = false; }
|
||||
bool sectionDragActive() const { return section_drag_active_; }
|
||||
|
||||
// ---- Render loop (#84-x) ----------------------------------------------
|
||||
//
|
||||
// Encode one frame: acquire the swapchain texture, run cull (parallel
|
||||
@@ -608,6 +649,41 @@ public:
|
||||
// async (pickObjectAtAsync) readbacks. Caller validates bounds/attachments.
|
||||
void encodePickReadbackToStaging(int x_pixels, int y_pixels, bool want_normal);
|
||||
|
||||
// Encode the pick pass + copy the (x,y,w,h) object_id sub-rect into
|
||||
// box_pick_staging_buffer_ and submit. Clamps the rect (x/y/w/h in-out) and
|
||||
// reports the padded bytes-per-row + total mapped size. Shared by the sync
|
||||
// picksInRect and async picksInRectAsync — they differ only in the map.
|
||||
// False if nothing is pickable or the rect is empty.
|
||||
bool encodeBoxPickToStaging(int& x, int& y, int& w, int& h,
|
||||
std::uint64_t& padded_bpr_out,
|
||||
std::uint64_t& needed_bytes_out);
|
||||
// Read the (already-mapped) box-pick staging buffer → unique non-zero ids in
|
||||
// the w×h rect (rows padded to padded_bpr). Unmaps before returning.
|
||||
std::vector<std::uint32_t> collectMappedBoxPickIds(std::uint64_t padded_bpr,
|
||||
int w, int h,
|
||||
std::uint64_t needed_bytes);
|
||||
|
||||
// CPU half of pickSurfaceAt: cast the pixel's world ray against every
|
||||
// instance carrying `object_id`, returning the closest hit's world pos,
|
||||
// normal (mrt_normal if non-degenerate, else the AABB-face normal), and the
|
||||
// instance bounding-sphere radius. Shared by the sync pickSurfaceAt and the
|
||||
// async pickSurfaceAtAsync. False if no instance is hit.
|
||||
bool raycastSurfaceForObject(std::uint32_t object_id, int x_pixels, int y_pixels,
|
||||
const Eigen::Vector3f& mrt_normal,
|
||||
Eigen::Vector3f& world_pos_out,
|
||||
Eigen::Vector3f& world_normal_out,
|
||||
float& aabb_radius_out);
|
||||
// Decode the RGBA16F pick-normal from the (already-mapped) normal staging
|
||||
// buffer into a unit world normal; unmaps. False if degenerate. Shared by
|
||||
// the sync pickObjectAt and the async pickSurfaceAtAsync.
|
||||
bool decodeMappedPickNormal(Eigen::Vector3f& out);
|
||||
// Logical (CSS-px) viewport size from the host framebuffer / DPR, for the
|
||||
// section-gizmo hit-test + drag (which work in logical pixels).
|
||||
void sectionLogicalViewport(int& w, int& h) const;
|
||||
// Decode the RGBA32F exact world position from the (already-mapped) position
|
||||
// staging buffer; unmaps. False if the texel was a miss (w == 0).
|
||||
bool decodeMappedPickPosition(Eigen::Vector3f& out);
|
||||
|
||||
// Tear down every pick-owned wgpu resource (pipeline + MRTs +
|
||||
// staging buffers). Called from shutdown() before device_ dies.
|
||||
void releasePickResources();
|
||||
@@ -624,6 +700,11 @@ public:
|
||||
// Marks selection_ dirty for the next render's flush.
|
||||
void applyPickToSelection(std::uint32_t object_id, bool add, bool remove);
|
||||
|
||||
// Apply a marquee box-pick result to the selection: plain = replace with
|
||||
// `ids`, add = union, remove = subtract. Schedules a frame.
|
||||
void applyMarqueeToSelection(const std::vector<std::uint32_t>& ids,
|
||||
bool add, bool remove);
|
||||
|
||||
// Visibility + X-ray, shared by desktop (H / Shift+H / Alt+H / Alt+X) and
|
||||
// web. Hidden objects are skipped by the cull and xray_alpha_cap_ is read
|
||||
// by the frame uniform, both per frame — so each call just mutates state and
|
||||
@@ -649,9 +730,18 @@ public:
|
||||
|
||||
// Marquee box select: encode the pick pass, copy the (x, y, w, h)
|
||||
// sub-rect of the object_id MRT back, return the set of unique
|
||||
// non-zero ids. Synchronous (rare interaction).
|
||||
// non-zero ids. Synchronous (rare interaction) — desktop only path.
|
||||
std::vector<std::uint32_t> picksInRect(int x, int y, int w, int h);
|
||||
|
||||
#if defined(__EMSCRIPTEN__)
|
||||
// Async marquee box select for web (the sync spin-map would hang the JS
|
||||
// loop). Same pick pass + rect copy as picksInRect, mapped via a spontaneous
|
||||
// callback that delivers the unique non-zero ids to `cb`. One in flight at a
|
||||
// time (a box-pick issued while another is mapping is dropped → cb({})).
|
||||
void picksInRectAsync(int x, int y, int w, int h,
|
||||
std::function<void(std::vector<std::uint32_t>)> cb);
|
||||
#endif
|
||||
|
||||
// Run pickObjectAt + raycast against every instance carrying the
|
||||
// hit object_id, then return the closest hit's world position,
|
||||
// world normal, and (optionally) the bounding-sphere radius. The
|
||||
@@ -663,8 +753,24 @@ public:
|
||||
Eigen::Vector3f& world_normal_out,
|
||||
float* aabb_radius_out = nullptr);
|
||||
|
||||
#if defined(__EMSCRIPTEN__)
|
||||
// Async surface pick for web (drives the section tool). Reuses the async
|
||||
// object pick (no new GPU readback), then runs the same CPU ray-AABB cast as
|
||||
// pickSurfaceAt. On web the normal is the AABB-face normal (the precise MRT
|
||||
// normal would need a second async map — a later refinement).
|
||||
struct SurfaceHit {
|
||||
bool found = false;
|
||||
std::uint32_t object_id = 0;
|
||||
Eigen::Vector3f world_pos = Eigen::Vector3f::Zero();
|
||||
Eigen::Vector3f world_normal = Eigen::Vector3f::UnitZ();
|
||||
float aabb_radius = 0.0f;
|
||||
};
|
||||
void pickSurfaceAtAsync(int x_pixels, int y_pixels,
|
||||
std::function<void(SurfaceHit)> cb);
|
||||
#endif
|
||||
|
||||
// Per-pick result for the Area / Length / Volume tools. The
|
||||
// composed_transform mirrors InstanceCpu::transform so callers can
|
||||
// composed_transform mirrors InstanceInfo::transform so callers can
|
||||
// round-trip from mesh-local back to world without re-deriving it.
|
||||
struct MeshLocalPick {
|
||||
std::uint32_t object_id = 0;
|
||||
@@ -793,6 +899,10 @@ private:
|
||||
WGPUPipelineLayout pipeline_layout_ = nullptr;
|
||||
WGPURenderPipeline main_pipeline_ = nullptr;
|
||||
WGPURenderPipeline main_pipeline_transparent_ = nullptr;
|
||||
// Section-plane gizmo, shared by desktop + web (both render via render()).
|
||||
// Lifted out of the Qt-coupled OverlayRenderer so one identical gizmo draws
|
||||
// everywhere; the desktop's OverlayRenderer no longer draws it.
|
||||
SectionGizmoRenderer section_gizmo_;
|
||||
|
||||
// HiZ occlusion-cull pipeline group. Downsamples MSAA depth into a
|
||||
// mip pyramid; consumed by next-frame cull.
|
||||
@@ -882,10 +992,13 @@ private:
|
||||
WGPUTextureView pick_color_view_ = nullptr;
|
||||
WGPUTexture pick_normal_texture_ = nullptr;
|
||||
WGPUTextureView pick_normal_view_ = nullptr;
|
||||
WGPUTexture pick_position_texture_ = nullptr; // RGBA32F exact world pos
|
||||
WGPUTextureView pick_position_view_ = nullptr;
|
||||
WGPUTexture pick_depth_texture_ = nullptr;
|
||||
WGPUTextureView pick_depth_view_ = nullptr;
|
||||
WGPUBuffer pick_staging_buffer_ = nullptr;
|
||||
WGPUBuffer pick_normal_staging_buffer_ = nullptr;
|
||||
WGPUBuffer pick_position_staging_buffer_ = nullptr;
|
||||
int pick_w_ = 0;
|
||||
int pick_h_ = 0;
|
||||
WGPUBuffer box_pick_staging_buffer_ = nullptr;
|
||||
@@ -895,6 +1008,22 @@ private:
|
||||
// pick_async_cb_ fires with object_id when the spontaneous map resolves.
|
||||
bool pick_async_in_flight_ = false;
|
||||
std::function<void(std::uint32_t)> pick_async_cb_;
|
||||
// Async box-pick (marquee) state (web). Rect dims are stashed so the
|
||||
// spontaneous map callback knows how to walk the padded staging rows.
|
||||
bool box_pick_async_in_flight_ = false;
|
||||
std::function<void(std::vector<std::uint32_t>)> box_pick_async_cb_;
|
||||
int box_pick_async_w_ = 0;
|
||||
int box_pick_async_h_ = 0;
|
||||
std::uint64_t box_pick_async_padded_bpr_ = 0;
|
||||
std::uint64_t box_pick_async_bytes_ = 0;
|
||||
// Async surface pick (section tool): chained id→normal staging maps. Reuses
|
||||
// pick_async_in_flight_ (same staging buffers as the single object pick).
|
||||
std::function<void(SurfaceHit)> surface_async_cb_;
|
||||
int surface_async_x_ = 0;
|
||||
int surface_async_y_ = 0;
|
||||
std::uint32_t surface_async_id_ = 0;
|
||||
Eigen::Vector3f surface_async_normal_ = Eigen::Vector3f::Zero();
|
||||
void finishSurfaceAsync(SurfaceHit hit);
|
||||
#endif
|
||||
|
||||
// ---- Frame uniforms + selection bind ----------------------------------
|
||||
@@ -918,6 +1047,13 @@ private:
|
||||
// removeSectionPlane (still Qt-bound — they wire into the input
|
||||
// path). Reading happens here.
|
||||
std::vector<SectionPlane> section_planes_;
|
||||
// Section-gizmo drag state (shared): which plane, its press-time origin, and
|
||||
// the press point (logical px) so update can slide it along the normal.
|
||||
bool section_drag_active_ = false;
|
||||
int section_drag_index_ = -1;
|
||||
Eigen::Vector3f section_drag_start_origin_ = Eigen::Vector3f::Zero();
|
||||
int section_drag_start_mx_ = 0;
|
||||
int section_drag_start_my_ = 0;
|
||||
|
||||
// X-ray mode alpha clamp: when < 1.0 every instance routes through
|
||||
// the transparent pass with fragment.a clamped to min(in.color.a, cap).
|
||||
@@ -1026,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>>
|
||||
@@ -1151,6 +1287,10 @@ private:
|
||||
// Fly-camera move speed (m/s), wheel-adjustable via flyAdjustSpeed. Shared
|
||||
// by desktop + web fly mode; the mode flag itself lives in each host.
|
||||
float fly_move_speed_ = 5.0f;
|
||||
// Nav mouse bindings; default matches the historical "blender" preset.
|
||||
NavBindings nav_bindings_ = { MouseBtn::Middle, NavMod::Plain,
|
||||
MouseBtn::Middle, NavMod::Shift,
|
||||
MouseBtn::Left, NavMod::Plain };
|
||||
// Perspective by default; toggleProjection (P key) flips this. When
|
||||
// true, buildViewProj uses an orthographic matrix sized by
|
||||
// camera_distance_ × tan(fov/2) so toggling looks like a smooth
|
||||
|
||||
@@ -436,7 +436,8 @@ void ViewportWindow::encodeOverlaysInMainPass(WGPURenderPassEncoder pass,
|
||||
// — drawn inside the MSAA pass so depth-test correctly hides them
|
||||
// behind closer geometry. (Corner axis / marquee / labels run on the
|
||||
// resolved surface; see encodeOverlaysPostMain.)
|
||||
overlays_.encodeSectionGizmos(pass, frame, section_planes_);
|
||||
// NB: section-plane gizmos now draw from ViewportCore::render via the shared
|
||||
// SectionGizmoRenderer (desktop + web), so they are NOT drawn here.
|
||||
overlays_.encodeHighlightTriangles(pass, frame);
|
||||
overlays_.encodePivot(pass, frame, pivot_indicator_visible_);
|
||||
overlays_.encodeOverlayLines(pass, frame);
|
||||
@@ -558,17 +559,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); }
|
||||
|
||||
@@ -1047,11 +1050,11 @@ bool ViewportWindow::meshLocalToGlobal(uint32_t object_id,
|
||||
double global_out[3]) const {
|
||||
// Find the instance via the per-model object_id_to_instance map.
|
||||
// Use the live map key (`mid`) — see pickMeshLocalAt comment about
|
||||
// stale InstanceCpu::model_id from sidecar writes.
|
||||
// stale InstanceInfo::model_id from sidecar writes.
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
auto it = m.object_id_to_instance.find(object_id);
|
||||
if (it == m.object_id_to_instance.end()) continue;
|
||||
const InstanceCpu& inst = m.instances[it->second];
|
||||
const InstanceInfo& inst = m.instances[it->second];
|
||||
// CoordinateOperation · placement · local — gives the IFC's own
|
||||
// georeferenced world frame (ENH). Excludes FederatedFalseOrigin
|
||||
// and ModelTransformation, matching the GL meshLocalToGlobal
|
||||
@@ -1147,7 +1150,7 @@ void ViewportWindow::invertElementVisibility() {
|
||||
to_hide.reserve(1024);
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden) continue;
|
||||
for (const InstanceCpu& inst : m.instances) {
|
||||
for (const InstanceInfo& inst : m.instances) {
|
||||
if (inst.object_id == 0) continue;
|
||||
if (!visibility_.isHidden(inst.object_id)) {
|
||||
to_hide.push_back(inst.object_id);
|
||||
@@ -1243,7 +1246,7 @@ void ViewportWindow::updateVolumeReadout() {
|
||||
total += v;
|
||||
if (!show_labels) continue;
|
||||
// O(1) instance lookup via object_id_to_instance, then read the
|
||||
// world AABB from the cached InstanceCpu directly — same data
|
||||
// world AABB from the cached InstanceInfo directly — same data
|
||||
// computeObjectAabb's linear scan would have produced for the
|
||||
// first matching instance. For label placement at the AABB
|
||||
// centre this is identical-looking; only the rare multi-
|
||||
@@ -1251,7 +1254,7 @@ void ViewportWindow::updateVolumeReadout() {
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
auto it = m.object_id_to_instance.find(oid);
|
||||
if (it == m.object_id_to_instance.end()) continue;
|
||||
const InstanceCpu& inst = m.instances[it->second];
|
||||
const InstanceInfo& inst = m.instances[it->second];
|
||||
OverlayRenderer::Label lbl;
|
||||
lbl.world_pos[0] = (inst.world_aabb_min[0] + inst.world_aabb_max[0]) * 0.5f;
|
||||
lbl.world_pos[1] = (inst.world_aabb_min[1] + inst.world_aabb_max[1]) * 0.5f;
|
||||
@@ -1274,93 +1277,12 @@ void ViewportWindow::updateVolumeReadout() {
|
||||
overlays_.setOverlayLabels(labels);
|
||||
}
|
||||
|
||||
// Project a world point to LOGICAL pixel coords (Qt's mouse-event units).
|
||||
// Returns false if behind the camera.
|
||||
static bool projectWorldToLogicalScreen(const Eigen::Matrix4f& vp,
|
||||
const Eigen::Vector3f& world,
|
||||
int win_w, int win_h,
|
||||
Eigen::Vector2f& out) {
|
||||
const Eigen::Vector4f clip = vp * Eigen::Vector4f(world.x(), world.y(), world.z(), 1.0f);
|
||||
if (clip.w() <= 0.0f) return false;
|
||||
const float invw = 1.0f / clip.w();
|
||||
out = Eigen::Vector2f(
|
||||
(clip.x() * invw * 0.5f + 0.5f) * float(win_w),
|
||||
(1.0f - (clip.y() * invw * 0.5f + 0.5f)) * float(win_h));
|
||||
return true;
|
||||
}
|
||||
// projectWorldToLogicalScreen moved to SectionGizmoRenderer (its only users,
|
||||
// the section hit-test + drag, now live in ViewportCore).
|
||||
|
||||
int ViewportWindow::hitTestSectionGizmo(int x, int y) const {
|
||||
if (section_planes_.empty()) return -1;
|
||||
const int w = width();
|
||||
const int h = height();
|
||||
if (w <= 0 || h <= 0) return -1;
|
||||
Eigen::Matrix4f view, proj;
|
||||
core_.buildViewProj(view, proj);
|
||||
const Eigen::Matrix4f vp = proj * view;
|
||||
const float grab_px = 12.0f;
|
||||
int best = -1;
|
||||
float best_d2 = grab_px * grab_px;
|
||||
for (int i = 0; i < int(section_planes_.size()); ++i) {
|
||||
const SectionPlane& p = section_planes_[i];
|
||||
Eigen::Vector2f s_origin, s_tip;
|
||||
if (!projectWorldToLogicalScreen(vp, p.origin,
|
||||
w, h, s_origin)) continue;
|
||||
// The gizmo's arrow extends along +n by exactly 1 m in world
|
||||
// space — OverlayRenderer::encodeSectionGizmos uses
|
||||
// half_size = 1.0 to scale a plane-local arrow tip at z = 1.
|
||||
// Mirror that here.
|
||||
if (!projectWorldToLogicalScreen(vp, p.origin + p.n * 1.0f,
|
||||
w, h, s_tip)) continue;
|
||||
const Eigen::Vector2f q{float(x), float(y)};
|
||||
const Eigen::Vector2f ab = s_tip - s_origin;
|
||||
const float ab_len2 = ab.squaredNorm();
|
||||
if (ab_len2 < 1e-3f) continue;
|
||||
float t = (q - s_origin).dot(ab) / ab_len2;
|
||||
t = std::clamp(t, 0.0f, 1.0f);
|
||||
const Eigen::Vector2f proj_pt = s_origin + ab * t;
|
||||
const float d2 = (q - proj_pt).squaredNorm();
|
||||
if (d2 < best_d2) { best_d2 = d2; best = i; }
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
void ViewportWindow::updateSectionDrag(int x, int y) {
|
||||
if (!section_drag_active_) return;
|
||||
if (section_drag_index_ < 0
|
||||
|| section_drag_index_ >= int(section_planes_.size())) return;
|
||||
SectionPlane& p = section_planes_[section_drag_index_];
|
||||
|
||||
const int w = width();
|
||||
const int h = height();
|
||||
if (w <= 0 || h <= 0) return;
|
||||
Eigen::Matrix4f view, proj;
|
||||
core_.buildViewProj(view, proj);
|
||||
const Eigen::Matrix4f vp = proj * view;
|
||||
|
||||
// Re-project the press-time origin and origin + n to screen space.
|
||||
// The press-time origin is what `start` should be relative to — so the
|
||||
// plane slides smoothly even as the camera moves (we re-project every
|
||||
// frame to handle mid-drag camera rotation cleanly).
|
||||
Eigen::Vector2f s_origin, s_n;
|
||||
if (!projectWorldToLogicalScreen(vp, section_drag_start_origin_,
|
||||
w, h, s_origin)) return;
|
||||
if (!projectWorldToLogicalScreen(vp, section_drag_start_origin_ + p.n,
|
||||
w, h, s_n)) return;
|
||||
const Eigen::Vector2f screen_axis = s_n - s_origin;
|
||||
const float screen_axis_len2 = screen_axis.squaredNorm();
|
||||
if (screen_axis_len2 < 1e-3f) return; // arrow is edge-on
|
||||
|
||||
// Project pixel delta onto the screen-space axis; convert to metres
|
||||
// via (delta · axis) / |axis|² (axis is 1 m long in world space).
|
||||
const Eigen::Vector2f delta_px(float(x - section_drag_start_mouse_.x()),
|
||||
float(y - section_drag_start_mouse_.y()));
|
||||
const float meters = delta_px.dot(screen_axis)
|
||||
/ screen_axis_len2;
|
||||
|
||||
p.origin = section_drag_start_origin_ + p.n * meters;
|
||||
p.d = -p.n.dot(p.origin);
|
||||
requestUpdate();
|
||||
}
|
||||
// Section-gizmo hit-test + drag-to-move now live in ViewportCore (shared with
|
||||
// web, using SectionGizmoRenderer::hitTest). The mouse handlers call
|
||||
// core_.hitTestSectionGizmo / beginSectionDrag / updateSectionDrag / endSectionDrag.
|
||||
|
||||
// buildHizPipeline moved to ViewportCore (#84-r).
|
||||
|
||||
@@ -1542,21 +1464,32 @@ void ViewportWindow::fpsIntegrate() {
|
||||
|
||||
// chunkScreenAreaPx moved to ViewportCore (#84-h).
|
||||
|
||||
void ViewportWindow::applyNavPreset(const char* name) {
|
||||
// Matches GL AppSettings::NavPreset semantics exactly.
|
||||
// blender — Orbit MMB, Pan Shift+MMB (default)
|
||||
// rhino — Orbit RMB, Pan Shift+RMB
|
||||
// revit — Orbit Shift+MMB, Pan MMB
|
||||
if (name && std::strcmp(name, "rhino") == 0) {
|
||||
orbit_button_ = Qt::RightButton; orbit_mods_ = Qt::NoModifier;
|
||||
pan_button_ = Qt::RightButton; pan_mods_ = Qt::ShiftModifier;
|
||||
} else if (name && std::strcmp(name, "revit") == 0) {
|
||||
orbit_button_ = Qt::MiddleButton; orbit_mods_ = Qt::ShiftModifier;
|
||||
pan_button_ = Qt::MiddleButton; pan_mods_ = Qt::NoModifier;
|
||||
} else {
|
||||
orbit_button_ = Qt::MiddleButton; orbit_mods_ = Qt::NoModifier;
|
||||
pan_button_ = Qt::MiddleButton; pan_mods_ = Qt::ShiftModifier;
|
||||
static Qt::MouseButton toQtBtn(ViewportCore::MouseBtn b) {
|
||||
switch (b) {
|
||||
case ViewportCore::MouseBtn::Left: return Qt::LeftButton;
|
||||
case ViewportCore::MouseBtn::Middle: return Qt::MiddleButton;
|
||||
case ViewportCore::MouseBtn::Right: return Qt::RightButton;
|
||||
}
|
||||
return Qt::LeftButton;
|
||||
}
|
||||
static Qt::KeyboardModifiers toQtMod(ViewportCore::NavMod m) {
|
||||
switch (m) {
|
||||
case ViewportCore::NavMod::Plain: return Qt::NoModifier;
|
||||
case ViewportCore::NavMod::Shift: return Qt::ShiftModifier;
|
||||
case ViewportCore::NavMod::Ctrl: return Qt::ControlModifier;
|
||||
case ViewportCore::NavMod::Alt: return Qt::AltModifier;
|
||||
}
|
||||
return Qt::NoModifier;
|
||||
}
|
||||
|
||||
void ViewportWindow::applyNavPreset(const char* name) {
|
||||
// The preset table lives in ViewportCore (shared with web). Map its bindings
|
||||
// to the Qt types the mouse handlers compare against.
|
||||
core_.setNavPreset(name);
|
||||
const auto& b = core_.navBindings();
|
||||
orbit_button_ = toQtBtn(b.orbit); orbit_mods_ = toQtMod(b.orbit_mod);
|
||||
pan_button_ = toQtBtn(b.pan); pan_mods_ = toQtMod(b.pan_mod);
|
||||
select_button_ = toQtBtn(b.select); select_mods_ = toQtMod(b.select_mod);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -1615,13 +1548,9 @@ void ViewportWindow::mousePressEvent(QMouseEvent* event) {
|
||||
&& event->button() == Qt::LeftButton
|
||||
&& event->modifiers() == Qt::NoModifier) {
|
||||
const Eigen::Vector2i lp = toV2i(event->position().toPoint());
|
||||
const int hit = hitTestSectionGizmo(lp.x(), lp.y());
|
||||
if (hit >= 0) {
|
||||
section_drag_active_ = true;
|
||||
section_drag_index_ = hit;
|
||||
section_drag_start_mouse_ = lp;
|
||||
section_drag_start_origin_ = section_planes_[hit].origin;
|
||||
nav_drag_kind_ = NavDrag::Inactive;
|
||||
const int hit = core_.hitTestSectionGizmo(lp.x(), lp.y());
|
||||
if (hit >= 0 && core_.beginSectionDrag(hit, lp.x(), lp.y())) {
|
||||
nav_drag_kind_ = NavDrag::Inactive;
|
||||
Log::info().noquote().nospace()
|
||||
<< "[wgpu section] drag start: plane=" << hit;
|
||||
return;
|
||||
@@ -1642,15 +1571,15 @@ void ViewportWindow::mousePressEvent(QMouseEvent* event) {
|
||||
&& (mods & Qt::KeyboardModifierMask) == pan_mods_) {
|
||||
nav_drag_kind_ = NavDrag::Pan;
|
||||
setPivotIndicatorVisible(true);
|
||||
} else if (event->button() == Qt::LeftButton
|
||||
} else if (event->button() == select_button_
|
||||
&& !section_tool_active_
|
||||
&& tool_mode_ != ToolMode::Area
|
||||
&& tool_mode_ != ToolMode::Length
|
||||
&& nav_drag_kind_ == NavDrag::Inactive) {
|
||||
// Arm marquee box-select. Plain / Shift / Ctrl LMB without a tool
|
||||
// intercepting the click; if the cursor never moves past the
|
||||
// threshold this stays armed-only and the release falls through
|
||||
// to single-pick.
|
||||
// Arm marquee box-select. Plain / Shift / Ctrl on the select button
|
||||
// (Shift/Ctrl = add/remove) without a tool intercepting the click; if
|
||||
// the cursor never moves past the threshold this stays armed-only and
|
||||
// the release falls through to single-pick.
|
||||
box_select_armed_ = true;
|
||||
box_select_active_ = false;
|
||||
box_select_start_pos_ = nav_press_pos_;
|
||||
@@ -1660,16 +1589,15 @@ void ViewportWindow::mousePressEvent(QMouseEvent* event) {
|
||||
}
|
||||
|
||||
void ViewportWindow::mouseReleaseEvent(QMouseEvent* event) {
|
||||
if (section_drag_active_ && event->button() == Qt::LeftButton) {
|
||||
section_drag_active_ = false;
|
||||
section_drag_index_ = -1;
|
||||
if (core_.sectionDragActive() && event->button() == Qt::LeftButton) {
|
||||
core_.endSectionDrag();
|
||||
nav_active_button_ = Qt::NoButton;
|
||||
return;
|
||||
}
|
||||
// Marquee finalisation: only commit when the drag actually became
|
||||
// active (cursor moved past threshold). Press-time mods decide the
|
||||
// set op so a mid-drag Shift release doesn't flip the behaviour.
|
||||
if (box_select_armed_ && event->button() == Qt::LeftButton) {
|
||||
if (box_select_armed_ && event->button() == select_button_) {
|
||||
const bool was_active = box_select_active_;
|
||||
box_select_armed_ = false;
|
||||
box_select_active_ = false;
|
||||
@@ -1711,7 +1639,7 @@ void ViewportWindow::mouseReleaseEvent(QMouseEvent* event) {
|
||||
// LMB-click without drag → pick the object under the cursor and
|
||||
// route through the selection state. Shift = add, Ctrl = remove,
|
||||
// no modifier = replace. Empty-space click clears.
|
||||
if (event->button() == Qt::LeftButton && !nav_dragged_) {
|
||||
if (event->button() == select_button_ && !nav_dragged_) {
|
||||
const Eigen::Vector2i pos = toV2i(event->position().toPoint());
|
||||
const int px = int(pos.x() * devicePixelRatio());
|
||||
const int py = int(pos.y() * devicePixelRatio());
|
||||
@@ -1861,9 +1789,9 @@ void ViewportWindow::mouseMoveEvent(QMouseEvent* event) {
|
||||
// Section drag intercepts the move handler entirely: the orbit/pan
|
||||
// classification already declined this drag in mousePressEvent, so all
|
||||
// we have to do is slide the plane along its normal.
|
||||
if (section_drag_active_) {
|
||||
if (core_.sectionDragActive()) {
|
||||
const Eigen::Vector2i pos = toV2i(event->position().toPoint());
|
||||
updateSectionDrag(pos.x(), pos.y());
|
||||
core_.updateSectionDrag(pos.x(), pos.y());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -239,12 +239,15 @@ public:
|
||||
private:
|
||||
// Re-aim the orbit camera so the bounding sphere of [mn, mx] fits.
|
||||
void frameAabb(const float mn[3], const float mx[3], float padding);
|
||||
// Resolve nav_preset_ env var to orbit/pan bindings.
|
||||
void applyNavPreset(const char* name);
|
||||
|
||||
// chunkScreenAreaPx moved to ViewportCore (#84-h).
|
||||
|
||||
public:
|
||||
// Apply a nav mouse preset by name ("blender"|"rhino"|"revit"|"web").
|
||||
// Sources the shared binding table from ViewportCore; called from init
|
||||
// (env / persisted setting) and live from the Settings dialog.
|
||||
void applyNavPreset(const char* name);
|
||||
|
||||
|
||||
// Queue a one-shot framebuffer capture: the next rendered frame is
|
||||
// copied back to host memory and saved to `path` as PNG. If
|
||||
@@ -393,7 +396,7 @@ public:
|
||||
// A point that actually lies on the model's first instance — the
|
||||
// first instance's mesh AABB centre transformed by that instance's
|
||||
// placement, in metres, pre-CoordinateOperation. Lookup only — the
|
||||
// viewport already keeps the CPU-side MeshInfo + InstanceCpu around
|
||||
// viewport already keeps the CPU-side MeshInfo + InstanceInfo around
|
||||
// for picking / measurement; the federation false-origin guess
|
||||
// (ViewportView::guessFederatedFalseOriginFromFirstModel) consumes
|
||||
// this lazily on modelGeometryReady. Returns false when the model
|
||||
@@ -734,20 +737,9 @@ private:
|
||||
Qt::KeyboardModifiers box_select_press_mods_ = Qt::NoModifier;
|
||||
static constexpr int kBoxSelectThresholdPx = 5;
|
||||
// box_pick_staging_buffer_/_capacity_ moved to ViewportCore (#84-t).
|
||||
// Drag-to-move state for the arrow gizmo. While `section_drag_active_`
|
||||
// is true, mouseMoveEvent calls updateSectionDrag instead of letting
|
||||
// the press fall through to the orbit/pan handlers.
|
||||
bool section_drag_active_ = false;
|
||||
int section_drag_index_ = -1;
|
||||
Eigen::Vector2i section_drag_start_mouse_;
|
||||
Eigen::Vector3f section_drag_start_origin_;
|
||||
// Mirrors GL ViewportWindow::hitTestSectionGizmo: returns the index of
|
||||
// the plane whose arrow gizmo is within grab_px of (x, y), or -1.
|
||||
int hitTestSectionGizmo(int x, int y) const;
|
||||
// Mirrors GL ViewportWindow::updateSectionDrag: projects the cursor
|
||||
// delta onto the plane's normal in screen space and slides the plane
|
||||
// along that direction.
|
||||
void updateSectionDrag(int x, int y);
|
||||
// Section-gizmo drag state + hit-test/drag math moved to ViewportCore
|
||||
// (shared with web; the mouse handlers call core_.begin/update/endSectionDrag
|
||||
// and core_.hitTestSectionGizmo).
|
||||
|
||||
// HiZ slot + pyramid aliases (storage in core_, #84-r). VW's render
|
||||
// loop reads hiz_valid_ / hiz_vp_ to gate the HizOccludedFn, and
|
||||
@@ -800,10 +792,15 @@ private:
|
||||
// so the click-vs-drag distinction at mouseReleaseEvent's pick path keeps
|
||||
// working. Set at init from WGPU_NAV_PRESET=blender|rhino|revit (default
|
||||
// blender, matching GL's AppSettings::NavPreset::Blender default).
|
||||
Qt::MouseButton orbit_button_ = Qt::MiddleButton;
|
||||
Qt::KeyboardModifiers orbit_mods_ = Qt::NoModifier;
|
||||
Qt::MouseButton pan_button_ = Qt::MiddleButton;
|
||||
Qt::KeyboardModifiers pan_mods_ = Qt::ShiftModifier;
|
||||
// Mirror of ViewportCore's preset bindings, mapped to Qt types by
|
||||
// applyNavPreset (the core owns the preset table; these are the Qt-side
|
||||
// cache the mouse handlers compare against).
|
||||
Qt::MouseButton orbit_button_ = Qt::MiddleButton;
|
||||
Qt::KeyboardModifiers orbit_mods_ = Qt::NoModifier;
|
||||
Qt::MouseButton pan_button_ = Qt::MiddleButton;
|
||||
Qt::KeyboardModifiers pan_mods_ = Qt::ShiftModifier;
|
||||
Qt::MouseButton select_button_ = Qt::LeftButton;
|
||||
Qt::KeyboardModifiers select_mods_ = Qt::NoModifier;
|
||||
// Set by mousePressEvent based on which binding matched; consumed by
|
||||
// mouseMoveEvent so mid-drag modifier changes don't switch axes.
|
||||
enum class NavDrag : uint8_t { Inactive, Orbit, Pan };
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -143,6 +143,63 @@ TEST_CASE("toggleXray flips the active state", "[camera][xray]") {
|
||||
REQUIRE_FALSE(core.xrayActive());
|
||||
}
|
||||
|
||||
TEST_CASE("section planes: add appends, clear drops all, capped at the max",
|
||||
"[camera][section]") {
|
||||
MockHost host; ViewportCore core(&host);
|
||||
REQUIRE(core.sectionPlaneCount() == 0);
|
||||
|
||||
const Eigen::Vector3f pt(1, 2, 3), n(0, 0, 1);
|
||||
REQUIRE(core.addSectionPlaneAtSurface(pt, n, 1.0f));
|
||||
REQUIRE(core.addSectionPlaneAtSurface(pt, n, 1.0f));
|
||||
REQUIRE(core.sectionPlaneCount() == 2);
|
||||
|
||||
// Fill to the cap (kMaxSectionPlanes == 6); further adds are rejected.
|
||||
while (core.sectionPlaneCount() < 6) REQUIRE(core.addSectionPlaneAtSurface(pt, n, 1.0f));
|
||||
REQUIRE_FALSE(core.addSectionPlaneAtSurface(pt, n, 1.0f));
|
||||
REQUIRE(core.sectionPlaneCount() == 6);
|
||||
|
||||
core.clearSectionPlanes();
|
||||
REQUIRE(core.sectionPlaneCount() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("setNavPreset maps names to the shared button bindings", "[camera][nav]") {
|
||||
MockHost host; ViewportCore core(&host);
|
||||
using B = ViewportCore::MouseBtn; using M = ViewportCore::NavMod;
|
||||
|
||||
// Default is blender: orbit MMB, pan Shift+MMB, select LMB.
|
||||
{
|
||||
const auto& b = core.navBindings();
|
||||
REQUIRE(b.orbit == B::Middle); REQUIRE(b.orbit_mod == M::Plain);
|
||||
REQUIRE(b.pan == B::Middle); REQUIRE(b.pan_mod == M::Shift);
|
||||
REQUIRE(b.select == B::Left); REQUIRE(b.select_mod == M::Plain);
|
||||
}
|
||||
SECTION("web: orbit LMB, pan MMB, select RMB") {
|
||||
core.setNavPreset("web");
|
||||
const auto& b = core.navBindings();
|
||||
REQUIRE(b.orbit == B::Left); REQUIRE(b.orbit_mod == M::Plain);
|
||||
REQUIRE(b.pan == B::Middle); REQUIRE(b.pan_mod == M::Plain);
|
||||
REQUIRE(b.select == B::Right); REQUIRE(b.select_mod == M::Plain);
|
||||
}
|
||||
SECTION("rhino: orbit RMB, pan Shift+RMB") {
|
||||
core.setNavPreset("rhino");
|
||||
const auto& b = core.navBindings();
|
||||
REQUIRE(b.orbit == B::Right); REQUIRE(b.pan == B::Right);
|
||||
REQUIRE(b.pan_mod == M::Shift); REQUIRE(b.select == B::Left);
|
||||
}
|
||||
SECTION("revit: orbit Shift+MMB, pan MMB") {
|
||||
core.setNavPreset("revit");
|
||||
const auto& b = core.navBindings();
|
||||
REQUIRE(b.orbit == B::Middle); REQUIRE(b.orbit_mod == M::Shift);
|
||||
REQUIRE(b.pan == B::Middle); REQUIRE(b.pan_mod == M::Plain);
|
||||
}
|
||||
SECTION("unknown name falls back to blender") {
|
||||
core.setNavPreset("web");
|
||||
core.setNavPreset("nonsense");
|
||||
const auto& b = core.navBindings();
|
||||
REQUIRE(b.orbit == B::Middle); REQUIRE(b.select == B::Left);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("hideSelected hides the selection; showAll restores", "[camera][visibility]") {
|
||||
MockHost host; ViewportCore core(&host);
|
||||
REQUIRE(core.hiddenCount() == 0);
|
||||
@@ -158,3 +215,25 @@ TEST_CASE("hideSelected hides the selection; showAll restores", "[camera][visibi
|
||||
core.showAll();
|
||||
REQUIRE(core.hiddenCount() == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("applyMarqueeToSelection: replace / add / remove", "[camera][selection]") {
|
||||
MockHost host; ViewportCore core(&host);
|
||||
// No public selection accessor, so verify via hideSelected → hiddenCount.
|
||||
SECTION("plain marquee replaces the selection") {
|
||||
core.applyMarqueeToSelection({1, 2, 3}, /*add*/false, /*remove*/false);
|
||||
core.hideSelected();
|
||||
REQUIRE(core.hiddenCount() == 3);
|
||||
}
|
||||
SECTION("add unions, remove subtracts") {
|
||||
core.applyMarqueeToSelection({5}, false, false); // replace → {5}
|
||||
core.applyMarqueeToSelection({6, 7}, true, false); // add → {5,6,7}
|
||||
core.applyMarqueeToSelection({6}, false, true); // remove → {5,7}
|
||||
core.hideSelected();
|
||||
REQUIRE(core.hiddenCount() == 2);
|
||||
}
|
||||
SECTION("id 0 is ignored") {
|
||||
core.applyMarqueeToSelection({0, 9, 0}, false, false);
|
||||
core.hideSelected();
|
||||
REQUIRE(core.hiddenCount() == 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user