mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-28 07:49:59 +00:00
ifcviewer-web: draw the axis indicator (corner gizmo + orbit pivot)
The desktop viewport draws an RGB triad in the bottom-left corner and a second one at the orbit target while navigating; the web build drew neither. Both lived in the Qt-coupled OverlayRenderer, which only ViewportWindow drives — the web host no-ops the overlay hooks — so the wasm build had no path to them at all. Lift them into AxisIndicatorRenderer, a Qt-free renderer in IfcViewerCore, and drive it from ViewportCore::render for desktop and web alike. Same move SectionGizmoRenderer already made; the drawing code is unchanged apart from swapping qDegreesToRadians for CameraMath's kPiF. Pivot visibility moves to the core with it: it was a QTimer on ViewportWindow, so the afterglow couldn't follow the gizmo across. It is now a Stopwatch deadline next to the drawing, with render() requesting frames until an armed afterglow expires. Hosts keep the same three triggers (on for orbit/pan drags, off on release, 600 ms on wheel). The web demo shell's log overlay sat exactly on top of the corner gizmo, so it shifts right of the 110 px box. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -253,6 +253,10 @@ EM_BOOL onMouseDown(int, const EmscriptenMouseEvent* e, void* user) {
|
||||
app->nav_drag_px = 0.0f;
|
||||
app->down_x = e->targetX; // canvas-relative CSS px
|
||||
app->down_y = e->targetY;
|
||||
// Show the pivot triad for the duration of an orbit / pan drag, so
|
||||
// it's visible what the camera turns around (matches the desktop).
|
||||
if (kind == NavKind::Orbit || kind == NavKind::Pan)
|
||||
app->core.setPivotIndicatorVisible(true);
|
||||
}
|
||||
return EM_TRUE;
|
||||
}
|
||||
@@ -297,6 +301,10 @@ EM_BOOL onMouseUp(int, const EmscriptenMouseEvent* e, void* user) {
|
||||
const NavKind kind = app->nav_kind;
|
||||
app->nav_active = false;
|
||||
app->nav_kind = NavKind::None;
|
||||
// Drag is over — hide the pivot indicator without afterglow. Only for the
|
||||
// gesture that raised it; a stray mouseup must not cut a wheel afterglow.
|
||||
if (was_active && (kind == NavKind::Orbit || kind == NavKind::Pan))
|
||||
app->core.setPivotIndicatorVisible(false);
|
||||
|
||||
// End a section-gizmo drag (took over the press; no pick/orbit on release).
|
||||
if (app->section_dragging) {
|
||||
@@ -373,6 +381,9 @@ EM_BOOL onWheel(int, const EmscriptenWheelEvent* e, void* user) {
|
||||
// In fly mode the wheel tunes move speed (Blender convention), not zoom.
|
||||
if (app->fly_mode) { app->core.flyAdjustSpeed(-float(dy) / 100.0f); return EM_TRUE; }
|
||||
app->core.dollyBy(-float(dy) / 100.0f);
|
||||
// Pivot afterglow on wheel — visible for 600 ms so the user can see what
|
||||
// they're zooming around without holding a drag.
|
||||
app->core.setPivotIndicatorVisible(true, 600);
|
||||
return EM_TRUE; // consume so the page doesn't scroll
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
// This file was generated with the assistance of an AI coding tool.
|
||||
//
|
||||
// The RGB axis indicator, in both of its guises: the corner gizmo that sits
|
||||
// in the viewport's bottom-left, and the pivot triad that appears at the
|
||||
// orbit target while a navigation drag is running. Both are drawn by the
|
||||
// shared AxisIndicatorRenderer from ViewportCore, so a regression here would
|
||||
// most likely be a wiring one — the renderer never inited, the pivot gate
|
||||
// never set, the corner pass encoded before the surface resolved — none of
|
||||
// which any other test in the suite would notice.
|
||||
import { test, expect } from '@playwright/test';
|
||||
import zlib from 'node:zlib';
|
||||
|
||||
// Decode the top-left pixel (RGB) of a PNG buffer. Row 0 pixel 0 is
|
||||
// filter-agnostic — every PNG predictor references zero neighbours there —
|
||||
// so this can skip filter handling entirely.
|
||||
function firstPixelRGB(png) {
|
||||
let off = 8;
|
||||
const idat = [];
|
||||
while (off + 8 <= png.length) {
|
||||
const len = png.readUInt32BE(off);
|
||||
const type = png.toString('ascii', off + 4, off + 8);
|
||||
const data = png.subarray(off + 8, off + 8 + len);
|
||||
if (type === 'IDAT') idat.push(data);
|
||||
else if (type === 'IEND') break;
|
||||
off += 12 + len;
|
||||
}
|
||||
const raw = zlib.inflateSync(Buffer.concat(idat));
|
||||
return [raw[1], raw[2], raw[3]]; // skip the row filter byte
|
||||
}
|
||||
|
||||
// Is this pixel on the +Z arm? Its colour is Bonsai's decorator blue
|
||||
// (0.157, 0.565, 1.000), so blue leads red by a mile. Everything it can be
|
||||
// drawn over stays well under the threshold: the background is a near-grey
|
||||
// (32, 35, 41), the sample model is white, and even the dim x-ray pass —
|
||||
// 0.3 alpha where the arm is behind geometry — lands around (191, 222, 255).
|
||||
const isAxisBlue = ([r, , b]) => b - r > 30;
|
||||
|
||||
// Sample 1x1 pixels straight up from (cx, cy), which is where the +Z arm
|
||||
// points at the default camera pitch. Stepping rather than picking one exact
|
||||
// pixel keeps this off the anti-aliased edges of a 2.5 px line.
|
||||
async function scanUp(page, cx, cy, from, to, step = 4) {
|
||||
const hits = [];
|
||||
for (let dy = from; dy <= to; dy += step) {
|
||||
const png = await page.screenshot({
|
||||
clip: { x: Math.round(cx), y: Math.round(cy - dy), width: 1, height: 1 },
|
||||
});
|
||||
hits.push(firstPixelRGB(png));
|
||||
}
|
||||
return hits;
|
||||
}
|
||||
|
||||
async function boot(page) {
|
||||
await page.goto('/IfcViewerWeb.html');
|
||||
await page.waitForFunction(
|
||||
() => !!(window.Module && window.Module._app_ptr), null, { timeout: 30_000 });
|
||||
await page.waitForTimeout(1200);
|
||||
return page.locator('#viewer-canvas').boundingBox();
|
||||
}
|
||||
|
||||
test('corner axis gizmo draws in the bottom-left', async ({ page }) => {
|
||||
const box = await boot(page);
|
||||
// Gizmo box: 110 CSS px square, 10 px in from the bottom-left corner. The
|
||||
// +Z arm runs up from its centre for ~39 px (arm 1.0 in a 1.4 half-extent
|
||||
// ortho, over a 55 px half-box).
|
||||
const cx = box.x + 10 + 55;
|
||||
const cy = box.y + box.height - 10 - 55;
|
||||
const hits = await scanUp(page, cx, cy, 10, 34);
|
||||
expect(
|
||||
hits.some(isAxisBlue),
|
||||
`no +Z arm above the gizmo centre — corner axis missing (sampled ${JSON.stringify(hits)})`,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('pivot triad shows during an orbit drag and clears on release', async ({ page }) => {
|
||||
const box = await boot(page);
|
||||
// The orbit target projects to the viewport centre, and the pivot arms are
|
||||
// 30 CSS px, so the +Z arm runs up from there.
|
||||
const cx = box.x + box.width / 2;
|
||||
const cy = box.y + box.height / 2;
|
||||
|
||||
const before = await scanUp(page, cx, cy, 8, 26);
|
||||
expect(before.some(isAxisBlue), 'pivot visible before any drag').toBe(false);
|
||||
|
||||
await page.mouse.move(cx, cy);
|
||||
await page.mouse.down();
|
||||
await page.mouse.move(cx + 90, cy + 30, { steps: 8 });
|
||||
await page.waitForTimeout(200);
|
||||
const during = await scanUp(page, cx, cy, 8, 26);
|
||||
await page.mouse.up();
|
||||
expect(
|
||||
during.some(isAxisBlue),
|
||||
`no pivot triad mid-drag (sampled ${JSON.stringify(during)})`,
|
||||
).toBe(true);
|
||||
|
||||
// Released without afterglow — the indicator goes on the next frame.
|
||||
await page.waitForTimeout(400);
|
||||
const after = await scanUp(page, cx, cy, 8, 26);
|
||||
expect(after.some(isAxisBlue), 'pivot triad still up after mouse release').toBe(false);
|
||||
});
|
||||
@@ -12,8 +12,10 @@
|
||||
eats pointer events so the drag keeps reaching the canvas. */
|
||||
#marquee { position: fixed; display: none; z-index: 50; pointer-events: none;
|
||||
border: 1px solid #4a9eff; background: rgba(74, 158, 255, 0.15); }
|
||||
/* Log overlay sits bottom-left and never eats pointer events. */
|
||||
#status { position: fixed; bottom: 8px; left: 12px;
|
||||
/* Log overlay sits bottom-left and never eats pointer events. Kept clear
|
||||
of the corner axis gizmo, which the viewport draws in the bottom-left
|
||||
110 CSS px (plus a 10 px margin). */
|
||||
#status { position: fixed; bottom: 8px; left: 132px;
|
||||
max-width: min(60vw, 680px); max-height: 28vh; overflow-y: auto;
|
||||
font-size: 11px;
|
||||
font-family: ui-monospace, "Cascadia Mono", Menlo, Consolas, monospace;
|
||||
|
||||
@@ -0,0 +1,423 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#include "AxisIndicatorRenderer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "CameraMath.h"
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kAxisUniformSlot = 256; // dynamic-offset slot stride
|
||||
constexpr uint32_t kAxisVertexCount = 18; // 3 arms x 2 triangles x 3 verts
|
||||
|
||||
// Uniform slots in the shared buffer.
|
||||
constexpr uint32_t kSlotCorner = 0;
|
||||
constexpr uint32_t kSlotPivot = 1;
|
||||
constexpr uint32_t kSlotPivotXray = 2;
|
||||
|
||||
WGPUStringView svFromCStr(const char* s) {
|
||||
WGPUStringView v;
|
||||
v.data = s;
|
||||
v.length = s ? std::strlen(s) : 0;
|
||||
return v;
|
||||
}
|
||||
|
||||
// Thick-line rendering helper (shared shape with the other overlays) + the
|
||||
// axis vertex shader. Each arm is expanded to a screen-space-thick,
|
||||
// anti-aliased quad.
|
||||
static const std::string AXIS_WGSL = std::string(R"WGSL(
|
||||
struct VsOut {
|
||||
@builtin(position) clip_pos: vec4<f32>,
|
||||
@location(0) color: vec4<f32>,
|
||||
@location(1) side_t: f32,
|
||||
};
|
||||
|
||||
fn thick_line_clip(p_start: vec4<f32>, p_end: vec4<f32>,
|
||||
t: f32, side: f32,
|
||||
viewport_size: vec2<f32>,
|
||||
line_width_px: f32) -> vec4<f32> {
|
||||
let p_here = mix(p_start, p_end, t);
|
||||
let s_start = (p_start.xy / p_start.w) * viewport_size * 0.5;
|
||||
let s_end = (p_end.xy / p_end.w ) * viewport_size * 0.5;
|
||||
let dir = normalize(s_end - s_start);
|
||||
let perp = vec2<f32>(-dir.y, dir.x);
|
||||
let off_pixels = perp * (line_width_px * 0.5) * side;
|
||||
let off_ndc = off_pixels * 2.0 / viewport_size;
|
||||
return vec4<f32>(p_here.xy + off_ndc * p_here.w, p_here.zw);
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
||||
let d = abs(in.side_t);
|
||||
let aa = fwidth(in.side_t);
|
||||
let coverage = 1.0 - smoothstep(1.0 - aa, 1.0, d);
|
||||
return vec4<f32>(in.color.xyz, in.color.w * coverage);
|
||||
}
|
||||
|
||||
struct AxisUniforms {
|
||||
mvp: mat4x4<f32>,
|
||||
origin: vec3<f32>,
|
||||
arm: f32,
|
||||
alpha: f32,
|
||||
line_width_px: f32,
|
||||
viewport_size: vec2<f32>,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> u: AxisUniforms;
|
||||
|
||||
@vertex
|
||||
fn vs_main(@location(0) start: vec3<f32>,
|
||||
@location(1) end: vec3<f32>,
|
||||
@location(2) col: vec3<f32>,
|
||||
@location(3) t: f32,
|
||||
@location(4) side: f32) -> VsOut {
|
||||
let p_start = u.mvp * vec4<f32>(u.origin + start * u.arm, 1.0);
|
||||
let p_end = u.mvp * vec4<f32>(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<f32>(col, u.alpha);
|
||||
out.side_t = side;
|
||||
return out;
|
||||
}
|
||||
)WGSL");
|
||||
|
||||
// Pack the axis uniform's 256-byte slot. Layout matches WGSL AxisUniforms:
|
||||
// mat4 + vec3 + f32 + f32 + f32 + vec2 = 96 B used, padded to 256.
|
||||
void packAxisUniform(uint8_t* dst,
|
||||
const Eigen::Matrix4f& mvp, const Eigen::Vector3f& origin,
|
||||
float arm, float alpha, float line_width_px,
|
||||
float viewport_w, float viewport_h) {
|
||||
std::memset(dst, 0, kAxisUniformSlot);
|
||||
std::memcpy(dst, mvp.data(), 16 * sizeof(float));
|
||||
float ox = origin.x(), oy = origin.y(), oz = origin.z();
|
||||
std::memcpy(dst + 64, &ox, sizeof(float));
|
||||
std::memcpy(dst + 68, &oy, sizeof(float));
|
||||
std::memcpy(dst + 72, &oz, sizeof(float));
|
||||
std::memcpy(dst + 76, &arm, sizeof(float));
|
||||
std::memcpy(dst + 80, &alpha, sizeof(float));
|
||||
std::memcpy(dst + 84, &line_width_px, sizeof(float));
|
||||
std::memcpy(dst + 88, &viewport_w, sizeof(float));
|
||||
std::memcpy(dst + 92, &viewport_h, sizeof(float));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AxisIndicatorRenderer::~AxisIndicatorRenderer() { destroy(); }
|
||||
|
||||
bool AxisIndicatorRenderer::init(WGPUDevice device, WGPUQueue queue,
|
||||
WGPUTextureFormat color_format, int sample_count) {
|
||||
device_ = device;
|
||||
queue_ = queue;
|
||||
if (!device_ || !queue_) return false;
|
||||
|
||||
// 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 vb = {};
|
||||
vb.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst;
|
||||
vb.size = sizeof(axis_verts);
|
||||
vb.label = svFromCStr("ifcviewer-wgpu.axis_vbo");
|
||||
vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &vb);
|
||||
wgpuQueueWriteBuffer(queue_, vertex_buffer_, 0, axis_verts, sizeof(axis_verts));
|
||||
|
||||
WGPUBufferDescriptor ub = {};
|
||||
ub.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
|
||||
ub.size = 3u * kAxisUniformSlot;
|
||||
ub.label = svFromCStr("ifcviewer-wgpu.axis_uniforms");
|
||||
uniform_buffer_ = wgpuDeviceCreateBuffer(device_, &ub);
|
||||
|
||||
WGPUBindGroupLayoutEntry ble = {};
|
||||
ble.binding = 0;
|
||||
ble.visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment;
|
||||
ble.buffer.type = WGPUBufferBindingType_Uniform;
|
||||
ble.buffer.hasDynamicOffset = 1;
|
||||
ble.buffer.minBindingSize = 96;
|
||||
WGPUBindGroupLayoutDescriptor bgl_desc = {};
|
||||
bgl_desc.entryCount = 1;
|
||||
bgl_desc.entries = &ble;
|
||||
bgl_desc.label = svFromCStr("ifcviewer-wgpu.axis_bgl");
|
||||
bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc);
|
||||
|
||||
WGPUPipelineLayoutDescriptor pl_desc = {};
|
||||
pl_desc.bindGroupLayoutCount = 1;
|
||||
pl_desc.bindGroupLayouts = &bgl_;
|
||||
pl_desc.label = svFromCStr("ifcviewer-wgpu.axis_pipeline_layout");
|
||||
layout_ = wgpuDeviceCreatePipelineLayout(device_, &pl_desc);
|
||||
|
||||
WGPUBindGroupEntry bge = {};
|
||||
bge.binding = 0;
|
||||
bge.buffer = uniform_buffer_;
|
||||
bge.offset = 0;
|
||||
bge.size = kAxisUniformSlot;
|
||||
WGPUBindGroupDescriptor bg_desc = {};
|
||||
bg_desc.layout = bgl_;
|
||||
bg_desc.entryCount = 1;
|
||||
bg_desc.entries = &bge;
|
||||
bg_desc.label = svFromCStr("ifcviewer-wgpu.axis_bind_group");
|
||||
bind_group_ = wgpuDeviceCreateBindGroup(device_, &bg_desc);
|
||||
|
||||
WGPUShaderSourceWGSL wgsl = {};
|
||||
wgsl.chain.sType = WGPUSType_ShaderSourceWGSL;
|
||||
wgsl.code = svFromCStr(AXIS_WGSL.c_str());
|
||||
WGPUShaderModuleDescriptor sm_desc = {};
|
||||
sm_desc.nextInChain = &wgsl.chain;
|
||||
sm_desc.label = svFromCStr("ifcviewer-wgpu.axis_wgsl");
|
||||
shader_ = wgpuDeviceCreateShaderModule(device_, &sm_desc);
|
||||
|
||||
// Vertex layout: start vec3, end vec3, col vec3, t f32, side f32.
|
||||
WGPUVertexAttribute attribs[5] = {};
|
||||
attribs[0].format = WGPUVertexFormat_Float32x3; attribs[0].offset = 0; attribs[0].shaderLocation = 0;
|
||||
attribs[1].format = WGPUVertexFormat_Float32x3; attribs[1].offset = 12; attribs[1].shaderLocation = 1;
|
||||
attribs[2].format = WGPUVertexFormat_Float32x3; attribs[2].offset = 24; attribs[2].shaderLocation = 2;
|
||||
attribs[3].format = WGPUVertexFormat_Float32; attribs[3].offset = 36; attribs[3].shaderLocation = 3;
|
||||
attribs[4].format = WGPUVertexFormat_Float32; attribs[4].offset = 40; attribs[4].shaderLocation = 4;
|
||||
WGPUVertexBufferLayout vbl = {};
|
||||
vbl.arrayStride = 44;
|
||||
vbl.stepMode = WGPUVertexStepMode_Vertex;
|
||||
vbl.attributeCount = 5;
|
||||
vbl.attributes = attribs;
|
||||
|
||||
WGPUBlendState blend = {};
|
||||
blend.color.srcFactor = WGPUBlendFactor_SrcAlpha;
|
||||
blend.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
|
||||
blend.color.operation = WGPUBlendOperation_Add;
|
||||
blend.alpha.srcFactor = WGPUBlendFactor_One;
|
||||
blend.alpha.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
|
||||
blend.alpha.operation = WGPUBlendOperation_Add;
|
||||
|
||||
// Pivot: inside the main MSAA pass, depth-tested against the scene but
|
||||
// never writing depth. Two passes — LessEqual for the visible part,
|
||||
// GreaterEqual for the dim x-ray showing through geometry.
|
||||
auto build_pivot = [&](WGPUCompareFunction cmp, const char* label,
|
||||
WGPURenderPipeline& out) {
|
||||
WGPUColorTargetState ct = {};
|
||||
ct.format = color_format;
|
||||
ct.blend = &blend;
|
||||
ct.writeMask = WGPUColorWriteMask_All;
|
||||
|
||||
WGPUFragmentState frag = {};
|
||||
frag.module = shader_;
|
||||
frag.entryPoint = svFromCStr("fs_main");
|
||||
frag.targetCount = 1;
|
||||
frag.targets = &ct;
|
||||
|
||||
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 = {};
|
||||
rp.layout = layout_;
|
||||
rp.label = svFromCStr(label);
|
||||
rp.vertex.module = shader_;
|
||||
rp.vertex.entryPoint = svFromCStr("vs_main");
|
||||
rp.vertex.bufferCount = 1;
|
||||
rp.vertex.buffers = &vbl;
|
||||
rp.fragment = &frag;
|
||||
rp.depthStencil = &depth;
|
||||
rp.primitive.topology = WGPUPrimitiveTopology_TriangleList;
|
||||
rp.primitive.cullMode = WGPUCullMode_None;
|
||||
rp.multisample.count = uint32_t(sample_count);
|
||||
rp.multisample.mask = 0xFFFFFFFFu;
|
||||
out = wgpuDeviceCreateRenderPipeline(device_, &rp);
|
||||
};
|
||||
build_pivot(WGPUCompareFunction_LessEqual,
|
||||
"ifcviewer-wgpu.axis_pivot_pipeline", pivot_pipeline_);
|
||||
build_pivot(WGPUCompareFunction_GreaterEqual,
|
||||
"ifcviewer-wgpu.axis_pivot_xray_pipeline", pivot_xray_pipeline_);
|
||||
|
||||
// Corner: resolved surface, no depth, sampleCount=1.
|
||||
{
|
||||
WGPUColorTargetState ct = {};
|
||||
ct.format = color_format;
|
||||
ct.blend = &blend;
|
||||
ct.writeMask = WGPUColorWriteMask_All;
|
||||
|
||||
WGPUFragmentState frag = {};
|
||||
frag.module = shader_;
|
||||
frag.entryPoint = svFromCStr("fs_main");
|
||||
frag.targetCount = 1;
|
||||
frag.targets = &ct;
|
||||
|
||||
WGPURenderPipelineDescriptor rp = {};
|
||||
rp.layout = layout_;
|
||||
rp.label = svFromCStr("ifcviewer-wgpu.axis_corner_pipeline");
|
||||
rp.vertex.module = shader_;
|
||||
rp.vertex.entryPoint = svFromCStr("vs_main");
|
||||
rp.vertex.bufferCount = 1;
|
||||
rp.vertex.buffers = &vbl;
|
||||
rp.fragment = &frag;
|
||||
rp.primitive.topology = WGPUPrimitiveTopology_TriangleList;
|
||||
rp.primitive.cullMode = WGPUCullMode_None;
|
||||
rp.multisample.count = 1;
|
||||
rp.multisample.mask = 0xFFFFFFFFu;
|
||||
corner_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp);
|
||||
}
|
||||
|
||||
return pivot_pipeline_ && pivot_xray_pipeline_ && corner_pipeline_;
|
||||
}
|
||||
|
||||
void AxisIndicatorRenderer::encodePivot(WGPURenderPassEncoder pass,
|
||||
const OverlayFrame& f, bool visible) {
|
||||
if (!visible || !pivot_pipeline_ || !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 = f.camera_fov_y_deg * kPiF / 180.0f;
|
||||
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[kAxisUniformSlot];
|
||||
uint8_t slot_xray[kAxisUniformSlot];
|
||||
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 = kSlotPivot * kAxisUniformSlot;
|
||||
const uint32_t xray_off = kSlotPivotXray * kAxisUniformSlot;
|
||||
wgpuQueueWriteBuffer(queue_, uniform_buffer_, visible_off,
|
||||
slot_visible, sizeof(slot_visible));
|
||||
wgpuQueueWriteBuffer(queue_, uniform_buffer_, xray_off,
|
||||
slot_xray, sizeof(slot_xray));
|
||||
|
||||
wgpuRenderPassEncoderSetVertexBuffer(pass, 0, vertex_buffer_, 0, WGPU_WHOLE_SIZE);
|
||||
wgpuRenderPassEncoderSetPipeline(pass, pivot_xray_pipeline_);
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 1, &xray_off);
|
||||
wgpuRenderPassEncoderDraw(pass, kAxisVertexCount, 1, 0, 0);
|
||||
wgpuRenderPassEncoderSetPipeline(pass, pivot_pipeline_);
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 1, &visible_off);
|
||||
wgpuRenderPassEncoderDraw(pass, kAxisVertexCount, 1, 0, 0);
|
||||
}
|
||||
|
||||
void AxisIndicatorRenderer::encodeCornerAxis(WGPUCommandEncoder enc,
|
||||
WGPUTextureView surface_view,
|
||||
const OverlayFrame& f) {
|
||||
if (!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 = f.camera_yaw_deg * kPiF / 180.0f;
|
||||
const float pitch_rad = f.camera_pitch_deg * kPiF / 180.0f;
|
||||
const Eigen::Vector3f eye_dir(std::cos(pitch_rad) * std::cos(yaw_rad),
|
||||
std::cos(pitch_rad) * std::sin(yaw_rad),
|
||||
std::sin(pitch_rad));
|
||||
const Eigen::Vector3f world_up = (std::abs(f.camera_pitch_deg) >= 89.0f)
|
||||
? Eigen::Vector3f(0.0f, 1.0f, 0.0f)
|
||||
: Eigen::Vector3f(0.0f, 0.0f, 1.0f);
|
||||
const Eigen::Matrix4f gv = lookAtRH(eye_dir * 3.0f, Eigen::Vector3f::Zero(), world_up);
|
||||
const Eigen::Matrix4f gp = orthoGL(-1.4f, 1.4f, -1.4f, 1.4f, 0.1f, 10.0f);
|
||||
Eigen::Matrix4f z_remap = Eigen::Matrix4f::Identity();
|
||||
z_remap(2, 2) = 0.5f;
|
||||
z_remap(2, 3) = 0.5f;
|
||||
const Eigen::Matrix4f mvp = z_remap * gp * gv;
|
||||
|
||||
uint8_t slot[kAxisUniformSlot];
|
||||
const float line_w = 2.5f * float(dpr);
|
||||
packAxisUniform(slot, mvp, Eigen::Vector3f(0, 0, 0), 1.0f, 1.0f, line_w,
|
||||
float(gizmo_size), float(gizmo_size));
|
||||
const uint32_t slot_offset = kSlotCorner * kAxisUniformSlot;
|
||||
wgpuQueueWriteBuffer(queue_, 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, corner_pipeline_);
|
||||
wgpuRenderPassEncoderSetVertexBuffer(pass, 0, vertex_buffer_, 0, WGPU_WHOLE_SIZE);
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, bind_group_, 1, &slot_offset);
|
||||
wgpuRenderPassEncoderDraw(pass, kAxisVertexCount, 1, 0, 0);
|
||||
wgpuRenderPassEncoderEnd(pass);
|
||||
wgpuRenderPassEncoderRelease(pass);
|
||||
}
|
||||
|
||||
void AxisIndicatorRenderer::destroy() {
|
||||
if (pivot_pipeline_) { wgpuRenderPipelineRelease(pivot_pipeline_); pivot_pipeline_ = nullptr; }
|
||||
if (pivot_xray_pipeline_) { wgpuRenderPipelineRelease(pivot_xray_pipeline_); pivot_xray_pipeline_ = nullptr; }
|
||||
if (corner_pipeline_) { wgpuRenderPipelineRelease(corner_pipeline_); corner_pipeline_ = nullptr; }
|
||||
if (layout_) { wgpuPipelineLayoutRelease(layout_); layout_ = nullptr; }
|
||||
if (bgl_) { wgpuBindGroupLayoutRelease(bgl_); bgl_ = nullptr; }
|
||||
if (bind_group_) { wgpuBindGroupRelease(bind_group_); bind_group_ = nullptr; }
|
||||
if (vertex_buffer_) { wgpuBufferRelease(vertex_buffer_); vertex_buffer_ = nullptr; }
|
||||
if (uniform_buffer_) { wgpuBufferRelease(uniform_buffer_); uniform_buffer_ = nullptr; }
|
||||
if (shader_) { wgpuShaderModuleRelease(shader_); shader_ = nullptr; }
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* This file is part of IfcOpenShell. *
|
||||
* *
|
||||
* IfcOpenShell is free software: you can redistribute it and/or modify *
|
||||
* it under the terms of the Lesser GNU General Public License as published by *
|
||||
* the Free Software Foundation, either version 3.0 of the License, or *
|
||||
* (at your option) any later version. *
|
||||
* *
|
||||
* IfcOpenShell is distributed in the hope that it will be useful, *
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
|
||||
* Lesser GNU General Public License for more details. *
|
||||
* *
|
||||
* You should have received a copy of the Lesser GNU General Public License *
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
#ifndef AXISINDICATORRENDERER_H
|
||||
#define AXISINDICATORRENDERER_H
|
||||
|
||||
#include <webgpu/webgpu.h>
|
||||
|
||||
#include <Eigen/Dense>
|
||||
|
||||
#include "OverlayFrame.h"
|
||||
|
||||
// Qt-free renderer for the RGB axis indicator, in its two guises:
|
||||
//
|
||||
// - the corner gizmo: a fixed 110x110 px triad in the viewport's
|
||||
// bottom-left corner, drawn on the resolved surface with its own ortho
|
||||
// projection so only the camera's direction moves it;
|
||||
// - the pivot indicator: the same triad drawn in world space at the orbit
|
||||
// target while the user is navigating, depth-tested against the scene
|
||||
// with a dim x-ray pass behind it.
|
||||
//
|
||||
// Lifted out of the Qt-coupled OverlayRenderer so BOTH the desktop and web
|
||||
// builds draw one identical indicator from a single place (ViewportCore::render
|
||||
// calls it on both) — same move SectionGizmoRenderer made.
|
||||
class AxisIndicatorRenderer {
|
||||
public:
|
||||
AxisIndicatorRenderer() = default;
|
||||
~AxisIndicatorRenderer();
|
||||
AxisIndicatorRenderer(const AxisIndicatorRenderer&) = delete;
|
||||
AxisIndicatorRenderer& operator=(const AxisIndicatorRenderer&) = delete;
|
||||
|
||||
// Create the shared triad VBO, the uniform buffer (three dynamic-offset
|
||||
// slots: corner / pivot / pivot-xray), and the three pipelines.
|
||||
// `color_format` is the render target's format; `sample_count` the MSAA
|
||||
// count of the main pass the pivot draws into (the corner gizmo always
|
||||
// targets the resolved, single-sampled surface). Returns false — and
|
||||
// leaves the renderer inert — if pipeline creation fails.
|
||||
bool init(WGPUDevice device, WGPUQueue queue,
|
||||
WGPUTextureFormat color_format, int sample_count);
|
||||
void destroy();
|
||||
bool ready() const { return corner_pipeline_ != nullptr; }
|
||||
|
||||
// Orbit pivot indicator, drawn into the already-open main MSAA pass so it
|
||||
// shares depth with the scene. `visible` is the viewport's UI gate (orbit /
|
||||
// pan drag, wheel-zoom afterglow); when false this is a cheap no-op.
|
||||
void encodePivot(WGPURenderPassEncoder pass, const OverlayFrame& f,
|
||||
bool visible);
|
||||
|
||||
// Corner axis gizmo (bottom-left, 110x110 px). Opens its own load-op pass
|
||||
// on the resolved surface, so it must run after the main pass has resolved.
|
||||
void encodeCornerAxis(WGPUCommandEncoder enc, WGPUTextureView surface_view,
|
||||
const OverlayFrame& f);
|
||||
|
||||
private:
|
||||
WGPUDevice device_ = nullptr;
|
||||
WGPUQueue queue_ = nullptr;
|
||||
WGPUShaderModule shader_ = nullptr;
|
||||
WGPUBindGroupLayout bgl_ = nullptr;
|
||||
WGPUPipelineLayout layout_ = nullptr;
|
||||
WGPUBindGroup bind_group_ = nullptr;
|
||||
WGPUBuffer vertex_buffer_ = nullptr;
|
||||
WGPUBuffer uniform_buffer_ = nullptr;
|
||||
WGPURenderPipeline pivot_pipeline_ = nullptr;
|
||||
WGPURenderPipeline pivot_xray_pipeline_ = nullptr;
|
||||
WGPURenderPipeline corner_pipeline_ = nullptr;
|
||||
};
|
||||
|
||||
#endif // AXISINDICATORRENDERER_H
|
||||
@@ -139,6 +139,7 @@ endif()
|
||||
#
|
||||
# Keep this list explicit (no glob) — the boundary is the whole point.
|
||||
set(IFCVIEWER_CORE_SOURCES
|
||||
AxisIndicatorRenderer.cpp
|
||||
BufferPool.cpp
|
||||
ChunkPlanner.cpp
|
||||
InstanceCompose.cpp
|
||||
@@ -187,6 +188,7 @@ if(EMSCRIPTEN)
|
||||
# below which would mangle these absolute paths; added via target_sources.
|
||||
endif()
|
||||
set(IFCVIEWER_CORE_HEADERS
|
||||
AxisIndicatorRenderer.h
|
||||
BufferPool.h
|
||||
CameraMath.h
|
||||
ChunkPlanner.h
|
||||
|
||||
@@ -19,15 +19,12 @@
|
||||
|
||||
#include "OverlayRenderer.h"
|
||||
|
||||
#include "CameraMath.h"
|
||||
|
||||
#include <QFont>
|
||||
#include <QFontMetrics>
|
||||
#include <QImage>
|
||||
#include <QPainter>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
#include <QtMath>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
@@ -49,44 +46,6 @@ WGPUStringView svFromCStr(const char* s) {
|
||||
return v;
|
||||
}
|
||||
|
||||
// Populate `attribs[5]` with the standard thick-line vertex layout:
|
||||
// loc 0: start (vec3 @ 0) loc 1: end (vec3 @ 12)
|
||||
// loc 2: col (vec3 @ 24) loc 3: t (f32 @ 36)
|
||||
// loc 4: side (f32 @ 40)
|
||||
// Returns a WGPUVertexBufferLayout aliasing the caller-owned `attribs`.
|
||||
WGPUVertexBufferLayout thickLineVertexLayout(WGPUVertexAttribute attribs[5]) {
|
||||
attribs[0].format = WGPUVertexFormat_Float32x3; attribs[0].offset = 0; attribs[0].shaderLocation = 0;
|
||||
attribs[1].format = WGPUVertexFormat_Float32x3; attribs[1].offset = 12; attribs[1].shaderLocation = 1;
|
||||
attribs[2].format = WGPUVertexFormat_Float32x3; attribs[2].offset = 24; attribs[2].shaderLocation = 2;
|
||||
attribs[3].format = WGPUVertexFormat_Float32; attribs[3].offset = 36; attribs[3].shaderLocation = 3;
|
||||
attribs[4].format = WGPUVertexFormat_Float32; attribs[4].offset = 40; attribs[4].shaderLocation = 4;
|
||||
WGPUVertexBufferLayout vbl = {};
|
||||
vbl.arrayStride = 44;
|
||||
vbl.stepMode = WGPUVertexStepMode_Vertex;
|
||||
vbl.attributeCount = 5;
|
||||
vbl.attributes = attribs;
|
||||
return vbl;
|
||||
}
|
||||
|
||||
// Pack the axis uniform's 256-byte slot. Layout matches WGSL AxisUniforms:
|
||||
// mat4 + vec3 + f32 + f32 + f32 + vec2 = 96 B used, padded to 256.
|
||||
void packAxisUniform(uint8_t* dst,
|
||||
const Eigen::Matrix4f& mvp, const Eigen::Vector3f& origin,
|
||||
float arm, float alpha, float line_width_px,
|
||||
float viewport_w, float viewport_h) {
|
||||
std::memset(dst, 0, 256);
|
||||
std::memcpy(dst, mvp.data(), 16 * sizeof(float));
|
||||
float ox = origin.x(), oy = origin.y(), oz = origin.z();
|
||||
std::memcpy(dst + 64, &ox, sizeof(float));
|
||||
std::memcpy(dst + 68, &oy, sizeof(float));
|
||||
std::memcpy(dst + 72, &oz, sizeof(float));
|
||||
std::memcpy(dst + 76, &arm, sizeof(float));
|
||||
std::memcpy(dst + 80, &alpha, sizeof(float));
|
||||
std::memcpy(dst + 84, &line_width_px, sizeof(float));
|
||||
std::memcpy(dst + 88, &viewport_w, sizeof(float));
|
||||
std::memcpy(dst + 92, &viewport_h, sizeof(float));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -126,35 +85,6 @@ fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
||||
}
|
||||
)WGSL";
|
||||
|
||||
static const std::string AXIS_WGSL = std::string(THICK_LINE_HELPERS_WGSL) + R"WGSL(
|
||||
struct AxisUniforms {
|
||||
mvp: mat4x4<f32>,
|
||||
origin: vec3<f32>,
|
||||
arm: f32,
|
||||
alpha: f32,
|
||||
line_width_px: f32,
|
||||
viewport_size: vec2<f32>,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> u: AxisUniforms;
|
||||
|
||||
@vertex
|
||||
fn vs_main(@location(0) start: vec3<f32>,
|
||||
@location(1) end: vec3<f32>,
|
||||
@location(2) col: vec3<f32>,
|
||||
@location(3) t: f32,
|
||||
@location(4) side: f32) -> VsOut {
|
||||
let p_start = u.mvp * vec4<f32>(u.origin + start * u.arm, 1.0);
|
||||
let p_end = u.mvp * vec4<f32>(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<f32>(col, u.alpha);
|
||||
out.side_t = side;
|
||||
return out;
|
||||
}
|
||||
)WGSL";
|
||||
|
||||
static const std::string MARQUEE_WGSL = std::string(THICK_LINE_HELPERS_WGSL) + R"WGSL(
|
||||
struct MarqueeUniforms {
|
||||
rect_min: vec2<f32>,
|
||||
@@ -383,7 +313,6 @@ bool OverlayRenderer::init(WGPUInstance instance, WGPUDevice device,
|
||||
queue_ = queue;
|
||||
surface_format_ = surface_format;
|
||||
sample_count_ = sample_count;
|
||||
if (!buildAxisIndicator()) return false;
|
||||
// Section-plane gizmos moved to the shared SectionGizmoRenderer (ViewportCore).
|
||||
if (!buildMarquee()) return false;
|
||||
if (!buildOverlayLines()) return false;
|
||||
@@ -394,17 +323,6 @@ bool OverlayRenderer::init(WGPUInstance instance, WGPUDevice device,
|
||||
}
|
||||
|
||||
void OverlayRenderer::destroy() {
|
||||
// Axis indicator
|
||||
if (axis_bind_group_) { wgpuBindGroupRelease(axis_bind_group_); axis_bind_group_ = nullptr; }
|
||||
if (axis_pivot_pipeline_) { wgpuRenderPipelineRelease(axis_pivot_pipeline_); axis_pivot_pipeline_ = nullptr; }
|
||||
if (axis_pivot_xray_pipeline_){ wgpuRenderPipelineRelease(axis_pivot_xray_pipeline_); axis_pivot_xray_pipeline_ = nullptr; }
|
||||
if (axis_corner_pipeline_) { wgpuRenderPipelineRelease(axis_corner_pipeline_); axis_corner_pipeline_ = nullptr; }
|
||||
if (axis_shader_module_) { wgpuShaderModuleRelease(axis_shader_module_); axis_shader_module_ = nullptr; }
|
||||
if (axis_pipeline_layout_) { wgpuPipelineLayoutRelease(axis_pipeline_layout_); axis_pipeline_layout_ = nullptr; }
|
||||
if (axis_bgl_) { wgpuBindGroupLayoutRelease(axis_bgl_); axis_bgl_ = nullptr; }
|
||||
if (axis_uniform_buffer_) { wgpuBufferRelease(axis_uniform_buffer_); axis_uniform_buffer_ = nullptr; }
|
||||
if (axis_vertex_buffer_) { wgpuBufferRelease(axis_vertex_buffer_); axis_vertex_buffer_ = nullptr; }
|
||||
|
||||
// Section visualizer
|
||||
|
||||
// Marquee
|
||||
@@ -466,288 +384,6 @@ void OverlayRenderer::destroy() {
|
||||
hud_text_.clear();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Axis indicator
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
bool OverlayRenderer::buildAxisIndicator() {
|
||||
// Bonsai decorator palette (src/bonsai/bonsai/bim/ui.py:593+):
|
||||
// decorator_color_error = (1.000, 0.200, 0.322) — red → +X
|
||||
// decorator_color_selected = (0.545, 0.863, 0.000) — green → +Y
|
||||
// decorator_color_special = (0.157, 0.565, 1.000) — blue → +Z
|
||||
// Same palette is reused for the section gizmo + marquee so all overlay
|
||||
// colours come from one canonical source.
|
||||
static const float axis_verts[] = {
|
||||
// start end color (RGB — Bonsai decorators) t side
|
||||
// ---- +X red ----
|
||||
0,0,0, 1,0,0, 1.000f, 0.200f, 0.322f, 0.f, -1.f,
|
||||
0,0,0, 1,0,0, 1.000f, 0.200f, 0.322f, 0.f, +1.f,
|
||||
0,0,0, 1,0,0, 1.000f, 0.200f, 0.322f, 1.f, -1.f,
|
||||
0,0,0, 1,0,0, 1.000f, 0.200f, 0.322f, 1.f, -1.f,
|
||||
0,0,0, 1,0,0, 1.000f, 0.200f, 0.322f, 0.f, +1.f,
|
||||
0,0,0, 1,0,0, 1.000f, 0.200f, 0.322f, 1.f, +1.f,
|
||||
// ---- +Y green ----
|
||||
0,0,0, 0,1,0, 0.545f, 0.863f, 0.000f, 0.f, -1.f,
|
||||
0,0,0, 0,1,0, 0.545f, 0.863f, 0.000f, 0.f, +1.f,
|
||||
0,0,0, 0,1,0, 0.545f, 0.863f, 0.000f, 1.f, -1.f,
|
||||
0,0,0, 0,1,0, 0.545f, 0.863f, 0.000f, 1.f, -1.f,
|
||||
0,0,0, 0,1,0, 0.545f, 0.863f, 0.000f, 0.f, +1.f,
|
||||
0,0,0, 0,1,0, 0.545f, 0.863f, 0.000f, 1.f, +1.f,
|
||||
// ---- +Z blue ----
|
||||
0,0,0, 0,0,1, 0.157f, 0.565f, 1.000f, 0.f, -1.f,
|
||||
0,0,0, 0,0,1, 0.157f, 0.565f, 1.000f, 0.f, +1.f,
|
||||
0,0,0, 0,0,1, 0.157f, 0.565f, 1.000f, 1.f, -1.f,
|
||||
0,0,0, 0,0,1, 0.157f, 0.565f, 1.000f, 1.f, -1.f,
|
||||
0,0,0, 0,0,1, 0.157f, 0.565f, 1.000f, 0.f, +1.f,
|
||||
0,0,0, 0,0,1, 0.157f, 0.565f, 1.000f, 1.f, +1.f,
|
||||
};
|
||||
{
|
||||
WGPUBufferDescriptor bdesc = {};
|
||||
bdesc.usage = WGPUBufferUsage_Vertex | WGPUBufferUsage_CopyDst;
|
||||
bdesc.size = sizeof(axis_verts);
|
||||
bdesc.label = svFromCStr("ifcviewer-wgpu.axis_vbo");
|
||||
axis_vertex_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc);
|
||||
wgpuQueueWriteBuffer(queue_, axis_vertex_buffer_, 0, axis_verts, sizeof(axis_verts));
|
||||
}
|
||||
{
|
||||
WGPUBufferDescriptor bdesc = {};
|
||||
bdesc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
|
||||
bdesc.size = 3u * kAxisUniformSlotSize;
|
||||
bdesc.label = svFromCStr("ifcviewer-wgpu.axis_uniforms");
|
||||
axis_uniform_buffer_ = wgpuDeviceCreateBuffer(device_, &bdesc);
|
||||
}
|
||||
{
|
||||
WGPUBindGroupLayoutEntry entry = {};
|
||||
entry.binding = 0;
|
||||
entry.visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment;
|
||||
entry.buffer.type = WGPUBufferBindingType_Uniform;
|
||||
entry.buffer.hasDynamicOffset = 1;
|
||||
entry.buffer.minBindingSize = 96;
|
||||
WGPUBindGroupLayoutDescriptor bgl_desc = {};
|
||||
bgl_desc.entryCount = 1;
|
||||
bgl_desc.entries = &entry;
|
||||
bgl_desc.label = svFromCStr("ifcviewer-wgpu.axis_bgl");
|
||||
axis_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc);
|
||||
}
|
||||
{
|
||||
WGPUPipelineLayoutDescriptor pl_desc = {};
|
||||
pl_desc.bindGroupLayoutCount = 1;
|
||||
pl_desc.bindGroupLayouts = &axis_bgl_;
|
||||
pl_desc.label = svFromCStr("ifcviewer-wgpu.axis_pipeline_layout");
|
||||
axis_pipeline_layout_ = wgpuDeviceCreatePipelineLayout(device_, &pl_desc);
|
||||
}
|
||||
{
|
||||
WGPUBindGroupEntry entry = {};
|
||||
entry.binding = 0;
|
||||
entry.buffer = axis_uniform_buffer_;
|
||||
entry.offset = 0;
|
||||
entry.size = kAxisUniformSlotSize;
|
||||
WGPUBindGroupDescriptor bg_desc = {};
|
||||
bg_desc.layout = axis_bgl_;
|
||||
bg_desc.entryCount = 1;
|
||||
bg_desc.entries = &entry;
|
||||
bg_desc.label = svFromCStr("ifcviewer-wgpu.axis_bind_group");
|
||||
axis_bind_group_ = wgpuDeviceCreateBindGroup(device_, &bg_desc);
|
||||
}
|
||||
{
|
||||
WGPUShaderSourceWGSL wgsl_src = {};
|
||||
wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL;
|
||||
wgsl_src.code = svFromCStr(AXIS_WGSL.c_str());
|
||||
WGPUShaderModuleDescriptor sm_desc = {};
|
||||
sm_desc.nextInChain = &wgsl_src.chain;
|
||||
sm_desc.label = svFromCStr("ifcviewer-wgpu.axis_wgsl");
|
||||
axis_shader_module_ = wgpuDeviceCreateShaderModule(device_, &sm_desc);
|
||||
}
|
||||
|
||||
WGPUVertexAttribute attribs[5] = {};
|
||||
WGPUVertexBufferLayout vbl = thickLineVertexLayout(attribs);
|
||||
|
||||
WGPUBlendState blend = {};
|
||||
blend.color.srcFactor = WGPUBlendFactor_SrcAlpha;
|
||||
blend.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
|
||||
blend.color.operation = WGPUBlendOperation_Add;
|
||||
blend.alpha.srcFactor = WGPUBlendFactor_One;
|
||||
blend.alpha.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
|
||||
blend.alpha.operation = WGPUBlendOperation_Add;
|
||||
|
||||
auto build_pivot = [&](WGPUCompareFunction cmp, const char* label,
|
||||
WGPURenderPipeline& out) {
|
||||
WGPUColorTargetState ct = {};
|
||||
ct.format = surface_format_;
|
||||
ct.blend = &blend;
|
||||
ct.writeMask = WGPUColorWriteMask_All;
|
||||
|
||||
WGPUFragmentState frag = {};
|
||||
frag.module = axis_shader_module_;
|
||||
frag.entryPoint = svFromCStr("fs_main");
|
||||
frag.targetCount = 1;
|
||||
frag.targets = &ct;
|
||||
|
||||
WGPUDepthStencilState depth = {};
|
||||
depth.format = WGPUTextureFormat_Depth32Float;
|
||||
depth.depthWriteEnabled = WGPUOptionalBool_False;
|
||||
depth.depthCompare = cmp;
|
||||
depth.stencilFront.compare = WGPUCompareFunction_Always;
|
||||
depth.stencilBack.compare = WGPUCompareFunction_Always;
|
||||
|
||||
WGPURenderPipelineDescriptor rp_desc = {};
|
||||
rp_desc.layout = axis_pipeline_layout_;
|
||||
rp_desc.label = svFromCStr(label);
|
||||
rp_desc.vertex.module = axis_shader_module_;
|
||||
rp_desc.vertex.entryPoint = svFromCStr("vs_main");
|
||||
rp_desc.vertex.bufferCount = 1;
|
||||
rp_desc.vertex.buffers = &vbl;
|
||||
rp_desc.fragment = &frag;
|
||||
rp_desc.depthStencil = &depth;
|
||||
rp_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
|
||||
rp_desc.primitive.cullMode = WGPUCullMode_None;
|
||||
rp_desc.multisample.count = uint32_t(sample_count_);
|
||||
rp_desc.multisample.mask = 0xFFFFFFFFu;
|
||||
out = wgpuDeviceCreateRenderPipeline(device_, &rp_desc);
|
||||
};
|
||||
build_pivot(WGPUCompareFunction_LessEqual,
|
||||
"ifcviewer-wgpu.axis_pivot_pipeline",
|
||||
axis_pivot_pipeline_);
|
||||
build_pivot(WGPUCompareFunction_GreaterEqual,
|
||||
"ifcviewer-wgpu.axis_pivot_xray_pipeline",
|
||||
axis_pivot_xray_pipeline_);
|
||||
|
||||
// Corner: resolved surface, no depth, sampleCount=1.
|
||||
{
|
||||
WGPUColorTargetState ct = {};
|
||||
ct.format = surface_format_;
|
||||
ct.blend = &blend;
|
||||
ct.writeMask = WGPUColorWriteMask_All;
|
||||
|
||||
WGPUFragmentState frag = {};
|
||||
frag.module = axis_shader_module_;
|
||||
frag.entryPoint = svFromCStr("fs_main");
|
||||
frag.targetCount = 1;
|
||||
frag.targets = &ct;
|
||||
|
||||
WGPURenderPipelineDescriptor rp_desc = {};
|
||||
rp_desc.layout = axis_pipeline_layout_;
|
||||
rp_desc.label = svFromCStr("ifcviewer-wgpu.axis_corner_pipeline");
|
||||
rp_desc.vertex.module = axis_shader_module_;
|
||||
rp_desc.vertex.entryPoint = svFromCStr("vs_main");
|
||||
rp_desc.vertex.bufferCount = 1;
|
||||
rp_desc.vertex.buffers = &vbl;
|
||||
rp_desc.fragment = &frag;
|
||||
rp_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
|
||||
rp_desc.primitive.cullMode = WGPUCullMode_None;
|
||||
rp_desc.multisample.count = 1;
|
||||
rp_desc.multisample.mask = 0xFFFFFFFFu;
|
||||
axis_corner_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp_desc);
|
||||
}
|
||||
|
||||
return axis_pivot_pipeline_ && axis_pivot_xray_pipeline_
|
||||
&& axis_corner_pipeline_;
|
||||
}
|
||||
|
||||
void OverlayRenderer::encodePivot(WGPURenderPassEncoder pass,
|
||||
const OverlayFrame& f,
|
||||
bool visible) {
|
||||
if (!visible || !axis_pivot_pipeline_ || !axis_pivot_xray_pipeline_) return;
|
||||
if (f.viewport_h_px <= 0) return;
|
||||
|
||||
// Arm length = 30 logical px projected into world at the pivot's distance.
|
||||
const float fovy_rad = qDegreesToRadians(f.camera_fov_y_deg);
|
||||
const float world_per_pixel = f.camera_distance * std::tan(fovy_rad * 0.5f)
|
||||
* 2.0f / float(f.viewport_h_px);
|
||||
const float arm_pixels = 30.0f * float(f.device_pixel_ratio);
|
||||
const float arm_world = arm_pixels * world_per_pixel;
|
||||
|
||||
const float dpr = float(f.device_pixel_ratio);
|
||||
const float line_w = 2.5f * dpr;
|
||||
const float vw = float(f.viewport_w_px);
|
||||
const float vh = float(f.viewport_h_px);
|
||||
|
||||
uint8_t slot_visible[256];
|
||||
uint8_t slot_xray[256];
|
||||
packAxisUniform(slot_visible, f.view_proj, f.camera_target, arm_world,
|
||||
1.00f, line_w, vw, vh);
|
||||
packAxisUniform(slot_xray, f.view_proj, f.camera_target, arm_world,
|
||||
0.30f, line_w, vw, vh);
|
||||
const uint32_t visible_off = 1u * kAxisUniformSlotSize;
|
||||
const uint32_t xray_off = 2u * kAxisUniformSlotSize;
|
||||
wgpuQueueWriteBuffer(queue_, axis_uniform_buffer_, visible_off,
|
||||
slot_visible, sizeof(slot_visible));
|
||||
wgpuQueueWriteBuffer(queue_, axis_uniform_buffer_, xray_off,
|
||||
slot_xray, sizeof(slot_xray));
|
||||
|
||||
wgpuRenderPassEncoderSetVertexBuffer(pass, 0, axis_vertex_buffer_, 0,
|
||||
WGPU_WHOLE_SIZE);
|
||||
wgpuRenderPassEncoderSetPipeline(pass, axis_pivot_xray_pipeline_);
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, axis_bind_group_, 1, &xray_off);
|
||||
wgpuRenderPassEncoderDraw(pass, 18, 1, 0, 0);
|
||||
wgpuRenderPassEncoderSetPipeline(pass, axis_pivot_pipeline_);
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, axis_bind_group_, 1, &visible_off);
|
||||
wgpuRenderPassEncoderDraw(pass, 18, 1, 0, 0);
|
||||
}
|
||||
|
||||
void OverlayRenderer::encodeCornerAxis(WGPUCommandEncoder enc,
|
||||
WGPUTextureView surface_view,
|
||||
const OverlayFrame& f) {
|
||||
if (!axis_corner_pipeline_ || !surface_view) return;
|
||||
const int dpr = std::max(1, f.device_pixel_ratio);
|
||||
const uint32_t gizmo_size = uint32_t(110 * dpr);
|
||||
const uint32_t margin = uint32_t(10 * dpr);
|
||||
if (gizmo_size == 0 || f.viewport_w_px <= 0 || f.viewport_h_px <= 0) return;
|
||||
// Bottom-left in WebGPU framebuffer space (y down).
|
||||
const uint32_t fb_h = uint32_t(f.viewport_h_px);
|
||||
if (gizmo_size + margin > fb_h) return;
|
||||
const uint32_t y = fb_h - margin - gizmo_size;
|
||||
|
||||
// Independent ortho projection from the camera's direction. Near the
|
||||
// poles the up axis collapses against the look direction, so swap to
|
||||
// Y-up there — mirrors buildViewProj's identical fix on the viewport.
|
||||
const float yaw_rad = qDegreesToRadians(f.camera_yaw_deg);
|
||||
const float pitch_rad = qDegreesToRadians(f.camera_pitch_deg);
|
||||
const Eigen::Vector3f eye_dir(std::cos(pitch_rad) * std::cos(yaw_rad),
|
||||
std::cos(pitch_rad) * std::sin(yaw_rad),
|
||||
std::sin(pitch_rad));
|
||||
const Eigen::Vector3f world_up = (std::abs(f.camera_pitch_deg) >= 89.0f)
|
||||
? Eigen::Vector3f(0.0f, 1.0f, 0.0f)
|
||||
: Eigen::Vector3f(0.0f, 0.0f, 1.0f);
|
||||
const Eigen::Matrix4f gv = lookAtRH(eye_dir * 3.0f, Eigen::Vector3f::Zero(), world_up);
|
||||
const Eigen::Matrix4f gp = orthoGL(-1.4f, 1.4f, -1.4f, 1.4f, 0.1f, 10.0f);
|
||||
Eigen::Matrix4f z_remap = Eigen::Matrix4f::Identity();
|
||||
z_remap(2, 2) = 0.5f;
|
||||
z_remap(2, 3) = 0.5f;
|
||||
const Eigen::Matrix4f mvp = z_remap * gp * gv;
|
||||
|
||||
uint8_t slot[256];
|
||||
const float line_w = 2.5f * float(dpr);
|
||||
packAxisUniform(slot, mvp, Eigen::Vector3f(0, 0, 0), 1.0f, 1.0f, line_w,
|
||||
float(gizmo_size), float(gizmo_size));
|
||||
const uint32_t slot_offset = 0u;
|
||||
wgpuQueueWriteBuffer(queue_, axis_uniform_buffer_, slot_offset, slot, sizeof(slot));
|
||||
|
||||
WGPURenderPassColorAttachment color = {};
|
||||
color.view = surface_view;
|
||||
color.loadOp = WGPULoadOp_Load;
|
||||
color.storeOp = WGPUStoreOp_Store;
|
||||
color.clearValue = { 0.0, 0.0, 0.0, 1.0 };
|
||||
color.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
|
||||
|
||||
WGPURenderPassDescriptor pass_desc = {};
|
||||
pass_desc.colorAttachmentCount = 1;
|
||||
pass_desc.colorAttachments = &color;
|
||||
pass_desc.label = svFromCStr("ifcviewer-wgpu.corner_axis_pass");
|
||||
|
||||
WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(enc, &pass_desc);
|
||||
wgpuRenderPassEncoderSetViewport(pass, float(margin), float(y),
|
||||
float(gizmo_size), float(gizmo_size),
|
||||
0.0f, 1.0f);
|
||||
wgpuRenderPassEncoderSetPipeline(pass, axis_corner_pipeline_);
|
||||
wgpuRenderPassEncoderSetVertexBuffer(pass, 0, axis_vertex_buffer_, 0,
|
||||
WGPU_WHOLE_SIZE);
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, axis_bind_group_, 1, &slot_offset);
|
||||
wgpuRenderPassEncoderDraw(pass, 18, 1, 0, 0);
|
||||
wgpuRenderPassEncoderEnd(pass);
|
||||
wgpuRenderPassEncoderRelease(pass);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Marquee
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -33,10 +33,14 @@
|
||||
#include "OverlayFrame.h"
|
||||
#include "SectionPlane.h"
|
||||
|
||||
// All viewport overlays in one place: axis indicator (corner + pivot),
|
||||
// section plane gizmos, and the marquee drag rect. Mirrors GL's
|
||||
// OverlayRenderer split so ViewportWindow.cpp doesn't have to
|
||||
// carry ~1.5k lines of pipeline plumbing.
|
||||
// The Qt-coupled viewport overlays: the marquee drag rect, measure-tool
|
||||
// lines / points / highlight patches, and the QPainter-rasterised labels
|
||||
// and HUD. Mirrors GL's OverlayRenderer split so ViewportWindow.cpp
|
||||
// doesn't have to carry ~1.5k lines of pipeline plumbing.
|
||||
//
|
||||
// The Qt-free overlays live in their own shared renderers so the web build
|
||||
// gets them too: SectionGizmoRenderer and AxisIndicatorRenderer (corner
|
||||
// axis gizmo + orbit pivot), both driven by ViewportCore::render.
|
||||
//
|
||||
// Lifecycle: init() once after the device is up, destroy() before the
|
||||
// device dies. Pipelines are immutable after init; only per-frame
|
||||
@@ -56,16 +60,11 @@ public:
|
||||
void destroy();
|
||||
|
||||
// ---- Inside the main MSAA pass, after geometry ----
|
||||
// Both share depth with the scene so they're correctly occluded.
|
||||
// These 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 OverlayFrame& f,
|
||||
bool visible);
|
||||
|
||||
// Section-plane gizmos moved to the shared SectionGizmoRenderer (drawn by
|
||||
// ViewportCore for both desktop + web).
|
||||
// Section-plane gizmos moved to the shared SectionGizmoRenderer, and the
|
||||
// orbit pivot to AxisIndicatorRenderer (both drawn by ViewportCore for
|
||||
// desktop + web).
|
||||
|
||||
// Replace the highlight-triangle list. `world_xyz` is 3 floats per
|
||||
// vertex, 3 vertices per triangle, in world space (post-composed-
|
||||
@@ -148,12 +147,8 @@ public:
|
||||
const OverlayFrame& f);
|
||||
|
||||
// ---- 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 OverlayFrame& f);
|
||||
// (The corner axis gizmo also draws here — from ViewportCore, via
|
||||
// AxisIndicatorRenderer.)
|
||||
|
||||
// Marquee box-select drag rect (translucent fill + thick outline).
|
||||
// No-op when `active` is false.
|
||||
@@ -170,7 +165,6 @@ public:
|
||||
static constexpr int kMaxSectionPlanes = 6;
|
||||
|
||||
private:
|
||||
bool buildAxisIndicator();
|
||||
bool buildMarquee();
|
||||
bool buildOverlayLines();
|
||||
bool buildOverlayPoints();
|
||||
@@ -200,19 +194,6 @@ private:
|
||||
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;
|
||||
|
||||
// ---- Marquee (fill + outline pipelines, one uniform buffer) ----
|
||||
WGPUShaderModule marquee_shader_module_ = nullptr;
|
||||
WGPUBindGroupLayout marquee_bgl_ = nullptr;
|
||||
|
||||
@@ -576,6 +576,21 @@ void ViewportCore::dollyBy(float notches) {
|
||||
host_->requestFrame();
|
||||
}
|
||||
|
||||
void ViewportCore::setPivotIndicatorVisible(bool visible, int hide_after_ms) {
|
||||
pivot_indicator_visible_ = visible;
|
||||
pivot_indicator_hide_ms_ = hide_after_ms;
|
||||
if (visible && hide_after_ms > 0) pivot_indicator_timer_.start();
|
||||
else pivot_indicator_timer_.invalidate();
|
||||
host_->requestFrame();
|
||||
}
|
||||
|
||||
bool ViewportCore::pivotIndicatorVisible() const {
|
||||
if (!pivot_indicator_visible_) return false;
|
||||
// No armed afterglow means a drag is holding it up.
|
||||
if (!pivot_indicator_timer_.isValid()) return true;
|
||||
return pivot_indicator_timer_.elapsed() < pivot_indicator_hide_ms_;
|
||||
}
|
||||
|
||||
void ViewportCore::flyMove(bool fwd, bool back, bool right, bool left,
|
||||
bool up, bool down, bool boost, float dt_seconds) {
|
||||
if (dt_seconds <= 0.0f) return;
|
||||
@@ -1297,6 +1312,10 @@ bool ViewportCore::buildPipelines() {
|
||||
// Section-plane gizmo (shared desktop + web). Optional — a failure just
|
||||
// means no gizmo, not a dead viewport.
|
||||
section_gizmo_.init(device_, queue_, surface_view_format_, kViewportSampleCount);
|
||||
|
||||
// Corner axis gizmo + orbit pivot indicator (shared desktop + web).
|
||||
// Also optional: a failure costs the indicator, not the viewport.
|
||||
axis_indicator_.init(device_, queue_, surface_view_format_, kViewportSampleCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1913,6 +1932,7 @@ void ViewportCore::shutdown() {
|
||||
if (main_pipeline_no_cull_) { wgpuRenderPipelineRelease(main_pipeline_no_cull_); main_pipeline_no_cull_ = nullptr; }
|
||||
if (main_pipeline_transparent_) { wgpuRenderPipelineRelease(main_pipeline_transparent_); main_pipeline_transparent_ = nullptr; }
|
||||
section_gizmo_.destroy();
|
||||
axis_indicator_.destroy();
|
||||
if (main_shader_module_) { wgpuShaderModuleRelease(main_shader_module_); main_shader_module_ = nullptr; }
|
||||
if (pipeline_layout_) { wgpuPipelineLayoutRelease(pipeline_layout_); pipeline_layout_ = nullptr; }
|
||||
if (model_bgl_) { wgpuBindGroupLayoutRelease(model_bgl_); model_bgl_ = nullptr; }
|
||||
@@ -7606,8 +7626,15 @@ void ViewportCore::render() {
|
||||
section_gizmo_.encode(pass, vp_this_frame, section_planes_,
|
||||
viewport_w_px, viewport_h_px, dpr_int, section_selected_index_);
|
||||
|
||||
// Remaining in-pass overlays (highlight triangles, pivot, overlay
|
||||
// lines/points). QtViewportHost forwards to overlays_.X(); web host no-ops.
|
||||
// Orbit pivot indicator — same shared-renderer story. Drawn while the host
|
||||
// has it gated on (drag) or an afterglow is still running; in the latter
|
||||
// case keep frames coming so the one that clears it actually lands.
|
||||
const bool pivot_visible = pivotIndicatorVisible();
|
||||
axis_indicator_.encodePivot(pass, overlay_frame, pivot_visible);
|
||||
if (pivot_visible && pivot_indicator_timer_.isValid()) host_->requestFrame();
|
||||
|
||||
// Remaining in-pass overlays (highlight triangles, overlay lines/points).
|
||||
// QtViewportHost forwards to overlays_.X(); the web host no-ops.
|
||||
host_->encodeOverlaysInMainPass(pass, overlay_frame);
|
||||
|
||||
wgpuRenderPassEncoderEnd(pass);
|
||||
@@ -7626,8 +7653,12 @@ void ViewportCore::render() {
|
||||
int hiz_submitted_slot = -1;
|
||||
if (hiz_enabled_) hiz_submitted_slot = encodeHizResolve(enc);
|
||||
|
||||
// Post-main overlays (corner axis, marquee, labels) on the resolved
|
||||
// surface. QtViewportHost forwards to overlays_.X().
|
||||
// Corner axis gizmo on the resolved surface — shared renderer, ahead of the
|
||||
// host's own post-main overlays so marquee / labels still stack on top.
|
||||
axis_indicator_.encodeCornerAxis(enc, view, overlay_frame);
|
||||
|
||||
// Remaining post-main overlays (marquee, labels) on the resolved surface.
|
||||
// QtViewportHost forwards to overlays_.X(); the web host no-ops.
|
||||
host_->encodeOverlaysPostMain(enc, view, overlay_frame);
|
||||
|
||||
// Optional capture: encode copy on the same command buffer.
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "AxisIndicatorRenderer.h"
|
||||
#include "BufferPool.h"
|
||||
#include "InstanceCompose.h"
|
||||
#include "InstancedGeometry.h"
|
||||
@@ -55,6 +56,7 @@
|
||||
#include "SectionPlane.h"
|
||||
#include "SelectionState.h"
|
||||
#include "SidecarCache.h"
|
||||
#include "Stopwatch.h"
|
||||
#include "StreamingLoader.h"
|
||||
#include "StreamingThread.h"
|
||||
#include "ViewportHost.h"
|
||||
@@ -281,9 +283,9 @@ public:
|
||||
//
|
||||
// Pixel-delta camera moves, shared by every host (Qt desktop + web).
|
||||
// Hosts translate raw pointer/wheel events into these calls and own
|
||||
// their own UI concerns (drag promotion, pivot indicator, cursor
|
||||
// capture); the orbit math lives here so it can't drift between
|
||||
// platforms. Each schedules a frame via the host.
|
||||
// their own UI concerns (drag promotion, cursor capture); the orbit
|
||||
// math lives here so it can't drift between platforms. Each schedules
|
||||
// a frame via the host.
|
||||
//
|
||||
// orbitBy: drag-right yaws the world right (yaw -= dx), drag-down
|
||||
// tilts the camera up (pitch += dy). 0.4 deg/px matches GL.
|
||||
@@ -296,6 +298,18 @@ public:
|
||||
void panBy(float dx_px, float dy_px, int viewport_height_px);
|
||||
void dollyBy(float notches);
|
||||
|
||||
// ---- Pivot indicator ----------------------------------------------------
|
||||
//
|
||||
// The RGB triad drawn at the orbit target while the user navigates, so it's
|
||||
// obvious what the camera is turning around. Hosts gate it: (true) when an
|
||||
// orbit / pan drag starts, (false) when it ends. `hide_after_ms` > 0 arms an
|
||||
// afterglow instead — the wheel path uses it so a zoom without a held drag
|
||||
// still shows the pivot for a moment. State lives here (not in the host) so
|
||||
// desktop and web behave identically; render() consults it each frame and
|
||||
// keeps requesting frames until an armed afterglow expires.
|
||||
void setPivotIndicatorVisible(bool visible, int hide_after_ms = 0);
|
||||
bool pivotIndicatorVisible() const;
|
||||
|
||||
// ---- First-person / fly navigation --------------------------------------
|
||||
//
|
||||
// Shared fly-camera math (desktop + web). The HOST owns the fly-mode flag,
|
||||
@@ -1092,6 +1106,14 @@ private:
|
||||
// Lifted out of the Qt-coupled OverlayRenderer so one identical gizmo draws
|
||||
// everywhere; the desktop's OverlayRenderer no longer draws it.
|
||||
SectionGizmoRenderer section_gizmo_;
|
||||
// Corner axis gizmo + orbit pivot indicator, likewise shared by desktop +
|
||||
// web. Same lift out of the Qt-coupled OverlayRenderer.
|
||||
AxisIndicatorRenderer axis_indicator_;
|
||||
bool pivot_indicator_visible_ = false;
|
||||
// Only running while an afterglow is armed; a drag-held indicator leaves it
|
||||
// invalid so the triad stays up until the host clears it.
|
||||
Stopwatch pivot_indicator_timer_;
|
||||
int pivot_indicator_hide_ms_ = 0;
|
||||
|
||||
// HiZ occlusion-cull pipeline group. Downsamples MSAA depth into a
|
||||
// mip pyramid; consumed by next-frame cull.
|
||||
|
||||
@@ -435,14 +435,14 @@ void ViewportWindow::onFrameStats(const FrameStats& stats) {
|
||||
|
||||
void ViewportWindow::encodeOverlaysInMainPass(WGPURenderPassEncoder pass,
|
||||
const OverlayFrame& frame) {
|
||||
// Section gizmos, highlight triangles, pivot, overlay lines / points
|
||||
// — drawn inside the MSAA pass so depth-test correctly hides them
|
||||
// behind closer geometry. (Corner axis / marquee / labels run on the
|
||||
// resolved surface; see encodeOverlaysPostMain.)
|
||||
// NB: section-plane gizmos now draw from ViewportCore::render via the shared
|
||||
// SectionGizmoRenderer (desktop + web), so they are NOT drawn here.
|
||||
// Highlight triangles + overlay lines / points — drawn inside the MSAA
|
||||
// pass so depth-test correctly hides them behind closer geometry.
|
||||
// (Marquee / labels run on the resolved surface; see
|
||||
// encodeOverlaysPostMain.)
|
||||
// NB: section-plane gizmos and the pivot indicator now draw from
|
||||
// ViewportCore::render via their shared renderers (desktop + web), so
|
||||
// they are NOT drawn here.
|
||||
overlays_.encodeHighlightTriangles(pass, frame);
|
||||
overlays_.encodePivot(pass, frame, pivot_indicator_visible_);
|
||||
overlays_.encodeOverlayLines(pass, frame);
|
||||
overlays_.encodeOverlayPoints(pass, frame);
|
||||
}
|
||||
@@ -450,7 +450,8 @@ void ViewportWindow::encodeOverlaysInMainPass(WGPURenderPassEncoder pass,
|
||||
void ViewportWindow::encodeOverlaysPostMain(WGPUCommandEncoder enc,
|
||||
WGPUTextureView surface_view,
|
||||
const OverlayFrame& frame) {
|
||||
overlays_.encodeCornerAxis(enc, surface_view, frame);
|
||||
// NB: the corner axis gizmo draws from ViewportCore::render (shared
|
||||
// AxisIndicatorRenderer), just before this hook.
|
||||
overlays_.encodeMarquee(enc, surface_view, frame,
|
||||
box_select_start_pos_,
|
||||
box_select_current_pos_,
|
||||
@@ -907,25 +908,8 @@ bool ViewportWindow::initWgpu() {
|
||||
|
||||
// encodeEdgePass moved to ViewportCore (#84-s).
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
void ViewportWindow::setPivotIndicatorVisible(bool visible, int hide_after_ms) {
|
||||
if (!pivot_indicator_hide_timer_) {
|
||||
pivot_indicator_hide_timer_ = new QTimer(this);
|
||||
pivot_indicator_hide_timer_->setSingleShot(true);
|
||||
QObject::connect(pivot_indicator_hide_timer_, &QTimer::timeout, this,
|
||||
[this]() {
|
||||
pivot_indicator_visible_ = false;
|
||||
requestUpdate();
|
||||
});
|
||||
}
|
||||
pivot_indicator_visible_ = visible;
|
||||
if (visible && hide_after_ms > 0) {
|
||||
pivot_indicator_hide_timer_->start(hide_after_ms);
|
||||
} else {
|
||||
pivot_indicator_hide_timer_->stop();
|
||||
}
|
||||
requestUpdate();
|
||||
}
|
||||
// setPivotIndicatorVisible moved to ViewportCore (drawn by the shared
|
||||
// AxisIndicatorRenderer, so the visibility gate lives there too).
|
||||
// releaseEdgeResources moved to ViewportCore (#84-s).
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -1585,11 +1569,11 @@ void ViewportWindow::mousePressEvent(QMouseEvent* event) {
|
||||
if (event->button() == orbit_button_
|
||||
&& (mods & Qt::KeyboardModifierMask) == orbit_mods_) {
|
||||
nav_drag_kind_ = NavDrag::Orbit;
|
||||
setPivotIndicatorVisible(true); // hidden again on release
|
||||
core_.setPivotIndicatorVisible(true); // hidden again on release
|
||||
} else if (event->button() == pan_button_
|
||||
&& (mods & Qt::KeyboardModifierMask) == pan_mods_) {
|
||||
nav_drag_kind_ = NavDrag::Pan;
|
||||
setPivotIndicatorVisible(true);
|
||||
core_.setPivotIndicatorVisible(true);
|
||||
} else if (event->button() == select_button_
|
||||
&& !section_tool_active_
|
||||
&& tool_mode_ != ToolMode::Area
|
||||
@@ -1683,7 +1667,7 @@ void ViewportWindow::mouseReleaseEvent(QMouseEvent* event) {
|
||||
}
|
||||
nav_active_button_ = Qt::NoButton;
|
||||
nav_drag_kind_ = NavDrag::Inactive;
|
||||
setPivotIndicatorVisible(false);
|
||||
core_.setPivotIndicatorVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1700,7 +1684,7 @@ void ViewportWindow::mouseReleaseEvent(QMouseEvent* event) {
|
||||
emit surfacePickedInTool(px, py, int(event->modifiers()));
|
||||
nav_active_button_ = Qt::NoButton;
|
||||
nav_drag_kind_ = NavDrag::Inactive;
|
||||
setPivotIndicatorVisible(false);
|
||||
core_.setPivotIndicatorVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1715,7 +1699,7 @@ void ViewportWindow::mouseReleaseEvent(QMouseEvent* event) {
|
||||
emit surfacePickedInTool(px, py, int(event->modifiers()));
|
||||
nav_active_button_ = Qt::NoButton;
|
||||
nav_drag_kind_ = NavDrag::Inactive;
|
||||
setPivotIndicatorVisible(false);
|
||||
core_.setPivotIndicatorVisible(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1800,7 +1784,7 @@ void ViewportWindow::mouseReleaseEvent(QMouseEvent* event) {
|
||||
nav_active_button_ = Qt::NoButton;
|
||||
nav_drag_kind_ = NavDrag::Inactive;
|
||||
// Drag is over — hide the pivot indicator without afterglow.
|
||||
setPivotIndicatorVisible(false);
|
||||
core_.setPivotIndicatorVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2050,7 +2034,7 @@ void ViewportWindow::wheelEvent(QWheelEvent* event) {
|
||||
core_.dollyBy(notches);
|
||||
// Pivot afterglow on wheel — visible for 600 ms so the user can see
|
||||
// what they're zooming around without holding a drag.
|
||||
setPivotIndicatorVisible(true, 600);
|
||||
core_.setPivotIndicatorVisible(true, 600);
|
||||
}
|
||||
|
||||
void ViewportWindow::shutdown() {
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
#define WGPUVIEWPORTWINDOW_H
|
||||
|
||||
#include <QWindow>
|
||||
#include <QTimer>
|
||||
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
@@ -306,12 +305,9 @@ private:
|
||||
bool buildHizPipeline();
|
||||
bool buildEdgePipeline();
|
||||
void encodeEdgePass(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. The
|
||||
// actual gizmo rendering lives in OverlayRenderer — this just
|
||||
// manages the UI-side visibility timer.
|
||||
void setPivotIndicatorVisible(bool visible, int hide_after_ms = 0);
|
||||
// setPivotIndicatorVisible moved to ViewportCore — the indicator is drawn
|
||||
// by the shared AxisIndicatorRenderer now, so its visibility (afterglow
|
||||
// included) lives next to the drawing for desktop + web alike.
|
||||
// releaseEdgeResources / buildPickPipeline / ensurePickAttachments /
|
||||
// releasePickResources moved to ViewportCore (#84-s, #84-t).
|
||||
|
||||
@@ -670,15 +666,10 @@ private:
|
||||
WGPUBindGroup& edge_bind_group_;
|
||||
bool& edges_enabled_;
|
||||
|
||||
// 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 OverlayFrame each frame and asks the renderer to
|
||||
// encode each overlay; see OverlayRenderer.h.
|
||||
// The Qt-coupled viewport overlays (marquee rect, measure lines /
|
||||
// points / labels, highlight triangles) — pipelines + shaders +
|
||||
// buffers + encoders. The viewport builds a OverlayFrame each frame
|
||||
// and asks the renderer to encode each overlay; see OverlayRenderer.h.
|
||||
OverlayRenderer overlays_;
|
||||
|
||||
// Active measurement tool. setToolMode() / setSelection mutations
|
||||
|
||||
Reference in New Issue
Block a user