Warning, /firebird/packages/firebird-ng/display/src/lib/extensions/viewport-gizmo.extension.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * Camera navigation cube (viewport gizmo), wired as a builtin ThreeExtension.
0003 *
0004 * The cube shows the camera orientation and IS a camera control: click a face,
0005 * edge or corner to animate there, drag to orbit, roll the view with the
0006 * curved-arrow buttons, or return to the home view. Rendering, axis
0007 * conventions and the WebGPU handling live in `@dexvis/viewport-gizmo`
0008 * (dexvis/viewport-gizmo submodule); this file binds it to Firebird's main
0009 * RenderView as a view overlay: the overlay renders after the view's scene
0010 * render and follows the view's camera, container and resizes. An extension
0011 * that wants its own overlay uses the same seam (`view.addOverlay`).
0012 *
0013 * Loaded through `withLazyThreeExtension` — the gizmo pulls three.js code and
0014 * must stay out of the initial bundle.
0015 */
0016
0017 import { Injectable, inject } from '@angular/core';
0018 import { ViewportGizmo } from '@dexvis/viewport-gizmo';
0019 import type { OrthographicCamera, PerspectiveCamera } from 'three';
0020
0021 import type { RenderView, SceneContext, ThreeExtension, ViewOverlay } from '@dexvis/firebird-ng/api';
0022 import { CommandBusService } from '@dexvis/app-features';
0023
0024 @Injectable()
0025 export class ViewportGizmoExtension implements ThreeExtension {
0026 private commandBus = inject(CommandBusService);
0027
0028 private gizmo?: ViewportGizmo;
0029 private view?: RenderView;
0030 private overlay?: ViewOverlay;
0031 private lastCamera?: PerspectiveCamera | OrthographicCamera;
0032
0033 onSceneInit(ctx: SceneContext): void {
0034 const view = ctx.mainView;
0035 this.view = view;
0036
0037 this.gizmo = new ViewportGizmo(view.camera, ctx.renderer, {
0038 // Overlay positioning is relative to the view container.
0039 container: view.container,
0040 size: 90,
0041 placement: 'top-right',
0042 offset: { top: 10, right: 20 },
0043 background: { color: 0x444444, hover: { color: 0x444444 } },
0044 corners: { color: 0x333333, hover: { color: 0x4bac84 } },
0045 // HENP axis convention: beam along Z, Y up, X toward the accelerator
0046 // center. The front view has Y up and Z pointing right on the screen
0047 // (camera on -X, +X goes into the screen); the top/bottom views keep Z
0048 // pointing right, which puts X toward the top of the screen when viewed
0049 // from above (poleUp).
0050 poleUp: { top: [1, 0, 0], bottom: [-1, 0, 0] },
0051 nx: { label: 'Front' },
0052 x: { label: 'Back' },
0053 z: { label: 'Right' },
0054 nz: { label: 'Left' },
0055 y: { label: 'Top' },
0056 ny: { label: 'Bottom' },
0057 });
0058
0059 this.gizmo.attachControls(view.controls);
0060 this.lastCamera = view.camera;
0061
0062 // Home goes through the command bus like any other camera move, so the
0063 // same view is reachable from deep links and batch: camera-preset:home.
0064 this.gizmo.addEventListener('home', () => {
0065 void this.commandBus.dispatch({ type: 'camera-preset', name: 'home', source: 'ui' });
0066 });
0067
0068 // Render-on-demand chain for cube interactions: clicks and view
0069 // transitions emit 'change' per step (the transition advances inside the
0070 // overlay render), so each step schedules the next frame until the
0071 // animation settles.
0072 this.gizmo.addEventListener('start', () => ctx.invalidate());
0073 this.gizmo.addEventListener('change', () => ctx.invalidate());
0074
0075 // View overlay: renders after the view's scene render (the cube must
0076 // draw on top) and re-anchors when the view is resized (pane toggles,
0077 // window resizes — the view calls onViewResize from updateViewport).
0078 this.overlay = {
0079 render: () => this.renderGizmo(),
0080 onViewResize: () => this.gizmo?.update(false),
0081 // The cube follows the view across page layouts (single display,
0082 // quad view): its overlay div moves into the view's new container.
0083 onViewContainerChange: (changed) => this.gizmo?.setContainer(changed.container),
0084 dispose: () => this.disposeGizmo(),
0085 };
0086 view.addOverlay(this.overlay);
0087 }
0088
0089 onDispose(): void {
0090 if (this.view && this.overlay) {
0091 this.view.removeOverlay(this.overlay);
0092 }
0093 this.disposeGizmo();
0094 this.view = undefined;
0095 this.overlay = undefined;
0096 }
0097
0098 private disposeGizmo(): void {
0099 this.gizmo?.dispose();
0100 this.gizmo = undefined;
0101 }
0102
0103 private renderGizmo(): void {
0104 const gizmo = this.gizmo;
0105 const view = this.view;
0106 if (!gizmo || !view) return;
0107
0108 // Follow perspective/orthographic camera switches
0109 const camera = view.camera;
0110 if (camera !== this.lastCamera) {
0111 this.lastCamera = camera;
0112 gizmo.camera = camera;
0113 gizmo.update(false);
0114 }
0115
0116 gizmo.cameraUpdate();
0117 gizmo.render();
0118 }
0119 }