diff --git a/src/ifcviewer-wgpu/WgpuOverlayRenderer.cpp b/src/ifcviewer-wgpu/WgpuOverlayRenderer.cpp new file mode 100644 index 0000000000..2ba209a56b --- /dev/null +++ b/src/ifcviewer-wgpu/WgpuOverlayRenderer.cpp @@ -0,0 +1,1027 @@ +/******************************************************************************** + * * + * 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 "WgpuOverlayRenderer.h" + +#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 QMatrix4x4& mvp, const QVector3D& origin, + float arm, float alpha, float line_width_px, + float viewport_w, float viewport_h) { + std::memset(dst, 0, 256); + std::memcpy(dst, mvp.constData(), 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 QMatrix4x4& mvp, + const QVector3D& origin, float half_size, + const QVector3D& tangent, float line_width_px, + const QVector3D& bitangent, + const QVector3D& normal, + float r, float g, float b, float a, + float viewport_w, float viewport_h) { + std::memset(dst, 0, 256); + std::memcpy(dst, mvp.constData(), 16 * sizeof(float)); + auto put_vec3_pad = [&](size_t off, const QVector3D& 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 +// ----------------------------------------------------------------------------- + +#define 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 char* AXIS_WGSL = 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 char* SECTION_WGSL = 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 char* MARQUEE_WGSL = 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"; + +// ----------------------------------------------------------------------------- +// Construction / destruction +// ----------------------------------------------------------------------------- + +WgpuOverlayRenderer::~WgpuOverlayRenderer() { + destroy(); +} + +bool WgpuOverlayRenderer::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; + return true; +} + +void WgpuOverlayRenderer::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; } +} + +// ----------------------------------------------------------------------------- +// Axis indicator +// ----------------------------------------------------------------------------- + +bool WgpuOverlayRenderer::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); + 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 WgpuOverlayRenderer::encodePivot(WGPURenderPassEncoder pass, + const WgpuOverlayFrame& 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 WgpuOverlayRenderer::encodeCornerAxis(WGPUCommandEncoder enc, + WGPUTextureView surface_view, + const WgpuOverlayFrame& 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 QVector3D eye_dir(std::cos(pitch_rad) * std::cos(yaw_rad), + std::cos(pitch_rad) * std::sin(yaw_rad), + std::sin(pitch_rad)); + const QVector3D world_up = (std::abs(f.camera_pitch_deg) >= 89.0f) + ? QVector3D(0.0f, 1.0f, 0.0f) + : QVector3D(0.0f, 0.0f, 1.0f); + QMatrix4x4 gv; + gv.lookAt(eye_dir * 3.0f, QVector3D(0, 0, 0), world_up); + QMatrix4x4 gp; + gp.ortho(-1.4f, 1.4f, -1.4f, 1.4f, 0.1f, 10.0f); + QMatrix4x4 z_remap; + z_remap(2, 2) = 0.5f; + z_remap(2, 3) = 0.5f; + const QMatrix4x4 mvp = z_remap * gp * gv; + + uint8_t slot[256]; + const float line_w = 2.5f * float(dpr); + packAxisUniform(slot, mvp, QVector3D(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 WgpuOverlayRenderer::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); + 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 WgpuOverlayRenderer::encodeSectionGizmos(WGPURenderPassEncoder pass, + const WgpuOverlayFrame& 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 WgpuSectionPlane& 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. + QVector3D nn = p.n.normalized(); + const float ax = std::abs(nn.x()), ay = std::abs(nn.y()), az = std::abs(nn.z()); + QVector3D seed = (ax < ay && ax < az) ? QVector3D(1, 0, 0) + : (ay < az) ? QVector3D(0, 1, 0) + : QVector3D(0, 0, 1); + QVector3D tangent = QVector3D::crossProduct(nn, seed); + if (tangent.lengthSquared() < 1e-12f) tangent = QVector3D(1, 0, 0); + tangent.normalize(); + QVector3D bitangent = QVector3D::crossProduct(nn, 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 WgpuOverlayRenderer::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); + 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 WgpuOverlayRenderer::encodeMarquee(WGPUCommandEncoder enc, + WGPUTextureView surface_view, + const WgpuOverlayFrame& f, + QPoint start_logical_px, + QPoint 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); +} diff --git a/src/ifcviewer-wgpu/WgpuOverlayRenderer.h b/src/ifcviewer-wgpu/WgpuOverlayRenderer.h new file mode 100644 index 0000000000..9412791328 --- /dev/null +++ b/src/ifcviewer-wgpu/WgpuOverlayRenderer.h @@ -0,0 +1,166 @@ +/******************************************************************************** + * * + * 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 . * + * * + ********************************************************************************/ + +#ifndef WGPUOVERLAYRENDERER_H +#define WGPUOVERLAYRENDERER_H + +#include +#include +#include + +#include + +#include +#include + +// Per-frame snapshot of viewport state that every overlay needs. Built once +// at the top of render() and passed by const-ref to each encodeX() call so +// the overlay renderer never reaches back into the viewport. +struct WgpuOverlayFrame { + QMatrix4x4 view_proj; + QVector3D camera_target; + float camera_distance = 5.0f; + float camera_yaw_deg = 0.0f; + float camera_pitch_deg = 0.0f; + float camera_fov_y_deg = 45.0f; + int viewport_w_px = 0; + int viewport_h_px = 0; + int device_pixel_ratio = 1; +}; + +// One section plane as the visualizer consumes it. The viewport owns the +// authoritative state vector (the section tool mutates it); the overlay +// reads from a non-owning span every frame. Held by value because the +// struct is small and copies happen at most six times per frame. +struct WgpuSectionPlane { + QVector3D n; // unit normal (camera-facing after auto-flip) + float d; // -dot(n, origin) + QVector3D origin; // surface point at the moment the plane was added + float visual_radius; // unused by the visualizer (kept here so the + // section tool's state struct round-trips + // through this overlay-facing definition). +}; + +// All viewport overlays in one place: axis indicator (corner + pivot), +// section plane gizmos, and the marquee drag rect. Mirrors GL's +// OverlayRenderer split so WgpuViewportWindow.cpp doesn't have to +// carry ~1.5k lines of pipeline plumbing. +// +// Lifecycle: init() once after the device is up, destroy() before the +// device dies. Pipelines are immutable after init; only per-frame +// uniforms get re-written. +// +// Threading: all calls are main-thread only — they touch the wgpu queue. +class WgpuOverlayRenderer { +public: + WgpuOverlayRenderer() = default; + ~WgpuOverlayRenderer(); + + WgpuOverlayRenderer(const WgpuOverlayRenderer&) = delete; + WgpuOverlayRenderer& operator=(const WgpuOverlayRenderer&) = delete; + + bool init(WGPUInstance instance, WGPUDevice device, WGPUQueue queue, + WGPUTextureFormat surface_format, int sample_count); + void destroy(); + + // ---- Inside the main MSAA pass, after geometry ---- + // Both share depth with the scene so they're correctly occluded. + + // Orbit pivot indicator. `visible` is the viewport's UI gate (orbit + // drag / wheel-zoom afterglow). When false this is a cheap no-op. + void encodePivot(WGPURenderPassEncoder pass, + const WgpuOverlayFrame& 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 WgpuOverlayFrame& f, + const std::vector& planes); + + // ---- After the edge silhouette pass, on the resolved surface ---- + + // Corner axis gizmo (bottom-left, 110×110 px). Independent ortho + // projection — only the camera direction matters. + void encodeCornerAxis(WGPUCommandEncoder enc, + WGPUTextureView surface_view, + const WgpuOverlayFrame& f); + + // Marquee box-select drag rect (translucent fill + thick outline). + // No-op when `active` is false. + void encodeMarquee(WGPUCommandEncoder enc, + WGPUTextureView surface_view, + const WgpuOverlayFrame& f, + QPoint start_logical_px, + QPoint current_logical_px, + bool active); + + // Shared with the viewport's main FrameUniforms: same cap so the + // viewport's clip-plane array and the gizmo visualiser agree on + // how many planes can ever be active. + static constexpr int kMaxSectionPlanes = 6; + +private: + bool buildAxisIndicator(); + bool buildSectionVisualizer(); + bool buildMarquee(); + + WGPUInstance instance_ = nullptr; + WGPUDevice device_ = nullptr; + WGPUQueue queue_ = nullptr; + WGPUTextureFormat surface_format_ = WGPUTextureFormat_Undefined; + int sample_count_ = 1; + + // ---- Axis indicator (shared shape, three pipelines) ---- + // Slot 0 = corner gizmo. Slots 1/2 = pivot visible/x-ray. + WGPUShaderModule axis_shader_module_ = nullptr; + WGPUBindGroupLayout axis_bgl_ = nullptr; + WGPUPipelineLayout axis_pipeline_layout_ = nullptr; + WGPURenderPipeline axis_pivot_pipeline_ = nullptr; + WGPURenderPipeline axis_pivot_xray_pipeline_ = nullptr; + WGPURenderPipeline axis_corner_pipeline_ = nullptr; + WGPUBuffer axis_vertex_buffer_ = nullptr; + WGPUBuffer axis_uniform_buffer_ = nullptr; + 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; + WGPUPipelineLayout marquee_pipeline_layout_ = nullptr; + WGPURenderPipeline marquee_pipeline_ = nullptr; + WGPURenderPipeline marquee_fill_pipeline_ = nullptr; + WGPUBuffer marquee_vertex_buffer_ = nullptr; + WGPUBuffer marquee_fill_vertex_buffer_ = nullptr; + WGPUBuffer marquee_uniform_buffer_ = nullptr; + WGPUBindGroup marquee_bind_group_ = nullptr; +}; + +#endif // WGPUOVERLAYRENDERER_H diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp index 4977a6aa60..947de6ef3c 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.cpp +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.cpp @@ -48,8 +48,9 @@ // std140-ish layout: every member naturally 16-aligned, struct stride = 96. // ----------------------------------------------------------------------------- -// Section-cutting cap. Matches GL ViewportWindow's MaxSectionPlanes. -static constexpr int kMaxSectionPlanes = 6; +// Section-cutting cap. Single source of truth lives in WgpuOverlayRenderer +// so the visualizer and the WGSL clip array agree by construction. +static constexpr int kMaxSectionPlanes = WgpuOverlayRenderer::kMaxSectionPlanes; struct FrameUniforms { float view_proj[16]; @@ -1614,9 +1615,10 @@ bool WgpuViewportWindow::initWgpu() { if (!buildPipelines()) return false; if (!buildHizPipeline()) return false; if (!buildEdgePipeline()) return false; - if (!buildAxisIndicator()) return false; - if (!buildSectionVisualizer()) return false; - if (!buildMarquee()) return false; + if (!overlays_.init(instance_, device_, queue_, surface_format_, SAMPLE_COUNT)) { + qWarning() << "WgpuOverlayRenderer init failed"; + return false; + } if (!buildPickPipeline()) return false; qInfo() << "wgpu init OK; surface format =" << int(surface_format_); @@ -2166,469 +2168,6 @@ void WgpuViewportWindow::encodeEdgePass(WGPUCommandEncoder enc, } // ----------------------------------------------------------------------------- -// Axis indicator: corner gizmo + orbit pivot -// ----------------------------------------------------------------------------- -// -// One shader and one 6-vertex unit-cross VBO drive both the camera-orientation -// gizmo in the bottom-left and the orbit-pivot marker at camera_target. The -// vertex shader transforms each vertex as `mvp * (origin + position * arm)` -// so the same primitive serves both: -// -// corner: mvp = ortho × lookAt(camera_dir) — camera-orientation only, -// origin = 0, arm = 1, viewport set to a small corner box. -// pivot : mvp = main view-proj, -// origin = camera_target, arm = 30 logical px in world units. -// -// Two pipelines: pivot draws inside the main MSAA render pass after geometry -// so depth interaction is correct; corner draws on the resolved surface after -// the edge silhouette pass so the laplacian can't darken its lines. - -// Shared by every thick-line gizmo (axis indicator, section plane -// gizmo, future overlays). Provides: -// - VsOut: rasterizer carry — clip_pos, rgba colour, side_t for AA -// - thick_line_clip(p_start, p_end, t, side, viewport_size, -// line_width_px): the actual screen-space expansion. -// Every vertex carries BOTH endpoints; expanding from -// `s_end - s_start` (NOT this vertex vs the other) keeps the -// perpendicular consistent so the quad stays a rectangle and not -// a bowtie. clip.w is preserved so depth interpolation stays correct. -// - fs_main: analytical 1-pixel AA via |side_t| + fwidth() — -// interpolated across the perpendicular, smoothstep over its -// per-fragment derivative gives a smooth edge regardless of MSAA. -// Pasted into each shader as a prefix via adjacent string literals. -#define 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 char* AXIS_WGSL = 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"; - -// Populate `attribs[5]` with the standard thick-line 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`. -static 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; -} - -bool WgpuViewportWindow::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 so all overlay colours - // come from one canonical source. - // - // Vertex buffer: three positive-axis rays, each expanded into a - // 4-corner quad (6 vertices in triangle-list order) so the vertex - // shader can offset by `line_width / 2` pixels in screen space. - // Per vertex (11 floats = 44 bytes): - // start (vec3) — the line's start endpoint (same for all 6 verts of an axis) - // end (vec3) — the line's end endpoint (same for all 6 verts of an axis) - // col (vec3) — RGB - // t (f32) — 0 if this vertex sits at `start`, 1 if at `end` - // side (f32) — -1 or +1, which half of the perpendicular - // - // The 6-vertex ordering for the two triangles of each quad is - // (start,-1) (start,+1) (end,-1) (end,-1) (start,+1) (end,+1) - // — a standard triangle-list two-tri quad split. - static const float axis_verts[] = { - // start end color (RGB — Bonsai decorators) t side - // ---- +X (decorator red) — start=(0,0,0), end=(1,0,0) ---- - 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 (decorator green) — start=(0,0,0), end=(0,1,0) ---- - 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 (decorator blue) — start=(0,0,0), end=(0,0,1) ---- - 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)); - } - - // Uniform buffer: three 256-byte-aligned slots - // (slot 0 = corner, slot 1 = pivot visible, slot 2 = pivot x-ray) - // addressed via a dynamic offset on the bind group. - { - 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); - } - - // Bind group layout: single uniform with dynamic offset. - { - WGPUBindGroupLayoutEntry entry = {}; - entry.binding = 0; - entry.visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment; - entry.buffer.type = WGPUBufferBindingType_Uniform; - entry.buffer.hasDynamicOffset = 1; - entry.buffer.minBindingSize = 96; // mat4 + vec3 + f32 + f32 + f32 + vec2 (struct size = 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); - } - - // Pipeline layout. - { - 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); - } - - // Bind group: one binding pointing at slot 0 (size = slot size; offset - // selected dynamically at setBindGroup time). - { - 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); - } - - // Shader module (shared between both pipelines). - { - WGPUShaderSourceWGSL wgsl_src = {}; - wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; - wgsl_src.code = svFromCStr(AXIS_WGSL); - 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); - - // Standard alpha blend so the corner gizmo can soften over the resolved - // background and the pivot can fade against scene colour. - 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; - - // ---- Pivot pipelines: MSAA color + depth, both x-ray + visible ---- - // The two pipelines differ only in depthCompare: GreaterEqual lets the - // x-ray pass land only on pixels where the scene's depth is AT or NEARER - // than the pivot (so we know the pivot is behind something there); the - // visible pass uses LessEqual to land where the pivot is in front. We - // build them in a small helper-like inline block to share state. - 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 = 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 pipeline: 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_; -} - -// Pack a 256-byte uniform slot. Layout matches the WGSL AxisUniforms -// struct's natural alignment: mat4 + vec3 + f32 + f32 + f32 + vec2. -static void packAxisUniform(uint8_t* dst, - const QMatrix4x4& mvp, - const QVector3D& origin, - float arm, float alpha, - float line_width_px, - float viewport_w, float viewport_h) { - std::memset(dst, 0, 256); - std::memcpy(dst, mvp.constData(), 16 * sizeof(float)); // 0..64 mvp - float ox = origin.x(), oy = origin.y(), oz = origin.z(); - std::memcpy(dst + 64, &ox, sizeof(float)); // 64..68 origin.x - std::memcpy(dst + 68, &oy, sizeof(float)); // 68..72 origin.y - std::memcpy(dst + 72, &oz, sizeof(float)); // 72..76 origin.z - std::memcpy(dst + 76, &arm, sizeof(float)); // 76..80 arm - std::memcpy(dst + 80, &alpha, sizeof(float)); // 80..84 alpha - std::memcpy(dst + 84, &line_width_px, sizeof(float)); // 84..88 line_width_px - std::memcpy(dst + 88, &viewport_w, sizeof(float)); // 88..92 viewport.x - std::memcpy(dst + 92, &viewport_h, sizeof(float)); // 92..96 viewport.y -} - -void WgpuViewportWindow::encodePivotIndicator(WGPURenderPassEncoder pass, - const QMatrix4x4& view_proj) { - if (!pivot_indicator_visible_ || !axis_pivot_pipeline_ - || !axis_pivot_xray_pipeline_) return; - if (configured_h_ <= 0) return; - - // Arm length = 30 logical px projected into world space at the pivot's - // distance. world_per_pixel matches GL's renderPivotIndicator math: - // 2 · d · tan(fovy/2) / viewport_h (ortho's box height collapses to the - // same formula because we size it from camera_distance at the pivot). - const float fovy_rad = qDegreesToRadians(camera_fov_y_deg_); - const float world_per_pixel = camera_distance_ * std::tan(fovy_rad * 0.5f) - * 2.0f / float(configured_h_); - const float arm_pixels = 30.0f * float(devicePixelRatio()); - const float arm_world = arm_pixels * world_per_pixel; - - const QVector3D origin(camera_target_[0], camera_target_[1], camera_target_[2]); - - // Slot 1 = visible (α=1), slot 2 = x-ray (α=0.30). Identical except for - // alpha — write both so a single bind group can dispatch both draws. - const float dpr = float(devicePixelRatio()); - const float line_w = 2.5f * dpr; - const float vw = float(configured_w_); - const float vh = float(configured_h_); - uint8_t slot_visible[256]; - uint8_t slot_xray[256]; - packAxisUniform(slot_visible, view_proj, origin, arm_world, 1.00f, line_w, vw, vh); - packAxisUniform(slot_xray, view_proj, origin, 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); - - // Pass 1: dim x-ray for the occluded portion. - wgpuRenderPassEncoderSetPipeline(pass, axis_pivot_xray_pipeline_); - wgpuRenderPassEncoderSetBindGroup(pass, 0, axis_bind_group_, 1, &xray_off); - wgpuRenderPassEncoderDraw(pass, 18, 1, 0, 0); - - // Pass 2: full-alpha visible portion. - wgpuRenderPassEncoderSetPipeline(pass, axis_pivot_pipeline_); - wgpuRenderPassEncoderSetBindGroup(pass, 0, axis_bind_group_, 1, &visible_off); - wgpuRenderPassEncoderDraw(pass, 18, 1, 0, 0); -} - -void WgpuViewportWindow::encodeCornerAxisGizmo(WGPUCommandEncoder enc, - WGPUTextureView surface_view) { - if (!axis_corner_pipeline_ || !surface_view) return; - const int dpr = std::max(1, int(devicePixelRatio())); - const uint32_t gizmo_size = uint32_t(110 * dpr); - const uint32_t margin = uint32_t(10 * dpr); - if (gizmo_size == 0 || configured_w_ <= 0 || configured_h_ <= 0) return; - // Pin to the BOTTOM-LEFT corner. WebGPU framebuffer y goes top→bottom, - // so a bottom-left corner sits at y = height - margin - gizmo_size. - const uint32_t fb_h = uint32_t(configured_h_); - if (gizmo_size + margin > fb_h) return; - const uint32_t y = fb_h - margin - gizmo_size; - - // Build the gizmo's MVP: a small ortho box looking at the origin from - // the camera's direction. The eye direction matches GL's renderAxisGizmo - // exactly (yaw/pitch → unit forward, world-up = +Z). Near the poles the - // up axis collapses against the look direction, so swap to Y-up there — - // mirrors buildViewProj's identical fallback for the main projection. - const float yaw_rad = qDegreesToRadians(camera_yaw_deg_); - const float pitch_rad = qDegreesToRadians(camera_pitch_deg_); - const QVector3D eye_dir(std::cos(pitch_rad) * std::cos(yaw_rad), - std::cos(pitch_rad) * std::sin(yaw_rad), - std::sin(pitch_rad)); - const QVector3D world_up = (std::abs(camera_pitch_deg_) >= 89.0f) - ? QVector3D(0.0f, 1.0f, 0.0f) - : QVector3D(0.0f, 0.0f, 1.0f); - QMatrix4x4 gv; - gv.lookAt(eye_dir * 3.0f, QVector3D(0, 0, 0), world_up); - QMatrix4x4 gp; - gp.ortho(-1.4f, 1.4f, -1.4f, 1.4f, 0.1f, 10.0f); - // Match the main projection's z-remap so the gizmo's NDC z falls in - // [0, 1] (WebGPU) rather than [-1, 1] (Qt's GL-style projection). - QMatrix4x4 z_remap; - z_remap(2, 2) = 0.5f; - z_remap(2, 3) = 0.5f; - const QMatrix4x4 mvp = z_remap * gp * gv; - - // line_width and viewport_size are in the gizmo's local viewport (not - // the framebuffer), because the vertex-shader perpendicular offset is - // computed in NDC and NDC is per-viewport. - uint8_t slot[256]; - const float line_w = 2.5f * float(dpr); - packAxisUniform(slot, mvp, QVector3D(0, 0, 0), 1.0f, 1.0f, line_w, - float(gizmo_size), float(gizmo_size)); - const uint32_t slot_offset = 0u; // corner lives in slot 0 - wgpuQueueWriteBuffer(queue_, axis_uniform_buffer_, slot_offset, slot, sizeof(slot)); - - WGPURenderPassColorAttachment color = {}; - color.view = surface_view; - color.loadOp = WGPULoadOp_Load; // preserve what was drawn before - 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); -} - void WgpuViewportWindow::setPivotIndicatorVisible(bool visible, int hide_after_ms) { if (!pivot_indicator_hide_timer_) { pivot_indicator_hide_timer_ = new QTimer(this); @@ -2647,646 +2186,12 @@ void WgpuViewportWindow::setPivotIndicatorVisible(bool visible, int hide_after_m } requestUpdate(); } - -// ----------------------------------------------------------------------------- -// Marquee overlay (drag-rect for box-select) -// ----------------------------------------------------------------------------- -// -// 4-segment unit-quad VBO; the vertex shader maps (0..1)² unit coords to NDC -// via the per-frame uniform (rect.min, rect.max in NDC, both in [-1, 1] -// with +y up). Reuses thick_line_clip / fs_main from the shared header so -// the rect has analytical AA and reads cleanly against any background. - -static const char* MARQUEE_WGSL = 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, // multiplier on color.a for the translucent fill pass -}; - -@group(0) @binding(0) var u: MarqueeUniforms; - -// Outline (thick line + AA). -@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; -} - -// Fill (flat translucent quad). Drawn before the outline so the outline -// renders on top with full opacity. -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"; - -bool WgpuViewportWindow::buildMarquee() { - // VBO: 4 line segments (top/right/bottom/left of the unit quad), - // each expanded into a 6-vertex thick-line. Per vertex (6 floats = - // 24 bytes): start_uv(vec2) + end_uv(vec2) + t(f32) + side(f32). - struct Seg { std::array s, e; }; - static const Seg segs[] = { - { {0, 0}, {1, 0} }, // top - { {1, 0}, {1, 1} }, // right - { {1, 1}, {0, 1} }, // bottom - { {0, 1}, {0, 0} }, // left - }; - 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)); - } - // Fill VBO: 6 verts of a unit-quad triangle list. - 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; // vec2 + vec2 + vec4 + vec2 + f32 + f32 = 48 B + pad - 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); - 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); - - // ---- Fill pipeline (translucent quad, vs_fill / fs_fill) ---- - WGPUVertexAttribute fill_attribs[1] = {}; - fill_attribs[0].format = WGPUVertexFormat_Float32x2; // pos_uv - 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 WgpuViewportWindow::encodeMarquee(WGPUCommandEncoder enc, - WGPUTextureView surface_view) { - if (!marquee_pipeline_ || !surface_view) return; - if (!box_select_active_) return; - if (configured_w_ <= 0 || configured_h_ <= 0) return; - - // Logical-pixel rect → NDC. Qt mouse y goes top→bottom; NDC y goes - // bottom→top, hence the (1 - 2y/h) flip. - const float w = float(configured_w_); - const float h = float(configured_h_); - const float dpr = float(std::max(1, int(devicePixelRatio()))); - const float lx0 = float(std::min(box_select_start_pos_.x(), - box_select_current_pos_.x())) * dpr; - const float ly0 = float(std::min(box_select_start_pos_.y(), - box_select_current_pos_.y())) * dpr; - const float lx1 = float(std::max(box_select_start_pos_.x(), - box_select_current_pos_.x())) * dpr; - const float ly1 = float(std::max(box_select_start_pos_.y(), - box_select_current_pos_.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; - // y: pixel 0 is top, so NDC y = 1 - 2*py/h - const float ny_top = 1.0f - 2.0f * ly0 / h; - const float ny_bottom = 1.0f - 2.0f * ly1 / h; - // The vs shader expects rect_max y > rect_min y in NDC; the rect_uv - // y = 0 maps to rect_min y. Top corner gets uv.y = 0, bottom gets - // uv.y = 1. So rect_min.y = ny_top, rect_max.y = ny_bottom. - // Bonsai decorator_color_special (axis +Z blue): 0.157, 0.565, 1.000. - // Outline alpha 0.95; fill_alpha (multiplied onto that) gives ~0.15 - // alpha in the fill so the underlying scene reads through. - float uniforms[16] = {}; - uniforms[0] = nx0; uniforms[1] = ny_top; // rect_min @ 0 - uniforms[2] = nx1; uniforms[3] = ny_bottom; // rect_max @ 8 - uniforms[4] = 0.157f; uniforms[5] = 0.565f; // color.rg @ 16 - uniforms[6] = 1.000f; uniforms[7] = 0.95f; // color.ba - uniforms[8] = w; uniforms[9] = h; // viewport_size @ 32 - uniforms[10] = 3.0f * dpr; // line_width_px @ 40 - uniforms[11] = 0.20f; // fill_alpha @ 44 - 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); - - // Pass 1: translucent fill (underneath the outline). - wgpuRenderPassEncoderSetPipeline(pass, marquee_fill_pipeline_); - wgpuRenderPassEncoderSetVertexBuffer(pass, 0, marquee_fill_vertex_buffer_, - 0, WGPU_WHOLE_SIZE); - wgpuRenderPassEncoderDraw(pass, 6, 1, 0, 0); - - // Pass 2: thick-line outline. - wgpuRenderPassEncoderSetPipeline(pass, marquee_pipeline_); - wgpuRenderPassEncoderSetVertexBuffer(pass, 0, marquee_vertex_buffer_, 0, - WGPU_WHOLE_SIZE); - wgpuRenderPassEncoderDraw(pass, 24, 1, 0, 0); - - wgpuRenderPassEncoderEnd(pass); - wgpuRenderPassEncoderRelease(pass); -} - -void WgpuViewportWindow::releaseMarquee() { - 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; } - if (box_pick_staging_buffer_) { wgpuBufferRelease(box_pick_staging_buffer_); box_pick_staging_buffer_ = nullptr; } - box_pick_staging_capacity_ = 0; -} - -// ----------------------------------------------------------------------------- -// Section plane visualisation -// ----------------------------------------------------------------------------- -// -// Each active plane is drawn as a quad sized to span the scene's AABB so the -// cut is visible at any zoom. The quad is a unit square in plane-local space; -// the vertex shader builds tangent / bitangent from the plane normal and -// places the quad at u_origin. Two draws per plane share one pipeline: -// a back-face fill at lower alpha gives an "occluded" hint when the camera -// sees the back of the plane (matches what GL's plane gizmo does visually). -// 6 active-plane × 1 quad × 6 verts = 36 verts max per frame, trivial. - -// GL's section gizmo (2 × 2 m wireframe quad outline + arrow shaft + 4-line -// arrow head) rendered as thick lines so it stays visible against busy -// BIM geometry. Shares VsOut, thick_line_clip and fs_main with the axis -// indicator via THICK_LINE_HELPERS_WGSL. -static const char* SECTION_WGSL = 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"; - -bool WgpuViewportWindow::buildSectionVisualizer() { - // 9 line segments (4 quad-outline + 1 arrow shaft + 4 arrow head), - // each expanded into a 6-vertex thick-line quad by the vertex shader. - // Per vertex (11 floats = 44 B): - // start_local (vec3) — segment's start point in plane-local space - // end_local (vec3) — segment's end point in plane-local space - // col (vec3) — RGB - // t (f32) — 0 at start, 1 at end - // side (f32) — -1 or +1, which half of the perpendicular - struct Seg { - std::array s, e; - std::array c; - }; - // Entire gizmo uses Bonsai's decorator_color_error red so it reads - // against any geometry. White quad + yellow arrow (GL's defaults) - // disappeared into light surfaces; one consistent saturated red is - // both more visible and visually identifies "this is a tool overlay". - 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 }, - }; - constexpr size_t kVertsPerSegment = 6; // (start,-1) (start,+1) (end,-1) (end,-1) (start,+1) (end,+1) - constexpr size_t kFloatsPerVertex = 11; // start.xyz, end.xyz, col.xyz, t, side - std::vector verts; - verts.reserve(std::size(segs) * kVertsPerSegment * kFloatsPerVertex); - 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)); - } - - // Uniform buffer: one slot per plane (cap at kMaxSectionPlanes). - { - 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); - } - - // Bind group layout: single uniform with dynamic offset (slot index × - // 256 chosen at setBindGroup time). - { - WGPUBindGroupLayoutEntry entry = {}; - entry.binding = 0; - entry.visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment; - entry.buffer.type = WGPUBufferBindingType_Uniform; - entry.buffer.hasDynamicOffset = 1; - entry.buffer.minBindingSize = 160; // mat4 + 4×(vec3+pad) + vec4 + vec2 + pad = 160 B - 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); - 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; - - // Depth: respect scene depth so half the plane is behind opaque - // geometry (giving the "this is where it cuts" hint), but don't - // write depth ourselves so further translucent draws aren't blocked. - 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 = SAMPLE_COUNT; - rp_desc.multisample.mask = 0xFFFFFFFFu; - section_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp_desc); - - return section_pipeline_ != nullptr; -} - -// Pack a 256-byte uniform slot for one section plane. Layout matches WGSL -// SectionUniforms: mat4 + 4 × (vec3 + scalar pad) + vec4 + vec2 + 8 B pad -// = 160 B used. -static void packSectionUniform(uint8_t* dst, - const QMatrix4x4& mvp, - const QVector3D& origin, float half_size, - const QVector3D& tangent, float line_width_px, - const QVector3D& bitangent, - const QVector3D& normal, - float r, float g, float b, float a, - float viewport_w, float viewport_h) { - std::memset(dst, 0, 256); - std::memcpy(dst, mvp.constData(), 16 * sizeof(float)); - auto put_vec3_pad = [&](size_t off, const QVector3D& 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); // 64..80 - put_vec3_pad(80, tangent, line_width_px); // 80..96 - put_vec3_pad(96, bitangent, 0.0f); // 96..112 - put_vec3_pad(112, normal, 0.0f); // 112..128 - float tint[4] = { r, g, b, a }; - std::memcpy(dst + 128, tint, sizeof(tint)); // 128..144 - std::memcpy(dst + 144, &viewport_w, sizeof(float)); // 144..148 - std::memcpy(dst + 148, &viewport_h, sizeof(float)); // 148..152 -} - -void WgpuViewportWindow::encodeSectionPlanes(WGPURenderPassEncoder pass, - const QMatrix4x4& view_proj) { - if (!section_pipeline_ || section_planes_.empty()) return; - - wgpuRenderPassEncoderSetPipeline(pass, section_pipeline_); - wgpuRenderPassEncoderSetVertexBuffer(pass, 0, section_vertex_buffer_, 0, - WGPU_WHOLE_SIZE); - - const int n = std::min(int(section_planes_.size()), kMaxSectionPlanes); - for (int i = 0; i < n; ++i) { - const SectionPlane& p = section_planes_[i]; - - // Stable in-plane basis: anyOrthogonal picks the world axis least - // parallel to n so the cross-product stays well-conditioned at - // any orientation. tangent → "u" axis, bitangent → "v" axis. - QVector3D nn = p.n.normalized(); - const float ax = std::abs(nn.x()), ay = std::abs(nn.y()), az = std::abs(nn.z()); - QVector3D seed = (ax < ay && ax < az) ? QVector3D(1, 0, 0) - : (ay < az) ? QVector3D(0, 1, 0) - : QVector3D(0, 0, 1); - QVector3D tangent = QVector3D::crossProduct(nn, seed); - if (tangent.lengthSquared() < 1e-12f) tangent = QVector3D(1, 0, 0); - tangent.normalize(); - QVector3D bitangent = QVector3D::crossProduct(nn, tangent).normalized(); - - // Fixed 1 m half-size matches GL's renderSectionPlanes constant - // (kHalfSize = 1.0). Per-plane visual_radius from the picked AABB - // is no longer used here — the GL gizmo is a small marker, not a - // sheet sized to the cut subject. - const float half_size = 1.0f; - const float dpr = float(std::max(1, int(devicePixelRatio()))); - const float line_w = 5.0f * dpr; - const float vw = float(configured_w_); - const float vh = float(configured_h_); - - uint8_t slot[256]; - // Neutral tint — actual colours come from the per-vertex VBO - // (white quad outline + red arrow). Tint stays available for a - // future "selected" multiplier; alpha controls the whole gizmo's - // opacity. - packSectionUniform(slot, 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); - // 9 segments × 6 verts (2 triangles each) = 54 vertices. - wgpuRenderPassEncoderDraw(pass, 54, 1, 0, 0); - } -} - -void WgpuViewportWindow::releaseSectionVisualizer() { - 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; } -} - void WgpuViewportWindow::releaseEdgeResources() { if (edge_bind_group_) { wgpuBindGroupRelease(edge_bind_group_); edge_bind_group_ = nullptr; } if (edge_pipeline_) { wgpuRenderPipelineRelease(edge_pipeline_); edge_pipeline_ = nullptr; } if (edge_shader_module_) { wgpuShaderModuleRelease(edge_shader_module_);edge_shader_module_ = nullptr; } if (edge_pipeline_layout_) { wgpuPipelineLayoutRelease(edge_pipeline_layout_); edge_pipeline_layout_ = nullptr; } if (edge_bgl_) { wgpuBindGroupLayoutRelease(edge_bgl_); edge_bgl_ = nullptr; } - - 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; } } // ----------------------------------------------------------------------------- @@ -3898,7 +2803,7 @@ bool WgpuViewportWindow::addSectionPlaneAtSurface(const QVector3D& point, const QVector3D eye_dir = eye - point; if (QVector3D::dotProduct(n, eye_dir) < 0.0f) n = -n; - SectionPlane p; + WgpuSectionPlane p; p.n = n; p.origin = point; p.d = -QVector3D::dotProduct(n, point); @@ -3953,13 +2858,14 @@ int WgpuViewportWindow::hitTestSectionGizmo(int x, int y) const { 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]; + const WgpuSectionPlane& p = section_planes_[i]; QVector2D 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 — encodeSectionPlanes uses half_size = 1.0 to scale a - // plane-local arrow tip at z = 1. Mirror that here. + // space — WgpuOverlayRenderer::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 QVector2D q{float(x), float(y)}; @@ -3979,7 +2885,7 @@ void WgpuViewportWindow::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_]; + WgpuSectionPlane& p = section_planes_[section_drag_index_]; const int w = width(); const int h = height(); @@ -5011,18 +3917,30 @@ void WgpuViewportWindow::render() { } } + // Snapshot the per-frame inputs every overlay needs. Built once and + // passed by const-ref so WgpuOverlayRenderer never reaches back into + // this viewport. + WgpuOverlayFrame overlay_frame; + overlay_frame.view_proj = vp_this_frame; + overlay_frame.camera_target = QVector3D(camera_target_[0], + camera_target_[1], + camera_target_[2]); + overlay_frame.camera_distance = camera_distance_; + overlay_frame.camera_yaw_deg = camera_yaw_deg_; + overlay_frame.camera_pitch_deg = camera_pitch_deg_; + overlay_frame.camera_fov_y_deg = camera_fov_y_deg_; + overlay_frame.viewport_w_px = int(width() * devicePixelRatio()); + overlay_frame.viewport_h_px = int(height() * devicePixelRatio()); + overlay_frame.device_pixel_ratio = int(devicePixelRatio()); + // Section planes — translucent overlay quads showing where each - // active clip plane cuts. Drawn inside the main MSAA pass with - // depth-LessEqual + writeMask off so they participate in depth tests - // (half hides behind closer geometry) without depth-blocking further - // overlays. Discard inside the shaders honours the clip itself, so - // the visible portion of the quad is always the kept-side region. - encodeSectionPlanes(pass, vp_this_frame); + // active clip plane cuts. Drawn inside the main MSAA pass. + overlays_.encodeSectionGizmos(pass, overlay_frame, section_planes_); // Pivot indicator. Encoded inside the main MSAA pass after geometry so // depth interaction is correct — the indicator vanishes behind closer // surfaces. Visibility is driven by orbit/wheel UI handlers. - encodePivotIndicator(pass, vp_this_frame); + overlays_.encodePivot(pass, overlay_frame, pivot_indicator_visible_); wgpuRenderPassEncoderEnd(pass); wgpuRenderPassEncoderRelease(pass); @@ -5036,12 +3954,15 @@ void WgpuViewportWindow::render() { // Corner axis gizmo. Encoded after the edge pass on the resolved // surface, so the laplacian can't darken its lines or its background. - encodeCornerAxisGizmo(enc, view); + overlays_.encodeCornerAxis(enc, view, overlay_frame); // Marquee box-select drag rect (visible only while a drag is active). // Drawn on the resolved surface so the rect outline isn't affected by // the edge silhouette pass. - encodeMarquee(enc, view); + overlays_.encodeMarquee(enc, view, overlay_frame, + box_select_start_pos_, + box_select_current_pos_, + box_select_active_); // ---- HiZ: resolve MSAA depth → small single-sample → ping-pong slot int hiz_submitted_slot = -1; @@ -6580,7 +5501,7 @@ void WgpuViewportWindow::updateFrameUniforms() { const int n = std::min(int(section_planes_.size()), kMaxSectionPlanes); u.clip_count = n; for (int i = 0; i < n; ++i) { - const SectionPlane& p = section_planes_[i]; + const WgpuSectionPlane& p = section_planes_[i]; u.clip_planes[i][0] = p.n.x(); u.clip_planes[i][1] = p.n.y(); u.clip_planes[i][2] = p.n.z(); @@ -7502,8 +6423,7 @@ void WgpuViewportWindow::shutdown() { releaseMsaaColorTexture(); releaseHizResources(); releaseEdgeResources(); - releaseSectionVisualizer(); - releaseMarquee(); + overlays_.destroy(); releasePickResources(); if (frame_bind_group_) { wgpuBindGroupRelease(frame_bind_group_); frame_bind_group_ = nullptr; } diff --git a/src/ifcviewer-wgpu/WgpuViewportWindow.h b/src/ifcviewer-wgpu/WgpuViewportWindow.h index 6171ad46a6..edb51cc238 100644 --- a/src/ifcviewer-wgpu/WgpuViewportWindow.h +++ b/src/ifcviewer-wgpu/WgpuViewportWindow.h @@ -40,6 +40,7 @@ #include "SidecarCache.h" #include "WgpuBufferPool.h" #include "WgpuModelGpuData.h" +#include "WgpuOverlayRenderer.h" #include "WgpuSelectionState.h" #include "WgpuStreamingThread.h" #include "WgpuVisibilityState.h" @@ -220,18 +221,11 @@ private: bool buildHizPipeline(); bool buildEdgePipeline(); void encodeEdgePass(WGPUCommandEncoder enc, WGPUTextureView surface_view); - bool buildAxisIndicator(); - // Encode the pivot indicator inside the main MSAA render pass — runs - // after geometry so depth interaction is correct. - void encodePivotIndicator(WGPURenderPassEncoder pass, - const QMatrix4x4& view_proj); - // Encode the corner gizmo on the resolved single-sample surface, - // after the edge silhouette pass. Uses setViewport for the corner box. - void encodeCornerAxisGizmo(WGPUCommandEncoder enc, - WGPUTextureView surface_view); // Show/hide the pivot indicator. hide_after_ms > 0 starts the // single-shot auto-hide timer used by the wheel-zoom afterglow; - // drag callers pass 0 and toggle manually on press/release. + // drag callers pass 0 and toggle manually on press/release. The + // actual gizmo rendering lives in WgpuOverlayRenderer — this just + // manages the UI-side visibility timer. void setPivotIndicatorVisible(bool visible, int hide_after_ms = 0); void releaseEdgeResources(); @@ -450,46 +444,17 @@ private: WGPUBindGroup edge_bind_group_ = nullptr; bool edges_enabled_ = true; - // Axis indicator overlay — drives both the corner gizmo and the - // orbit pivot indicator from the SAME 6-vertex unit cross + shader. - // One uniform buffer with three 256-byte-aligned slots - // (slot 0 = corner, slot 1 = pivot visible, slot 2 = pivot x-ray), - // one bind group with a dynamic offset, three pipelines that differ - // only in render-target / depth setup: - // axis_pivot_pipeline_: MSAA + depth LessEqual (visible α=1) - // axis_pivot_xray_pipeline_: MSAA + depth GreaterEqual (occluded α≈0.3) - // axis_corner_pipeline_: resolved, no depth, sampleCount=1 - // The pivot's x-ray pass renders first so the visible pass overdraws - // it where geometry isn't in the way. Matches GL's renderPivotIndicator. - WGPUShaderModule axis_shader_module_ = nullptr; - WGPUBindGroupLayout axis_bgl_ = nullptr; - WGPUPipelineLayout axis_pipeline_layout_ = nullptr; - WGPURenderPipeline axis_pivot_pipeline_ = nullptr; - WGPURenderPipeline axis_pivot_xray_pipeline_ = nullptr; - WGPURenderPipeline axis_corner_pipeline_ = nullptr; - // Marquee overlay (drag-rect for box-select). Renders on the resolved - // surface after the corner gizmo. Uses a 4-segment unit-quad VBO; the - // shader maps unit coords to NDC via per-frame uniform (rect min/max - // in NDC + colour + viewport + line width). - WGPUShaderModule marquee_shader_module_ = nullptr; - WGPUBindGroupLayout marquee_bgl_ = nullptr; - WGPUPipelineLayout marquee_pipeline_layout_ = nullptr; - WGPURenderPipeline marquee_pipeline_ = nullptr; // outline (thick line + AA) - WGPURenderPipeline marquee_fill_pipeline_ = nullptr; // translucent quad fill - WGPUBuffer marquee_vertex_buffer_ = nullptr; // outline geometry - WGPUBuffer marquee_fill_vertex_buffer_ = nullptr; // 6-vert quad - WGPUBuffer marquee_uniform_buffer_ = nullptr; - WGPUBindGroup marquee_bind_group_ = nullptr; - bool buildMarquee(); - void encodeMarquee(WGPUCommandEncoder enc, WGPUTextureView surface_view); - void releaseMarquee(); - WGPUBuffer axis_vertex_buffer_ = nullptr; // 6 × 24 B - WGPUBuffer axis_uniform_buffer_ = nullptr; // 3 × 256 B slots - WGPUBindGroup axis_bind_group_ = nullptr; // dynamic-offset - static constexpr uint32_t kAxisUniformSlotSize = 256; + // Pivot visibility state — the gizmo itself lives in overlays_. + // The timer auto-hides the pivot after a wheel-zoom afterglow. bool pivot_indicator_visible_ = false; QTimer* pivot_indicator_hide_timer_ = nullptr; + // All viewport overlays (axis indicator, section gizmos, marquee + // rect) — pipelines + shaders + buffers + encoders. The viewport + // builds a WgpuOverlayFrame each frame and asks the renderer to + // encode each overlay; see WgpuOverlayRenderer.h. + WgpuOverlayRenderer overlays_; + // Pick pass (stage 4). Single-sample R32UInt target + depth, vertex- // pulled from the same visible_draws / instances buffers as the main // pass — pick fragment outputs the instance's object_id. The pick @@ -510,18 +475,11 @@ private: int pick_w_ = 0; int pick_h_ = 0; - // Section-cutting state. Each plane is (n, d) with normal n in world - // space and signed distance d = -dot(n, point_on_plane); a point P is - // on the kept side iff dot(n, P) + d <= 0. The vector mirrors GL's - // ViewportWindow::section_planes_. - struct SectionPlane { - QVector3D n; // unit normal - float d; // -dot(n, origin) - QVector3D origin; // surface point at the moment the plane was added - float visual_radius; // half-extent for the gizmo quad (set from the picked instance's AABB diagonal) - }; - std::vector section_planes_; - bool section_tool_active_ = false; + // Section-cutting state. WgpuSectionPlane lives in WgpuOverlayRenderer.h + // because the visualiser reads it; the viewport owns the authoritative + // vector that the section tool mutates. + std::vector section_planes_; + bool section_tool_active_ = false; // Marquee box-select. Armed on LMB press (when no other tool consumes // the click), becomes active after the cursor moves past @@ -553,25 +511,6 @@ private: // along that direction. void updateSectionDrag(int x, int y); - // Section plane visualisation. Renders one translucent quad per active - // plane, sized to the scene AABB so the cut is visible at any zoom. - // Built once (unit quad in plane-local space), oriented per-plane in - // the vertex shader from u_origin + tangent/bitangent (derived from - // the plane normal). Two passes: a back-facing fill (alpha 0.18) for - // the "behind geometry" hint plus a front-facing fill (alpha 0.35). - 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; - bool buildSectionVisualizer(); - void encodeSectionPlanes(WGPURenderPassEncoder pass, - const QMatrix4x4& view_proj); - void releaseSectionVisualizer(); - enum class HizSlotState : uint8_t { Idle, Mapping, Mapped }; static constexpr int HIZ_SLOTS = 2; WGPUBuffer hiz_staging_buffers_[HIZ_SLOTS] = { nullptr, nullptr };