mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-22 07:08:36 +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;
|
||||
|
||||
Reference in New Issue
Block a user