/******************************************************************************** * * * 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 . * * * ********************************************************************************/ #include "OverlayRenderer.h" #include "CameraMath.h" #include #include #include #include #include #include #include #include #include #include #include #include #include // ----------------------------------------------------------------------------- // Local helpers // ----------------------------------------------------------------------------- namespace { WGPUStringView svFromCStr(const char* s) { WGPUStringView v{}; v.data = s; v.length = std::strlen(s); return v; } // Populate `attribs[5]` with the standard thick-line vertex layout: // loc 0: start (vec3 @ 0) loc 1: end (vec3 @ 12) // loc 2: col (vec3 @ 24) loc 3: t (f32 @ 36) // loc 4: side (f32 @ 40) // Returns a WGPUVertexBufferLayout aliasing the caller-owned `attribs`. WGPUVertexBufferLayout thickLineVertexLayout(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; return vbl; } // Pack the axis uniform's 256-byte slot. Layout matches WGSL AxisUniforms: // mat4 + vec3 + f32 + f32 + f32 + vec2 = 96 B used, padded to 256. void packAxisUniform(uint8_t* dst, const Eigen::Matrix4f& mvp, const Eigen::Vector3f& origin, float arm, float alpha, float line_width_px, float viewport_w, float viewport_h) { std::memset(dst, 0, 256); std::memcpy(dst, mvp.data(), 16 * sizeof(float)); float ox = origin.x(), oy = origin.y(), oz = origin.z(); std::memcpy(dst + 64, &ox, sizeof(float)); std::memcpy(dst + 68, &oy, sizeof(float)); std::memcpy(dst + 72, &oz, sizeof(float)); std::memcpy(dst + 76, &arm, sizeof(float)); std::memcpy(dst + 80, &alpha, sizeof(float)); std::memcpy(dst + 84, &line_width_px, sizeof(float)); std::memcpy(dst + 88, &viewport_w, sizeof(float)); 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 // ----------------------------------------------------------------------------- // Shared WGSL — VsOut + thick_line_clip helper + fs_main AA fragment // ----------------------------------------------------------------------------- // NB: defined as a `static const char* const` (not a `#define`) because GCC 11 // (Rocky Linux manylinux runner) does not parse a multi-line raw string inside // a `#define` body — newer GCC and Clang handle it fine. static const char* const THICK_LINE_HELPERS_WGSL = R"WGSL( struct VsOut { @builtin(position) clip_pos: vec4, @location(0) color: vec4, @location(1) side_t: f32, }; fn thick_line_clip(p_start: vec4, p_end: vec4, t: f32, side: f32, viewport_size: vec2, line_width_px: f32) -> vec4 { 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(-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(p_here.xy + off_ndc * p_here.w, p_here.zw); } @fragment fn fs_main(in: VsOut) -> @location(0) vec4 { 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(in.color.xyz, in.color.w * coverage); } )WGSL"; static const std::string AXIS_WGSL = std::string(THICK_LINE_HELPERS_WGSL) + R"WGSL( struct AxisUniforms { mvp: mat4x4, origin: vec3, arm: f32, alpha: f32, line_width_px: f32, viewport_size: vec2, }; @group(0) @binding(0) var u: AxisUniforms; @vertex fn vs_main(@location(0) start: vec3, @location(1) end: vec3, @location(2) col: vec3, @location(3) t: f32, @location(4) side: f32) -> VsOut { let p_start = u.mvp * vec4(u.origin + start * u.arm, 1.0); let p_end = u.mvp * vec4(u.origin + end * u.arm, 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(col, u.alpha); out.side_t = side; return out; } )WGSL"; static const std::string SECTION_WGSL = std::string(THICK_LINE_HELPERS_WGSL) + R"WGSL( struct SectionUniforms { mvp: mat4x4, origin: vec3, half_size: f32, tangent: vec3, line_width_px: f32, bitangent: vec3, _pad1: f32, normal: vec3, _pad2: f32, tint: vec4, viewport_size: vec2, _pad3: vec2, }; @group(0) @binding(0) var u: SectionUniforms; fn plane_to_world(p: vec3) -> vec3 { 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, @location(1) end_local: vec3, @location(2) col: vec3, @location(3) t: f32, @location(4) side: f32) -> VsOut { let p_start = u.mvp * vec4(plane_to_world(start_local), 1.0); let p_end = u.mvp * vec4(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(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, rect_max: vec2, color: vec4, viewport_size: vec2, line_width_px: f32, fill_alpha: f32, }; @group(0) @binding(0) var u: MarqueeUniforms; @vertex fn vs_main(@location(0) start_uv: vec2, @location(1) end_uv: vec2, @location(2) t: f32, @location(3) side: f32) -> VsOut { let p_start = vec4(mix(u.rect_min, u.rect_max, start_uv), 0.0, 1.0); let p_end = vec4(mix(u.rect_min, u.rect_max, end_uv), 0.0, 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 = u.color; out.side_t = side; return out; } struct VsFillOut { @builtin(position) clip_pos: vec4, }; @vertex fn vs_fill(@location(0) pos_uv: vec2) -> VsFillOut { var out: VsFillOut; let p = mix(u.rect_min, u.rect_max, pos_uv); out.clip_pos = vec4(p, 0.0, 1.0); return out; } @fragment fn fs_fill() -> @location(0) vec4 { return vec4(u.color.xyz, u.color.w * u.fill_alpha); } )WGSL"; // Overlay-line shader: world-space segments expanded into screen-space // quads. Same expansion strategy as the GL OverlayRenderer LINE_VS/FS pair — // per-vertex (a, b, side, along), per-fragment signed-perpendicular distance // for stroke pick and arc length for dash. Lives in its own module because // the FS needs the dash/stroke logic that the shared thick-line helper // doesn't carry (axis / section / marquee never dash). static const char* OVERLAY_LINES_WGSL = R"WGSL( struct LineUniforms { view_proj: mat4x4, inner_color: vec4, stroke_color: vec4, viewport_size: vec2, line_width_px: f32, // inner full-width (px) stroke_extra: f32, // halo per side (px) dash_period_px: f32, // 0 = solid dash_on_ratio: f32, }; @group(0) @binding(0) var u: LineUniforms; struct VsOut { @builtin(position) clip_pos: vec4, @location(0) dist_px: f32, // signed perpendicular distance (px) @location(1) along_px: f32, // arc length from segment start (px) }; @vertex fn vs_main(@location(0) a: vec3, @location(1) b: vec3, @location(2) side: f32, @location(3) along: f32) -> VsOut { let clip_a = u.view_proj * vec4(a, 1.0); let clip_b = u.view_proj * vec4(b, 1.0); let s_a = (clip_a.xy / clip_a.w) * 0.5 * u.viewport_size; let s_b = (clip_b.xy / clip_b.w) * 0.5 * u.viewport_size; let delta = s_b - s_a; let len = length(delta); var dir = vec2(1.0, 0.0); if (len > 1e-6) { dir = delta / len; } let perp = vec2(-dir.y, dir.x); let clip_self = mix(clip_a, clip_b, along); var s_self = (clip_self.xy / clip_self.w) * 0.5 * u.viewport_size; let half_total = u.line_width_px * 0.5 + u.stroke_extra; s_self = s_self + perp * side * half_total; let ndc_out = s_self / (u.viewport_size * 0.5); var out: VsOut; out.clip_pos = vec4(ndc_out * clip_self.w, clip_self.z, clip_self.w); out.dist_px = side * half_total; out.along_px = along * len; return out; } @fragment fn fs_main(in: VsOut) -> @location(0) vec4 { if (u.dash_period_px > 0.0) { let t = in.along_px - floor(in.along_px / u.dash_period_px) * u.dash_period_px; if (t > u.dash_period_px * u.dash_on_ratio) { discard; } } let ad = abs(in.dist_px); let half_inner = u.line_width_px * 0.5; let total = half_inner + u.stroke_extra; if (ad > total) { discard; } var col = u.inner_color; if (ad > half_inner) { col = u.stroke_color; } let outer_a = smoothstep(total, total - 1.0, ad); return vec4(col.xyz, col.w * outer_a); } )WGSL"; // Overlay-point shader: world-space positions expanded into screen-space // quads (sprite size = inner_diameter + 2*stroke_extra). The FS does the // sprite-distance pick + AA — same shape as GL OverlayRenderer's POINT_FS // but reads `corner` from a vertex varying instead of gl_PointCoord // because WebGPU has no point primitive with a sized sprite. static const char* OVERLAY_POINTS_WGSL = R"WGSL( struct PointUniforms { view_proj: mat4x4, inner_color: vec4, stroke_color: vec4, viewport_size: vec2, total_half_px: f32, // (inner_diameter + 2*stroke_extra) * 0.5 inner_radius_norm: f32, // inner_radius / total_half ∈ (0, 1] }; @group(0) @binding(0) var u: PointUniforms; struct VsOut { @builtin(position) clip_pos: vec4, @location(0) corner: vec2, }; @vertex fn vs_main(@location(0) world_pos: vec3, @location(1) corner: vec2) -> VsOut { let clip = u.view_proj * vec4(world_pos, 1.0); let ndc = clip.xy / clip.w; let s = ndc * 0.5 * u.viewport_size; let s_off = s + corner * u.total_half_px; let ndc_out = s_off / (u.viewport_size * 0.5); var out: VsOut; out.clip_pos = vec4(ndc_out * clip.w, clip.z, clip.w); out.corner = corner; return out; } @fragment fn fs_main(in: VsOut) -> @location(0) vec4 { let d = length(in.corner); if (d > 1.0) { discard; } var col = u.inner_color; if (d > u.inner_radius_norm) { col = u.stroke_color; } let aa = fwidth(d); let outer = 1.0 - smoothstep(1.0 - aa, 1.0, d); return vec4(col.xyz, col.w * outer); } )WGSL"; // Highlight-triangle shader: world-space triangle list, translucent // uniform fill. view_proj projects to clip; fragment outputs the // per-set RGBA tint. Depth-test honours occlusion against geometry; // depth-write off so subsequent overlays can still draw on top. static const char* HIGHLIGHT_TRIANGLES_WGSL = R"WGSL( struct HiUniforms { view_proj: mat4x4, color: vec4, }; @group(0) @binding(0) var u: HiUniforms; @vertex fn vs_main(@location(0) pos: vec3) -> @builtin(position) vec4 { return u.view_proj * vec4(pos, 1.0); } @fragment fn fs_main() -> @location(0) vec4 { return u.color; } )WGSL"; // Label shader: textured quads in screen space. Each visible label/HUD // item contributes 6 vertices (NDC position + uv); a pre-rasterised // QImage carrying both the dark-grey background fill and the white // text occupies the bound texture. Standard alpha blend. static const char* LABELS_WGSL = R"WGSL( @group(0) @binding(0) var samp: sampler; @group(0) @binding(1) var tex: texture_2d; struct VsOut { @builtin(position) clip_pos: vec4, @location(0) uv: vec2, }; @vertex fn vs_main(@location(0) ndc: vec2, @location(1) uv: vec2) -> VsOut { var out: VsOut; out.clip_pos = vec4(ndc, 0.0, 1.0); out.uv = uv; return out; } @fragment fn fs_main(in: VsOut) -> @location(0) vec4 { return textureSample(tex, samp, in.uv); } )WGSL"; // ----------------------------------------------------------------------------- // Construction / destruction // ----------------------------------------------------------------------------- OverlayRenderer::~OverlayRenderer() { destroy(); } bool OverlayRenderer::init(WGPUInstance instance, WGPUDevice device, WGPUQueue queue, WGPUTextureFormat surface_format, int sample_count) { instance_ = instance; device_ = device; queue_ = queue; surface_format_ = surface_format; sample_count_ = sample_count; if (!buildAxisIndicator()) return false; if (!buildSectionVisualizer()) return false; if (!buildMarquee()) return false; if (!buildOverlayLines()) return false; if (!buildOverlayPoints()) return false; if (!buildHighlightTriangles()) return false; if (!buildLabels()) return false; return true; } void OverlayRenderer::destroy() { // Axis indicator if (axis_bind_group_) { wgpuBindGroupRelease(axis_bind_group_); axis_bind_group_ = nullptr; } if (axis_pivot_pipeline_) { wgpuRenderPipelineRelease(axis_pivot_pipeline_); axis_pivot_pipeline_ = nullptr; } if (axis_pivot_xray_pipeline_){ wgpuRenderPipelineRelease(axis_pivot_xray_pipeline_); axis_pivot_xray_pipeline_ = nullptr; } if (axis_corner_pipeline_) { wgpuRenderPipelineRelease(axis_corner_pipeline_); axis_corner_pipeline_ = nullptr; } if (axis_shader_module_) { wgpuShaderModuleRelease(axis_shader_module_); axis_shader_module_ = nullptr; } if (axis_pipeline_layout_) { wgpuPipelineLayoutRelease(axis_pipeline_layout_); axis_pipeline_layout_ = nullptr; } if (axis_bgl_) { wgpuBindGroupLayoutRelease(axis_bgl_); axis_bgl_ = nullptr; } if (axis_uniform_buffer_) { wgpuBufferRelease(axis_uniform_buffer_); axis_uniform_buffer_ = nullptr; } 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; } if (marquee_pipeline_) { wgpuRenderPipelineRelease(marquee_pipeline_); marquee_pipeline_ = nullptr; } if (marquee_fill_pipeline_) { wgpuRenderPipelineRelease(marquee_fill_pipeline_); marquee_fill_pipeline_ = nullptr; } if (marquee_shader_module_) { wgpuShaderModuleRelease(marquee_shader_module_); marquee_shader_module_ = nullptr; } if (marquee_pipeline_layout_) { wgpuPipelineLayoutRelease(marquee_pipeline_layout_); marquee_pipeline_layout_ = nullptr; } if (marquee_bgl_) { wgpuBindGroupLayoutRelease(marquee_bgl_); marquee_bgl_ = nullptr; } if (marquee_uniform_buffer_) { wgpuBufferRelease(marquee_uniform_buffer_); marquee_uniform_buffer_ = nullptr; } if (marquee_vertex_buffer_) { wgpuBufferRelease(marquee_vertex_buffer_); marquee_vertex_buffer_ = nullptr; } if (marquee_fill_vertex_buffer_) { wgpuBufferRelease(marquee_fill_vertex_buffer_); marquee_fill_vertex_buffer_ = nullptr; } // Overlay lines if (overlay_line_bind_group_) { wgpuBindGroupRelease(overlay_line_bind_group_); overlay_line_bind_group_ = nullptr; } if (overlay_line_pipeline_) { wgpuRenderPipelineRelease(overlay_line_pipeline_); overlay_line_pipeline_ = nullptr; } if (overlay_line_shader_module_) { wgpuShaderModuleRelease(overlay_line_shader_module_); overlay_line_shader_module_ = nullptr; } if (overlay_line_pipeline_layout_) { wgpuPipelineLayoutRelease(overlay_line_pipeline_layout_); overlay_line_pipeline_layout_ = nullptr; } if (overlay_line_bgl_) { wgpuBindGroupLayoutRelease(overlay_line_bgl_); overlay_line_bgl_ = nullptr; } if (overlay_line_uniform_buffer_) { wgpuBufferRelease(overlay_line_uniform_buffer_); overlay_line_uniform_buffer_ = nullptr; } if (overlay_line_vertex_buffer_) { wgpuBufferRelease(overlay_line_vertex_buffer_); overlay_line_vertex_buffer_ = nullptr; } overlay_line_vertex_capacity_ = 0; overlay_line_uniform_slots_ = 0; overlay_line_draws_.clear(); // Overlay points if (overlay_point_bind_group_) { wgpuBindGroupRelease(overlay_point_bind_group_); overlay_point_bind_group_ = nullptr; } if (overlay_point_pipeline_) { wgpuRenderPipelineRelease(overlay_point_pipeline_); overlay_point_pipeline_ = nullptr; } if (overlay_point_shader_module_) { wgpuShaderModuleRelease(overlay_point_shader_module_); overlay_point_shader_module_ = nullptr; } if (overlay_point_pipeline_layout_) { wgpuPipelineLayoutRelease(overlay_point_pipeline_layout_); overlay_point_pipeline_layout_ = nullptr; } if (overlay_point_bgl_) { wgpuBindGroupLayoutRelease(overlay_point_bgl_); overlay_point_bgl_ = nullptr; } if (overlay_point_uniform_buffer_) { wgpuBufferRelease(overlay_point_uniform_buffer_); overlay_point_uniform_buffer_ = nullptr; } if (overlay_point_vertex_buffer_) { wgpuBufferRelease(overlay_point_vertex_buffer_); overlay_point_vertex_buffer_ = nullptr; } overlay_point_vertex_capacity_ = 0; overlay_point_vertex_count_ = 0; // Highlight triangles if (highlight_bind_group_) { wgpuBindGroupRelease(highlight_bind_group_); highlight_bind_group_ = nullptr; } if (highlight_pipeline_) { wgpuRenderPipelineRelease(highlight_pipeline_); highlight_pipeline_ = nullptr; } if (highlight_shader_module_) { wgpuShaderModuleRelease(highlight_shader_module_); highlight_shader_module_ = nullptr; } if (highlight_pipeline_layout_) { wgpuPipelineLayoutRelease(highlight_pipeline_layout_); highlight_pipeline_layout_ = nullptr; } if (highlight_bgl_) { wgpuBindGroupLayoutRelease(highlight_bgl_); highlight_bgl_ = nullptr; } if (highlight_uniform_buffer_) { wgpuBufferRelease(highlight_uniform_buffer_); highlight_uniform_buffer_ = nullptr; } if (highlight_vertex_buffer_) { wgpuBufferRelease(highlight_vertex_buffer_); highlight_vertex_buffer_ = nullptr; } highlight_vertex_capacity_ = 0; highlight_vertex_count_ = 0; highlight_color_[0] = highlight_color_[1] = highlight_color_[2] = highlight_color_[3] = 0.0f; // Labels + HUD releaseLabelTextures(); if (label_sampler_) { wgpuSamplerRelease(label_sampler_); label_sampler_ = nullptr; } if (label_pipeline_) { wgpuRenderPipelineRelease(label_pipeline_); label_pipeline_ = nullptr; } if (label_shader_module_) { wgpuShaderModuleRelease(label_shader_module_); label_shader_module_ = nullptr; } if (label_pipeline_layout_) { wgpuPipelineLayoutRelease(label_pipeline_layout_); label_pipeline_layout_ = nullptr; } if (label_bgl_) { wgpuBindGroupLayoutRelease(label_bgl_); label_bgl_ = nullptr; } if (label_vertex_buffer_) { wgpuBufferRelease(label_vertex_buffer_); label_vertex_buffer_ = nullptr; } label_vertex_capacity_ = 0; labels_.clear(); hud_text_.clear(); } // ----------------------------------------------------------------------------- // Axis indicator // ----------------------------------------------------------------------------- bool OverlayRenderer::buildAxisIndicator() { // Bonsai decorator palette (src/bonsai/bonsai/bim/ui.py:593+): // decorator_color_error = (1.000, 0.200, 0.322) — red → +X // decorator_color_selected = (0.545, 0.863, 0.000) — green → +Y // decorator_color_special = (0.157, 0.565, 1.000) — blue → +Z // Same palette is reused for the section gizmo + marquee so all overlay // colours come from one canonical source. static const float axis_verts[] = { // start end color (RGB — Bonsai decorators) t side // ---- +X red ---- 0,0,0, 1,0,0, 1.000f, 0.200f, 0.322f, 0.f, -1.f, 0,0,0, 1,0,0, 1.000f, 0.200f, 0.322f, 0.f, +1.f, 0,0,0, 1,0,0, 1.000f, 0.200f, 0.322f, 1.f, -1.f, 0,0,0, 1,0,0, 1.000f, 0.200f, 0.322f, 1.f, -1.f, 0,0,0, 1,0,0, 1.000f, 0.200f, 0.322f, 0.f, +1.f, 0,0,0, 1,0,0, 1.000f, 0.200f, 0.322f, 1.f, +1.f, // ---- +Y green ---- 0,0,0, 0,1,0, 0.545f, 0.863f, 0.000f, 0.f, -1.f, 0,0,0, 0,1,0, 0.545f, 0.863f, 0.000f, 0.f, +1.f, 0,0,0, 0,1,0, 0.545f, 0.863f, 0.000f, 1.f, -1.f, 0,0,0, 0,1,0, 0.545f, 0.863f, 0.000f, 1.f, -1.f, 0,0,0, 0,1,0, 0.545f, 0.863f, 0.000f, 0.f, +1.f, 0,0,0, 0,1,0, 0.545f, 0.863f, 0.000f, 1.f, +1.f, // ---- +Z blue ---- 0,0,0, 0,0,1, 0.157f, 0.565f, 1.000f, 0.f, -1.f, 0,0,0, 0,0,1, 0.157f, 0.565f, 1.000f, 0.f, +1.f, 0,0,0, 0,0,1, 0.157f, 0.565f, 1.000f, 1.f, -1.f, 0,0,0, 0,0,1, 0.157f, 0.565f, 1.000f, 1.f, -1.f, 0,0,0, 0,0,1, 0.157f, 0.565f, 1.000f, 0.f, +1.f, 0,0,0, 0,0,1, 0.157f, 0.565f, 1.000f, 1.f, +1.f, }; { WGPUBufferDescriptor bdesc = {}; bdesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst; bdesc.size = sizeof(axis_verts); bdesc.label = svFromCStr("ifcviewer-wgpu.axis_vbo"); axis_vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc); wgpuQueueWriteBuffer(queue_, axis_vertex_buffer_, 0, axis_verts, sizeof(axis_verts)); } { WGPUBufferDescriptor bdesc = {}; bdesc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst; bdesc.size = 3u * kAxisUniformSlotSize; bdesc.label = svFromCStr("ifcviewer-wgpu.axis_uniforms"); axis_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 = 96; WGPUBindGroupLayoutDescriptor bgl_desc = {}; bgl_desc.entryCount = 1; bgl_desc.entries = &entry; bgl_desc.label = svFromCStr("ifcviewer-wgpu.axis_bgl"); axis_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc); } { WGPUPipelineLayoutDescriptor pl_desc = {}; pl_desc.bindGroupLayoutCount = 1; pl_desc.bindGroupLayouts = &axis_bgl_; pl_desc.label = svFromCStr("ifcviewer-wgpu.axis_pipeline_layout"); axis_pipeline_layout_ = wgpuDeviceCreatePipelineLayout(device_, &pl_desc); } { WGPUBindGroupEntry entry = {}; entry.binding = 0; entry.buffer = axis_uniform_buffer_; entry.offset = 0; entry.size = kAxisUniformSlotSize; WGPUBindGroupDescriptor bg_desc = {}; bg_desc.layout = axis_bgl_; bg_desc.entryCount = 1; bg_desc.entries = &entry; bg_desc.label = svFromCStr("ifcviewer-wgpu.axis_bind_group"); axis_bind_group_ = wgpuDeviceCreateBindGroup(device_, &bg_desc); } { WGPUShaderSourceWGSL wgsl_src = {}; wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; wgsl_src.code = svFromCStr(AXIS_WGSL.c_str()); WGPUShaderModuleDescriptor sm_desc = {}; sm_desc.nextInChain = &wgsl_src.chain; sm_desc.label = svFromCStr("ifcviewer-wgpu.axis_wgsl"); axis_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; auto build_pivot = [&](WGPUCompareFunction cmp, const char* label, WGPURenderPipeline& out) { WGPUColorTargetState ct = {}; ct.format = surface_format_; ct.blend = &blend; ct.writeMask = WGPUColorWriteMask_All; WGPUFragmentState frag = {}; frag.module = axis_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 = cmp; depth.stencilFront.compare = WGPUCompareFunction_Always; depth.stencilBack.compare = WGPUCompareFunction_Always; WGPURenderPipelineDescriptor rp_desc = {}; rp_desc.layout = axis_pipeline_layout_; rp_desc.label = svFromCStr(label); rp_desc.vertex.module = axis_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; out = wgpuDeviceCreateRenderPipeline(device_, &rp_desc); }; build_pivot(WGPUCompareFunction_LessEqual, "ifcviewer-wgpu.axis_pivot_pipeline", axis_pivot_pipeline_); build_pivot(WGPUCompareFunction_GreaterEqual, "ifcviewer-wgpu.axis_pivot_xray_pipeline", axis_pivot_xray_pipeline_); // Corner: resolved surface, no depth, sampleCount=1. { WGPUColorTargetState ct = {}; ct.format = surface_format_; ct.blend = &blend; ct.writeMask = WGPUColorWriteMask_All; WGPUFragmentState frag = {}; frag.module = axis_shader_module_; frag.entryPoint = svFromCStr("fs_main"); frag.targetCount = 1; frag.targets = &ct; WGPURenderPipelineDescriptor rp_desc = {}; rp_desc.layout = axis_pipeline_layout_; rp_desc.label = svFromCStr("ifcviewer-wgpu.axis_corner_pipeline"); rp_desc.vertex.module = axis_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.primitive.topology = WGPUPrimitiveTopology_TriangleList; rp_desc.primitive.cullMode = WGPUCullMode_None; rp_desc.multisample.count = 1; rp_desc.multisample.mask = 0xFFFFFFFFu; axis_corner_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp_desc); } return axis_pivot_pipeline_ && axis_pivot_xray_pipeline_ && axis_corner_pipeline_; } void OverlayRenderer::encodePivot(WGPURenderPassEncoder pass, const OverlayFrame& f, bool visible) { if (!visible || !axis_pivot_pipeline_ || !axis_pivot_xray_pipeline_) return; if (f.viewport_h_px <= 0) return; // Arm length = 30 logical px projected into world at the pivot's distance. const float fovy_rad = qDegreesToRadians(f.camera_fov_y_deg); const float world_per_pixel = f.camera_distance * std::tan(fovy_rad * 0.5f) * 2.0f / float(f.viewport_h_px); const float arm_pixels = 30.0f * float(f.device_pixel_ratio); const float arm_world = arm_pixels * world_per_pixel; const float dpr = float(f.device_pixel_ratio); const float line_w = 2.5f * dpr; const float vw = float(f.viewport_w_px); const float vh = float(f.viewport_h_px); uint8_t slot_visible[256]; uint8_t slot_xray[256]; packAxisUniform(slot_visible, f.view_proj, f.camera_target, arm_world, 1.00f, line_w, vw, vh); packAxisUniform(slot_xray, f.view_proj, f.camera_target, arm_world, 0.30f, line_w, vw, vh); const uint32_t visible_off = 1u * kAxisUniformSlotSize; const uint32_t xray_off = 2u * kAxisUniformSlotSize; wgpuQueueWriteBuffer(queue_, axis_uniform_buffer_, visible_off, slot_visible, sizeof(slot_visible)); wgpuQueueWriteBuffer(queue_, axis_uniform_buffer_, xray_off, slot_xray, sizeof(slot_xray)); wgpuRenderPassEncoderSetVertexBuffer(pass, 0, axis_vertex_buffer_, 0, WGPU_WHOLE_SIZE); wgpuRenderPassEncoderSetPipeline(pass, axis_pivot_xray_pipeline_); wgpuRenderPassEncoderSetBindGroup(pass, 0, axis_bind_group_, 1, &xray_off); wgpuRenderPassEncoderDraw(pass, 18, 1, 0, 0); wgpuRenderPassEncoderSetPipeline(pass, axis_pivot_pipeline_); wgpuRenderPassEncoderSetBindGroup(pass, 0, axis_bind_group_, 1, &visible_off); wgpuRenderPassEncoderDraw(pass, 18, 1, 0, 0); } void OverlayRenderer::encodeCornerAxis(WGPUCommandEncoder enc, WGPUTextureView surface_view, const OverlayFrame& f) { if (!axis_corner_pipeline_ || !surface_view) return; const int dpr = std::max(1, f.device_pixel_ratio); const uint32_t gizmo_size = uint32_t(110 * dpr); const uint32_t margin = uint32_t(10 * dpr); if (gizmo_size == 0 || f.viewport_w_px <= 0 || f.viewport_h_px <= 0) return; // Bottom-left in WebGPU framebuffer space (y down). const uint32_t fb_h = uint32_t(f.viewport_h_px); if (gizmo_size + margin > fb_h) return; const uint32_t y = fb_h - margin - gizmo_size; // Independent ortho projection from the camera's direction. Near the // poles the up axis collapses against the look direction, so swap to // Y-up there — mirrors buildViewProj's identical fix on the viewport. const float yaw_rad = qDegreesToRadians(f.camera_yaw_deg); const float pitch_rad = qDegreesToRadians(f.camera_pitch_deg); const Eigen::Vector3f eye_dir(std::cos(pitch_rad) * std::cos(yaw_rad), std::cos(pitch_rad) * std::sin(yaw_rad), std::sin(pitch_rad)); const Eigen::Vector3f world_up = (std::abs(f.camera_pitch_deg) >= 89.0f) ? Eigen::Vector3f(0.0f, 1.0f, 0.0f) : Eigen::Vector3f(0.0f, 0.0f, 1.0f); const Eigen::Matrix4f gv = lookAtRH(eye_dir * 3.0f, Eigen::Vector3f::Zero(), world_up); const Eigen::Matrix4f gp = orthoGL(-1.4f, 1.4f, -1.4f, 1.4f, 0.1f, 10.0f); Eigen::Matrix4f z_remap = Eigen::Matrix4f::Identity(); z_remap(2, 2) = 0.5f; z_remap(2, 3) = 0.5f; const Eigen::Matrix4f mvp = z_remap * gp * gv; uint8_t slot[256]; const float line_w = 2.5f * float(dpr); packAxisUniform(slot, mvp, Eigen::Vector3f(0, 0, 0), 1.0f, 1.0f, line_w, float(gizmo_size), float(gizmo_size)); const uint32_t slot_offset = 0u; wgpuQueueWriteBuffer(queue_, axis_uniform_buffer_, slot_offset, slot, sizeof(slot)); WGPURenderPassColorAttachment color = {}; color.view = surface_view; color.loadOp = WGPULoadOp_Load; color.storeOp = WGPUStoreOp_Store; color.clearValue = { 0.0, 0.0, 0.0, 1.0 }; color.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; WGPURenderPassDescriptor pass_desc = {}; pass_desc.colorAttachmentCount = 1; pass_desc.colorAttachments = &color; pass_desc.label = svFromCStr("ifcviewer-wgpu.corner_axis_pass"); WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(enc, &pass_desc); wgpuRenderPassEncoderSetViewport(pass, float(margin), float(y), float(gizmo_size), float(gizmo_size), 0.0f, 1.0f); wgpuRenderPassEncoderSetPipeline(pass, axis_corner_pipeline_); wgpuRenderPassEncoderSetVertexBuffer(pass, 0, axis_vertex_buffer_, 0, WGPU_WHOLE_SIZE); wgpuRenderPassEncoderSetBindGroup(pass, 0, axis_bind_group_, 1, &slot_offset); wgpuRenderPassEncoderDraw(pass, 18, 1, 0, 0); wgpuRenderPassEncoderEnd(pass); wgpuRenderPassEncoderRelease(pass); } // ----------------------------------------------------------------------------- // Section plane visualizer // ----------------------------------------------------------------------------- bool OverlayRenderer::buildSectionVisualizer() { struct Seg { std::array s, e; std::array c; }; static constexpr std::array 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 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& 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(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 // ----------------------------------------------------------------------------- bool OverlayRenderer::buildMarquee() { struct Seg { std::array s, e; }; static const Seg segs[] = { { {0, 0}, {1, 0} }, { {1, 0}, {1, 1} }, { {1, 1}, {0, 1} }, { {0, 1}, {0, 0} }, }; std::vector verts; verts.reserve(std::size(segs) * 6 * 6); auto push_v = [&](const Seg& s, float t, float side) { verts.insert(verts.end(), { s.s[0], s.s[1], s.e[0], s.e[1], 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.marquee_vbo"); marquee_vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc); wgpuQueueWriteBuffer(queue_, marquee_vertex_buffer_, 0, verts.data(), verts.size() * sizeof(float)); } static const float fill_verts[] = { 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, }; { WGPUBufferDescriptor bdesc = {}; bdesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst; bdesc.size = sizeof(fill_verts); bdesc.label = svFromCStr("ifcviewer-wgpu.marquee_fill_vbo"); marquee_fill_vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc); wgpuQueueWriteBuffer(queue_, marquee_fill_vertex_buffer_, 0, fill_verts, sizeof(fill_verts)); } { WGPUBufferDescriptor bdesc = {}; bdesc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst; bdesc.size = 64; bdesc.label = svFromCStr("ifcviewer-wgpu.marquee_uniforms"); marquee_uniform_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc); } { WGPUBindGroupLayoutEntry entry = {}; entry.binding = 0; entry.visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment; entry.buffer.type = WGPUBufferBindingType_Uniform; entry.buffer.hasDynamicOffset = 0; entry.buffer.minBindingSize = 48; WGPUBindGroupLayoutDescriptor bgl_desc = {}; bgl_desc.entryCount = 1; bgl_desc.entries = &entry; bgl_desc.label = svFromCStr("ifcviewer-wgpu.marquee_bgl"); marquee_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc); } { WGPUPipelineLayoutDescriptor pl_desc = {}; pl_desc.bindGroupLayoutCount = 1; pl_desc.bindGroupLayouts = &marquee_bgl_; pl_desc.label = svFromCStr("ifcviewer-wgpu.marquee_pipeline_layout"); marquee_pipeline_layout_ = wgpuDeviceCreatePipelineLayout(device_, &pl_desc); } { WGPUBindGroupEntry entry = {}; entry.binding = 0; entry.buffer = marquee_uniform_buffer_; entry.offset = 0; entry.size = 64; WGPUBindGroupDescriptor bg_desc = {}; bg_desc.layout = marquee_bgl_; bg_desc.entryCount = 1; bg_desc.entries = &entry; bg_desc.label = svFromCStr("ifcviewer-wgpu.marquee_bind_group"); marquee_bind_group_ = wgpuDeviceCreateBindGroup(device_, &bg_desc); } { WGPUShaderSourceWGSL wgsl_src = {}; wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; wgsl_src.code = svFromCStr(MARQUEE_WGSL.c_str()); WGPUShaderModuleDescriptor sm_desc = {}; sm_desc.nextInChain = &wgsl_src.chain; sm_desc.label = svFromCStr("ifcviewer-wgpu.marquee_wgsl"); marquee_shader_module_ = wgpuDeviceCreateShaderModule(device_, &sm_desc); } // Vertex layout: start_uv(vec2) + end_uv(vec2) + t(f32) + side(f32), // stride 24. WGPUVertexAttribute attribs[4] = {}; attribs[0].format = WGPUVertexFormat_Float32x2; attribs[0].offset = 0; attribs[0].shaderLocation = 0; attribs[1].format = WGPUVertexFormat_Float32x2; attribs[1].offset = 8; attribs[1].shaderLocation = 1; attribs[2].format = WGPUVertexFormat_Float32; attribs[2].offset = 16; attribs[2].shaderLocation = 2; attribs[3].format = WGPUVertexFormat_Float32; attribs[3].offset = 20; attribs[3].shaderLocation = 3; WGPUVertexBufferLayout vbl = {}; vbl.arrayStride = 24; vbl.stepMode = WGPUVertexStepMode_Vertex; vbl.attributeCount = 4; 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 = surface_format_; ct.blend = &blend; ct.writeMask = WGPUColorWriteMask_All; WGPUFragmentState frag = {}; frag.module = marquee_shader_module_; frag.entryPoint = svFromCStr("fs_main"); frag.targetCount = 1; frag.targets = &ct; WGPURenderPipelineDescriptor rp_desc = {}; rp_desc.layout = marquee_pipeline_layout_; rp_desc.label = svFromCStr("ifcviewer-wgpu.marquee_pipeline"); rp_desc.vertex.module = marquee_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.primitive.topology = WGPUPrimitiveTopology_TriangleList; rp_desc.primitive.cullMode = WGPUCullMode_None; rp_desc.multisample.count = 1; rp_desc.multisample.mask = 0xFFFFFFFFu; marquee_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp_desc); WGPUVertexAttribute fill_attribs[1] = {}; fill_attribs[0].format = WGPUVertexFormat_Float32x2; fill_attribs[0].offset = 0; fill_attribs[0].shaderLocation = 0; WGPUVertexBufferLayout fill_vbl = {}; fill_vbl.arrayStride = 8; fill_vbl.stepMode = WGPUVertexStepMode_Vertex; fill_vbl.attributeCount = 1; fill_vbl.attributes = fill_attribs; WGPUFragmentState fill_frag = {}; fill_frag.module = marquee_shader_module_; fill_frag.entryPoint = svFromCStr("fs_fill"); fill_frag.targetCount = 1; fill_frag.targets = &ct; WGPURenderPipelineDescriptor fill_rp_desc = rp_desc; fill_rp_desc.label = svFromCStr("ifcviewer-wgpu.marquee_fill_pipeline"); fill_rp_desc.vertex.entryPoint = svFromCStr("vs_fill"); fill_rp_desc.vertex.bufferCount = 1; fill_rp_desc.vertex.buffers = &fill_vbl; fill_rp_desc.fragment = &fill_frag; marquee_fill_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &fill_rp_desc); return marquee_pipeline_ != nullptr && marquee_fill_pipeline_ != nullptr; } void OverlayRenderer::encodeMarquee(WGPUCommandEncoder enc, WGPUTextureView surface_view, const OverlayFrame& f, Eigen::Vector2i start_logical_px, Eigen::Vector2i current_logical_px, bool active) { if (!marquee_pipeline_ || !surface_view) return; if (!active) return; if (f.viewport_w_px <= 0 || f.viewport_h_px <= 0) return; const float w = float(f.viewport_w_px); const float h = float(f.viewport_h_px); const float dpr = float(std::max(1, f.device_pixel_ratio)); const float lx0 = float(std::min(start_logical_px.x(), current_logical_px.x())) * dpr; const float ly0 = float(std::min(start_logical_px.y(), current_logical_px.y())) * dpr; const float lx1 = float(std::max(start_logical_px.x(), current_logical_px.x())) * dpr; const float ly1 = float(std::max(start_logical_px.y(), current_logical_px.y())) * dpr; if (lx1 <= lx0 || ly1 <= ly0) return; const float nx0 = (lx0 / w) * 2.0f - 1.0f; const float nx1 = (lx1 / w) * 2.0f - 1.0f; const float ny_top = 1.0f - 2.0f * ly0 / h; const float ny_bottom = 1.0f - 2.0f * ly1 / h; // Bonsai decorator_color_special (axis +Z blue): 0.157, 0.565, 1.000. // Outline alpha 0.95; fill_alpha (multiplied onto that) gives ~0.19 // alpha for the translucent fill. float uniforms[16] = {}; uniforms[0] = nx0; uniforms[1] = ny_top; uniforms[2] = nx1; uniforms[3] = ny_bottom; uniforms[4] = 0.157f; uniforms[5] = 0.565f; uniforms[6] = 1.000f; uniforms[7] = 0.95f; uniforms[8] = w; uniforms[9] = h; uniforms[10] = 3.0f * dpr; uniforms[11] = 0.20f; wgpuQueueWriteBuffer(queue_, marquee_uniform_buffer_, 0, uniforms, 12 * sizeof(float)); WGPURenderPassColorAttachment color = {}; color.view = surface_view; color.loadOp = WGPULoadOp_Load; color.storeOp = WGPUStoreOp_Store; color.clearValue = { 0, 0, 0, 1 }; color.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; WGPURenderPassDescriptor pass_desc = {}; pass_desc.colorAttachmentCount = 1; pass_desc.colorAttachments = &color; pass_desc.label = svFromCStr("ifcviewer-wgpu.marquee_pass"); WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(enc, &pass_desc); wgpuRenderPassEncoderSetBindGroup(pass, 0, marquee_bind_group_, 0, nullptr); wgpuRenderPassEncoderSetPipeline(pass, marquee_fill_pipeline_); wgpuRenderPassEncoderSetVertexBuffer(pass, 0, marquee_fill_vertex_buffer_, 0, WGPU_WHOLE_SIZE); wgpuRenderPassEncoderDraw(pass, 6, 1, 0, 0); wgpuRenderPassEncoderSetPipeline(pass, marquee_pipeline_); wgpuRenderPassEncoderSetVertexBuffer(pass, 0, marquee_vertex_buffer_, 0, WGPU_WHOLE_SIZE); wgpuRenderPassEncoderDraw(pass, 24, 1, 0, 0); wgpuRenderPassEncoderEnd(pass); wgpuRenderPassEncoderRelease(pass); } // ----------------------------------------------------------------------------- // Overlay lines // ----------------------------------------------------------------------------- bool OverlayRenderer::buildOverlayLines() { // Empty initial buffers — both grow on demand inside setOverlayLines. // Use a tiny starter capacity so the very first set call doesn't have // to special-case "buffer is null." { WGPUBufferDescriptor bdesc = {}; bdesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst; bdesc.size = 256; bdesc.label = svFromCStr("ifcviewer-wgpu.overlay_line_vbo"); overlay_line_vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc); overlay_line_vertex_capacity_ = 256; } { WGPUBufferDescriptor bdesc = {}; bdesc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst; bdesc.size = kOverlayLineUniformSlotSize; bdesc.label = svFromCStr("ifcviewer-wgpu.overlay_line_uniforms"); overlay_line_uniform_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc); overlay_line_uniform_slots_ = 1; } { WGPUBindGroupLayoutEntry entry = {}; entry.binding = 0; entry.visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment; entry.buffer.type = WGPUBufferBindingType_Uniform; entry.buffer.hasDynamicOffset = 1; entry.buffer.minBindingSize = 128; WGPUBindGroupLayoutDescriptor bgl_desc = {}; bgl_desc.entryCount = 1; bgl_desc.entries = &entry; bgl_desc.label = svFromCStr("ifcviewer-wgpu.overlay_line_bgl"); overlay_line_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc); } { WGPUPipelineLayoutDescriptor pl_desc = {}; pl_desc.bindGroupLayoutCount = 1; pl_desc.bindGroupLayouts = &overlay_line_bgl_; pl_desc.label = svFromCStr("ifcviewer-wgpu.overlay_line_pipeline_layout"); overlay_line_pipeline_layout_ = wgpuDeviceCreatePipelineLayout(device_, &pl_desc); } { WGPUBindGroupEntry entry = {}; entry.binding = 0; entry.buffer = overlay_line_uniform_buffer_; entry.offset = 0; entry.size = 128; WGPUBindGroupDescriptor bg_desc = {}; bg_desc.layout = overlay_line_bgl_; bg_desc.entryCount = 1; bg_desc.entries = &entry; bg_desc.label = svFromCStr("ifcviewer-wgpu.overlay_line_bind_group"); overlay_line_bind_group_ = wgpuDeviceCreateBindGroup(device_, &bg_desc); } { WGPUShaderSourceWGSL wgsl_src = {}; wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; wgsl_src.code = svFromCStr(OVERLAY_LINES_WGSL); WGPUShaderModuleDescriptor sm_desc = {}; sm_desc.nextInChain = &wgsl_src.chain; sm_desc.label = svFromCStr("ifcviewer-wgpu.overlay_line_wgsl"); overlay_line_shader_module_ = wgpuDeviceCreateShaderModule(device_, &sm_desc); } // Per-vertex layout: (a.xyz, b.xyz, side, along) = 8 floats = 32 bytes. WGPUVertexAttribute attribs[4] = {}; 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_Float32; attribs[2].offset = 24; attribs[2].shaderLocation = 2; attribs[3].format = WGPUVertexFormat_Float32; attribs[3].offset = 28; attribs[3].shaderLocation = 3; WGPUVertexBufferLayout vbl = {}; vbl.arrayStride = 32; vbl.stepMode = WGPUVertexStepMode_Vertex; vbl.attributeCount = 4; 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 = surface_format_; ct.blend = &blend; ct.writeMask = WGPUColorWriteMask_All; WGPUFragmentState frag = {}; frag.module = overlay_line_shader_module_; frag.entryPoint = svFromCStr("fs_main"); frag.targetCount = 1; frag.targets = &ct; // Depth-tested against the main MSAA depth so lines correctly hide // behind geometry; depth-write off so they don't occlude later // overlays. 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 = overlay_line_pipeline_layout_; rp_desc.label = svFromCStr("ifcviewer-wgpu.overlay_line_pipeline"); rp_desc.vertex.module = overlay_line_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; overlay_line_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp_desc); return overlay_line_pipeline_ != nullptr; } void OverlayRenderer::setOverlayLines(const std::vector& groups) { overlay_line_draws_.clear(); if (groups.empty()) return; // Per-segment expansion: 6 vertices × 8 floats = 48 floats per segment. // Each vertex carries (a.xyz, b.xyz, side, along) where (side, along) // selects one of six fixed corners of the screen-space quad. static const float CORNERS[6][2] = { {-1.0f, 0.0f}, {+1.0f, 0.0f}, {-1.0f, 1.0f}, {-1.0f, 1.0f}, {+1.0f, 0.0f}, {+1.0f, 1.0f}, }; std::vector verts; uint32_t first_vertex = 0; overlay_line_draws_.reserve(groups.size()); for (const auto& g : groups) { if (g.world_xyz.size() < 6) { overlay_line_draws_.push_back({first_vertex, 0}); continue; } const size_t n_segs = g.world_xyz.size() / 6; const uint32_t group_vcount = uint32_t(n_segs) * 6; verts.reserve(verts.size() + size_t(group_vcount) * 8); for (size_t s = 0; s < n_segs; ++s) { const float* a = &g.world_xyz[s * 6 + 0]; const float* b = &g.world_xyz[s * 6 + 3]; for (int c = 0; c < 6; ++c) { verts.push_back(a[0]); verts.push_back(a[1]); verts.push_back(a[2]); verts.push_back(b[0]); verts.push_back(b[1]); verts.push_back(b[2]); verts.push_back(CORNERS[c][0]); verts.push_back(CORNERS[c][1]); } } overlay_line_draws_.push_back({first_vertex, group_vcount}); first_vertex += group_vcount; } // Grow vertex buffer if needed (1.5× headroom so steady-state setters // don't re-allocate every frame). const uint64_t bytes = uint64_t(verts.size()) * sizeof(float); if (bytes > overlay_line_vertex_capacity_) { const uint64_t new_cap = bytes + bytes / 2; if (overlay_line_vertex_buffer_) { wgpuBufferRelease(overlay_line_vertex_buffer_); } WGPUBufferDescriptor bdesc = {}; bdesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst; bdesc.size = new_cap; bdesc.label = svFromCStr("ifcviewer-wgpu.overlay_line_vbo"); overlay_line_vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc); overlay_line_vertex_capacity_ = new_cap; } if (bytes > 0) { wgpuQueueWriteBuffer(queue_, overlay_line_vertex_buffer_, 0, verts.data(), size_t(bytes)); } // Grow uniform buffer to one 256-byte slot per group; re-create the // bind group on each grow so the dynamic-offset stride still binds // exactly 128 bytes per group (the WGSL struct size). if (uint32_t(groups.size()) > overlay_line_uniform_slots_) { const uint32_t new_slots = uint32_t(groups.size()); if (overlay_line_uniform_buffer_) { wgpuBufferRelease(overlay_line_uniform_buffer_); } WGPUBufferDescriptor bdesc = {}; bdesc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst; bdesc.size = uint64_t(new_slots) * kOverlayLineUniformSlotSize; bdesc.label = svFromCStr("ifcviewer-wgpu.overlay_line_uniforms"); overlay_line_uniform_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc); overlay_line_uniform_slots_ = new_slots; if (overlay_line_bind_group_) { wgpuBindGroupRelease(overlay_line_bind_group_); } WGPUBindGroupEntry entry = {}; entry.binding = 0; entry.buffer = overlay_line_uniform_buffer_; entry.offset = 0; entry.size = 128; WGPUBindGroupDescriptor bg_desc = {}; bg_desc.layout = overlay_line_bgl_; bg_desc.entryCount = 1; bg_desc.entries = &entry; bg_desc.label = svFromCStr("ifcviewer-wgpu.overlay_line_bind_group"); overlay_line_bind_group_ = wgpuDeviceCreateBindGroup(device_, &bg_desc); } // Pack each group's static slot tail (everything after the per-frame // view_proj). Layout matches WGSL LineUniforms (see OVERLAY_LINES_WGSL): // [ 0..64) view_proj (written per-frame) // [ 64..80) inner_color // [ 80..96) stroke_color // [ 96..104) viewport_size (written per-frame) // [104..108) line_width_px // [108..112) stroke_extra // [112..116) dash_period_px // [116..120) dash_on_ratio for (size_t i = 0; i < groups.size(); ++i) { const auto& g = groups[i]; const uint64_t slot_off = uint64_t(i) * kOverlayLineUniformSlotSize; uint8_t slot_tail[56] = {}; // bytes [64..120) std::memcpy(slot_tail + 0, g.color, 16); // 64.. 80 std::memcpy(slot_tail + 16, g.stroke_color, 16); // 80.. 96 // viewport_size occupies [96..104) — written per-frame. std::memcpy(slot_tail + 40, &g.line_width, 4); // 104..108 std::memcpy(slot_tail + 44, &g.stroke_extra, 4); // 108..112 std::memcpy(slot_tail + 48, &g.dash_period_px, 4); // 112..116 std::memcpy(slot_tail + 52, &g.dash_on_ratio, 4); // 116..120 wgpuQueueWriteBuffer(queue_, overlay_line_uniform_buffer_, slot_off + 64, slot_tail, sizeof(slot_tail)); } } void OverlayRenderer::encodeOverlayLines(WGPURenderPassEncoder pass, const OverlayFrame& f) { if (!overlay_line_pipeline_ || overlay_line_draws_.empty()) return; if (f.viewport_w_px <= 0 || f.viewport_h_px <= 0) return; // Per-frame slot prefix: view_proj (64 B) into [0..64), viewport_size // (8 B) into [96..104). The static [64..96) and [104..120) ranges were // filled by setOverlayLines so we don't touch them again. float vp[16]; std::memcpy(vp, f.view_proj.data(), sizeof(vp)); const float viewport[2] = { float(f.viewport_w_px), float(f.viewport_h_px) }; wgpuRenderPassEncoderSetPipeline(pass, overlay_line_pipeline_); wgpuRenderPassEncoderSetVertexBuffer(pass, 0, overlay_line_vertex_buffer_, 0, WGPU_WHOLE_SIZE); for (size_t i = 0; i < overlay_line_draws_.size(); ++i) { const auto& d = overlay_line_draws_[i]; if (d.vertex_count == 0) continue; const uint64_t slot_off = uint64_t(i) * kOverlayLineUniformSlotSize; wgpuQueueWriteBuffer(queue_, overlay_line_uniform_buffer_, slot_off + 0, vp, sizeof(vp)); wgpuQueueWriteBuffer(queue_, overlay_line_uniform_buffer_, slot_off + 96, viewport, sizeof(viewport)); const uint32_t dynamic_offsets[1] = { uint32_t(slot_off) }; wgpuRenderPassEncoderSetBindGroup(pass, 0, overlay_line_bind_group_, 1, dynamic_offsets); wgpuRenderPassEncoderDraw(pass, d.vertex_count, 1, d.first_vertex, 0); } } // ----------------------------------------------------------------------------- // Overlay points // ----------------------------------------------------------------------------- bool OverlayRenderer::buildOverlayPoints() { { WGPUBufferDescriptor bdesc = {}; bdesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst; bdesc.size = 256; bdesc.label = svFromCStr("ifcviewer-wgpu.overlay_point_vbo"); overlay_point_vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc); overlay_point_vertex_capacity_ = 256; } { // WGSL PointUniforms struct size: mat4(64) + 2×vec4(32) + vec2(8) + // 2×f32(8) = 112 B; struct rounds up to 128 (alignOf == 16). WGPUBufferDescriptor bdesc = {}; bdesc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst; bdesc.size = 128; bdesc.label = svFromCStr("ifcviewer-wgpu.overlay_point_uniforms"); overlay_point_uniform_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc); } { WGPUBindGroupLayoutEntry entry = {}; entry.binding = 0; entry.visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment; entry.buffer.type = WGPUBufferBindingType_Uniform; entry.buffer.minBindingSize = 128; WGPUBindGroupLayoutDescriptor bgl_desc = {}; bgl_desc.entryCount = 1; bgl_desc.entries = &entry; bgl_desc.label = svFromCStr("ifcviewer-wgpu.overlay_point_bgl"); overlay_point_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc); } { WGPUPipelineLayoutDescriptor pl_desc = {}; pl_desc.bindGroupLayoutCount = 1; pl_desc.bindGroupLayouts = &overlay_point_bgl_; pl_desc.label = svFromCStr("ifcviewer-wgpu.overlay_point_pipeline_layout"); overlay_point_pipeline_layout_ = wgpuDeviceCreatePipelineLayout(device_, &pl_desc); } { WGPUBindGroupEntry entry = {}; entry.binding = 0; entry.buffer = overlay_point_uniform_buffer_; entry.offset = 0; entry.size = 128; WGPUBindGroupDescriptor bg_desc = {}; bg_desc.layout = overlay_point_bgl_; bg_desc.entryCount = 1; bg_desc.entries = &entry; bg_desc.label = svFromCStr("ifcviewer-wgpu.overlay_point_bind_group"); overlay_point_bind_group_ = wgpuDeviceCreateBindGroup(device_, &bg_desc); } { WGPUShaderSourceWGSL wgsl_src = {}; wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; wgsl_src.code = svFromCStr(OVERLAY_POINTS_WGSL); WGPUShaderModuleDescriptor sm_desc = {}; sm_desc.nextInChain = &wgsl_src.chain; sm_desc.label = svFromCStr("ifcviewer-wgpu.overlay_point_wgsl"); overlay_point_shader_module_ = wgpuDeviceCreateShaderModule(device_, &sm_desc); } // Per-vertex layout: (world_pos.xyz, corner.xy) = 5 floats = 20 bytes. WGPUVertexAttribute attribs[2] = {}; attribs[0].format = WGPUVertexFormat_Float32x3; attribs[0].offset = 0; attribs[0].shaderLocation = 0; attribs[1].format = WGPUVertexFormat_Float32x2; attribs[1].offset = 12; attribs[1].shaderLocation = 1; WGPUVertexBufferLayout vbl = {}; vbl.arrayStride = 20; vbl.stepMode = WGPUVertexStepMode_Vertex; vbl.attributeCount = 2; 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 = surface_format_; ct.blend = &blend; ct.writeMask = WGPUColorWriteMask_All; WGPUFragmentState frag = {}; frag.module = overlay_point_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 = overlay_point_pipeline_layout_; rp_desc.label = svFromCStr("ifcviewer-wgpu.overlay_point_pipeline"); rp_desc.vertex.module = overlay_point_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; overlay_point_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp_desc); return overlay_point_pipeline_ != nullptr; } void OverlayRenderer::setOverlayPoints(const std::vector& world_xyz, float r, float g, float b, float a, float pixel_size, float stroke_r, float stroke_g, float stroke_b, float stroke_a, float stroke_extra) { overlay_point_vertex_count_ = 0; if (world_xyz.size() < 3 || pixel_size <= 0.0f) return; const size_t n_pts = world_xyz.size() / 3; // Six vertices per point (two triangles), 5 floats each. static const float CORNERS[6][2] = { {-1.0f, -1.0f}, {+1.0f, -1.0f}, {-1.0f, +1.0f}, {-1.0f, +1.0f}, {+1.0f, -1.0f}, {+1.0f, +1.0f}, }; std::vector verts; verts.reserve(n_pts * 6 * 5); for (size_t p = 0; p < n_pts; ++p) { const float* w = &world_xyz[p * 3]; for (int c = 0; c < 6; ++c) { verts.push_back(w[0]); verts.push_back(w[1]); verts.push_back(w[2]); verts.push_back(CORNERS[c][0]); verts.push_back(CORNERS[c][1]); } } overlay_point_vertex_count_ = uint32_t(n_pts) * 6; const uint64_t bytes = uint64_t(verts.size()) * sizeof(float); if (bytes > overlay_point_vertex_capacity_) { const uint64_t new_cap = bytes + bytes / 2; if (overlay_point_vertex_buffer_) { wgpuBufferRelease(overlay_point_vertex_buffer_); } WGPUBufferDescriptor bdesc = {}; bdesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst; bdesc.size = new_cap; bdesc.label = svFromCStr("ifcviewer-wgpu.overlay_point_vbo"); overlay_point_vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc); overlay_point_vertex_capacity_ = new_cap; } wgpuQueueWriteBuffer(queue_, overlay_point_vertex_buffer_, 0, verts.data(), size_t(bytes)); // Pack the [64..120) tail of the uniform slot (inner/stroke + sprite // geometry — view_proj and viewport_size are written per-frame in // encodeOverlayPoints). Slot layout matches WGSL PointUniforms: // [ 0..64) view_proj (per-frame) // [ 64..80) inner_color // [ 80..96) stroke_color // [ 96..104) viewport_size (per-frame) // [104..108) total_half_px // [108..112) inner_radius_norm // pixel_size is the inner full diameter; total diameter = pixel_size // + 2*stroke_extra; inner_radius_norm = inner_radius / total_half. const float total_diam = pixel_size + 2.0f * stroke_extra; const float total_half = total_diam * 0.5f; const float inner_radius = pixel_size * 0.5f; const float inner_norm = (total_half > 1e-6f) ? (inner_radius / total_half) : 1.0f; uint8_t slot_tail[44] = {}; const float inner_rgba [4] = { r, g, b, a }; const float stroke_rgba[4] = { stroke_r, stroke_g, stroke_b, stroke_a }; std::memcpy(slot_tail + 0, inner_rgba, 16); std::memcpy(slot_tail + 16, stroke_rgba, 16); std::memcpy(slot_tail + 40, &total_half, 4); // inner_radius_norm sits at slot offset 108 → tail offset 44, but the // tail above only spans [64..108). The norm goes in its own write. wgpuQueueWriteBuffer(queue_, overlay_point_uniform_buffer_, 64, slot_tail, sizeof(slot_tail)); wgpuQueueWriteBuffer(queue_, overlay_point_uniform_buffer_, 108, &inner_norm, sizeof(inner_norm)); } void OverlayRenderer::encodeOverlayPoints(WGPURenderPassEncoder pass, const OverlayFrame& f) { if (!overlay_point_pipeline_ || overlay_point_vertex_count_ == 0) return; if (f.viewport_w_px <= 0 || f.viewport_h_px <= 0) return; float vp[16]; std::memcpy(vp, f.view_proj.data(), sizeof(vp)); const float viewport[2] = { float(f.viewport_w_px), float(f.viewport_h_px) }; wgpuQueueWriteBuffer(queue_, overlay_point_uniform_buffer_, 0, vp, sizeof(vp)); wgpuQueueWriteBuffer(queue_, overlay_point_uniform_buffer_, 96, viewport, sizeof(viewport)); wgpuRenderPassEncoderSetPipeline(pass, overlay_point_pipeline_); wgpuRenderPassEncoderSetBindGroup(pass, 0, overlay_point_bind_group_, 0, nullptr); wgpuRenderPassEncoderSetVertexBuffer(pass, 0, overlay_point_vertex_buffer_, 0, WGPU_WHOLE_SIZE); wgpuRenderPassEncoderDraw(pass, overlay_point_vertex_count_, 1, 0, 0); } // ----------------------------------------------------------------------------- // Highlight triangles (translucent world-space triangle list) // ----------------------------------------------------------------------------- bool OverlayRenderer::buildHighlightTriangles() { { WGPUBufferDescriptor bdesc = {}; bdesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst; bdesc.size = 256; // grows in setHighlightTriangles bdesc.label = svFromCStr("ifcviewer-wgpu.highlight_vbo"); highlight_vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc); highlight_vertex_capacity_ = 256; } { // WGSL HiUniforms: mat4(64) + vec4(16) = 80 B; struct rounds up // to 16-multiple = 80 B already. Allocate 256 for slack. WGPUBufferDescriptor bdesc = {}; bdesc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst; bdesc.size = 256; bdesc.label = svFromCStr("ifcviewer-wgpu.highlight_uniforms"); highlight_uniform_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc); } { WGPUBindGroupLayoutEntry entry = {}; entry.binding = 0; entry.visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment; entry.buffer.type = WGPUBufferBindingType_Uniform; entry.buffer.minBindingSize = 80; WGPUBindGroupLayoutDescriptor bgl_desc = {}; bgl_desc.entryCount = 1; bgl_desc.entries = &entry; bgl_desc.label = svFromCStr("ifcviewer-wgpu.highlight_bgl"); highlight_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc); } { WGPUPipelineLayoutDescriptor pl_desc = {}; pl_desc.bindGroupLayoutCount = 1; pl_desc.bindGroupLayouts = &highlight_bgl_; pl_desc.label = svFromCStr("ifcviewer-wgpu.highlight_pipeline_layout"); highlight_pipeline_layout_ = wgpuDeviceCreatePipelineLayout(device_, &pl_desc); } { WGPUBindGroupEntry entry = {}; entry.binding = 0; entry.buffer = highlight_uniform_buffer_; entry.offset = 0; entry.size = 80; WGPUBindGroupDescriptor bg_desc = {}; bg_desc.layout = highlight_bgl_; bg_desc.entryCount = 1; bg_desc.entries = &entry; bg_desc.label = svFromCStr("ifcviewer-wgpu.highlight_bind_group"); highlight_bind_group_ = wgpuDeviceCreateBindGroup(device_, &bg_desc); } { WGPUShaderSourceWGSL wgsl_src = {}; wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; wgsl_src.code = svFromCStr(HIGHLIGHT_TRIANGLES_WGSL); WGPUShaderModuleDescriptor sm_desc = {}; sm_desc.nextInChain = &wgsl_src.chain; sm_desc.label = svFromCStr("ifcviewer-wgpu.highlight_wgsl"); highlight_shader_module_ = wgpuDeviceCreateShaderModule(device_, &sm_desc); } WGPUVertexAttribute attribs[1] = {}; attribs[0].format = WGPUVertexFormat_Float32x3; attribs[0].offset = 0; attribs[0].shaderLocation = 0; WGPUVertexBufferLayout vbl = {}; vbl.arrayStride = 12; vbl.stepMode = WGPUVertexStepMode_Vertex; vbl.attributeCount = 1; 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 = surface_format_; ct.blend = &blend; ct.writeMask = WGPUColorWriteMask_All; WGPUFragmentState frag = {}; frag.module = highlight_shader_module_; frag.entryPoint = svFromCStr("fs_main"); frag.targetCount = 1; frag.targets = &ct; // Depth-tested but no depth-write — patches sit behind closer // geometry but later overlays (axis gizmo, labels) still draw over. 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 = highlight_pipeline_layout_; rp_desc.label = svFromCStr("ifcviewer-wgpu.highlight_pipeline"); rp_desc.vertex.module = highlight_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; highlight_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp_desc); return highlight_pipeline_ != nullptr; } void OverlayRenderer::setHighlightTriangles( const std::vector& world_xyz, float r, float g, float b, float a) { highlight_color_[0] = r; highlight_color_[1] = g; highlight_color_[2] = b; highlight_color_[3] = a; const size_t n_floats = world_xyz.size(); if (n_floats < 9 || (n_floats % 9) != 0 || a <= 0.0f) { highlight_vertex_count_ = 0; return; } const uint64_t bytes = uint64_t(n_floats) * sizeof(float); if (bytes > highlight_vertex_capacity_) { const uint64_t new_cap = bytes + bytes / 2; if (highlight_vertex_buffer_) wgpuBufferRelease(highlight_vertex_buffer_); WGPUBufferDescriptor bdesc = {}; bdesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst; bdesc.size = new_cap; bdesc.label = svFromCStr("ifcviewer-wgpu.highlight_vbo"); highlight_vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc); highlight_vertex_capacity_ = new_cap; } wgpuQueueWriteBuffer(queue_, highlight_vertex_buffer_, 0, world_xyz.data(), size_t(bytes)); highlight_vertex_count_ = uint32_t(n_floats / 3); } void OverlayRenderer::encodeHighlightTriangles(WGPURenderPassEncoder pass, const OverlayFrame& f) { if (!highlight_pipeline_ || highlight_vertex_count_ == 0) return; // Pack mat4 + vec4 into the slot. mat4 is column-major 16 floats. uint8_t slot[80] = {}; std::memcpy(slot, f.view_proj.data(), 16 * sizeof(float)); std::memcpy(slot + 64, highlight_color_, 4 * sizeof(float)); wgpuQueueWriteBuffer(queue_, highlight_uniform_buffer_, 0, slot, sizeof(slot)); wgpuRenderPassEncoderSetPipeline(pass, highlight_pipeline_); wgpuRenderPassEncoderSetBindGroup(pass, 0, highlight_bind_group_, 0, nullptr); wgpuRenderPassEncoderSetVertexBuffer(pass, 0, highlight_vertex_buffer_, 0, WGPU_WHOLE_SIZE); wgpuRenderPassEncoderDraw(pass, highlight_vertex_count_, 1, 0, 0); } // ----------------------------------------------------------------------------- // Labels + HUD text (textured quads, content-cached) // ----------------------------------------------------------------------------- bool OverlayRenderer::buildLabels() { { WGPUSamplerDescriptor sd = {}; sd.minFilter = WGPUFilterMode_Linear; sd.magFilter = WGPUFilterMode_Linear; sd.mipmapFilter = WGPUMipmapFilterMode_Nearest; sd.addressModeU = WGPUAddressMode_ClampToEdge; sd.addressModeV = WGPUAddressMode_ClampToEdge; sd.addressModeW = WGPUAddressMode_ClampToEdge; sd.lodMinClamp = 0.0f; sd.lodMaxClamp = 0.0f; sd.maxAnisotropy = 1; sd.label = svFromCStr("ifcviewer-wgpu.label_sampler"); label_sampler_ = wgpuDeviceCreateSampler(device_, &sd); } { WGPUBindGroupLayoutEntry entries[2] = {}; entries[0].binding = 0; entries[0].visibility = WGPUShaderStage_Fragment; entries[0].sampler.type = WGPUSamplerBindingType_Filtering; entries[1].binding = 1; entries[1].visibility = WGPUShaderStage_Fragment; entries[1].texture.sampleType = WGPUTextureSampleType_Float; entries[1].texture.viewDimension = WGPUTextureViewDimension_2D; WGPUBindGroupLayoutDescriptor bgl_desc = {}; bgl_desc.entryCount = 2; bgl_desc.entries = entries; bgl_desc.label = svFromCStr("ifcviewer-wgpu.label_bgl"); label_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc); } { WGPUPipelineLayoutDescriptor pl_desc = {}; pl_desc.bindGroupLayoutCount = 1; pl_desc.bindGroupLayouts = &label_bgl_; pl_desc.label = svFromCStr("ifcviewer-wgpu.label_pipeline_layout"); label_pipeline_layout_ = wgpuDeviceCreatePipelineLayout(device_, &pl_desc); } { WGPUShaderSourceWGSL wgsl_src = {}; wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; wgsl_src.code = svFromCStr(LABELS_WGSL); WGPUShaderModuleDescriptor sm_desc = {}; sm_desc.nextInChain = &wgsl_src.chain; sm_desc.label = svFromCStr("ifcviewer-wgpu.label_wgsl"); label_shader_module_ = wgpuDeviceCreateShaderModule(device_, &sm_desc); } { WGPUBufferDescriptor bdesc = {}; bdesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst; bdesc.size = 256; bdesc.label = svFromCStr("ifcviewer-wgpu.label_vbo"); label_vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc); label_vertex_capacity_ = 256; } // Per-vertex: vec2 NDC + vec2 uv = 16 B stride. WGPUVertexAttribute attribs[2] = {}; attribs[0].format = WGPUVertexFormat_Float32x2; attribs[0].offset = 0; attribs[0].shaderLocation = 0; attribs[1].format = WGPUVertexFormat_Float32x2; attribs[1].offset = 8; attribs[1].shaderLocation = 1; WGPUVertexBufferLayout vbl = {}; vbl.arrayStride = 16; vbl.stepMode = WGPUVertexStepMode_Vertex; vbl.attributeCount = 2; 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 = surface_format_; ct.blend = &blend; ct.writeMask = WGPUColorWriteMask_All; WGPUFragmentState frag = {}; frag.module = label_shader_module_; frag.entryPoint = svFromCStr("fs_main"); frag.targetCount = 1; frag.targets = &ct; WGPURenderPipelineDescriptor rp_desc = {}; rp_desc.layout = label_pipeline_layout_; rp_desc.label = svFromCStr("ifcviewer-wgpu.label_pipeline"); rp_desc.vertex.module = label_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.primitive.topology = WGPUPrimitiveTopology_TriangleList; rp_desc.primitive.cullMode = WGPUCullMode_None; rp_desc.multisample.count = 1; rp_desc.multisample.mask = 0xFFFFFFFFu; label_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp_desc); return label_pipeline_ != nullptr; } void OverlayRenderer::releaseLabelTextures() { for (auto it = label_tex_cache_.begin(); it != label_tex_cache_.end(); ++it) { if (it.value().bind_group) wgpuBindGroupRelease(it.value().bind_group); if (it.value().view) wgpuTextureViewRelease(it.value().view); if (it.value().texture) wgpuTextureRelease(it.value().texture); } label_tex_cache_.clear(); } void OverlayRenderer::setOverlayLabels(const std::vector