From cb22a0d6e4d09675ceb32807482a99291d9cb679 Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Thu, 22 Oct 2026 10:28:34 -0700 Subject: [PATCH 1/4] feat: resize the server window with the canvas --- .../src/jest-tests/ServerCanvasStream.test.ts | 12 ++++ .../jest-tests/ServerRendererCache.test.ts | 44 +++++++++++++ .../src/jest-tests/ServerRendererPick.test.ts | 1 + .../visor-client/src/rca/ServerCanvas.ts | 17 ++++- .../src/renderer/ServerRenderer.ts | 14 +++- tests/unit/app/test_server_app.py | 65 ++++++++++++++++++- 6 files changed, 147 insertions(+), 6 deletions(-) diff --git a/src/ansys/visor/visor-client/src/jest-tests/ServerCanvasStream.test.ts b/src/ansys/visor/visor-client/src/jest-tests/ServerCanvasStream.test.ts index a0f0f426..9072470e 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/ServerCanvasStream.test.ts +++ b/src/ansys/visor/visor-client/src/jest-tests/ServerCanvasStream.test.ts @@ -131,4 +131,16 @@ describe('ServerCanvas stream', () => { expect(session.call).not.toHaveBeenCalled(); }); + + test('a resize listener hears each non-zero element size and no zero one', () => { + const session = makeSession(); + const canvas = new ServerCanvas(session, 'view'); + const listener = jest.fn(); + canvas.addResizeListener(listener); + + observeSize(0, 300); + observeSize(400, 300); + + expect(listener).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/ansys/visor/visor-client/src/jest-tests/ServerRendererCache.test.ts b/src/ansys/visor/visor-client/src/jest-tests/ServerRendererCache.test.ts index daace525..12df5b21 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/ServerRendererCache.test.ts +++ b/src/ansys/visor/visor-client/src/jest-tests/ServerRendererCache.test.ts @@ -53,6 +53,7 @@ function stubCanvas(): IServerCanvas { frameSize: null, addFrameListener: () => () => {}, addClickListener: () => () => {}, + addResizeListener: () => () => {}, }; } @@ -119,3 +120,46 @@ describe('ServerRenderer camera cache', () => { }); }); }); + +describe('ServerRenderer canvas resize', () => { + let originalDevicePixelRatio: number; + + beforeEach(() => { + originalDevicePixelRatio = window.devicePixelRatio; + Object.defineProperty(window, 'devicePixelRatio', { configurable: true, value: 1.5 }); + }); + + afterEach(() => { + Object.defineProperty(window, 'devicePixelRatio', { + configurable: true, + value: originalDevicePixelRatio, + }); + }); + + test('a canvas resize gives camera listeners the snapshot at the truncated device-pixel height', () => { + const element = document.createElement('div'); + Object.defineProperty(element, 'clientHeight', { configurable: true, value: 100 }); + const resizeListeners: (() => void)[] = []; + const canvas: IServerCanvas = { + element, + frameSize: null, + addFrameListener: () => () => {}, + addClickListener: () => () => {}, + addResizeListener: (callback) => { + resizeListeners.push(callback); + return () => {}; + }, + }; + const state = new FakeState(cameraRecord([0, 0, 9], [0, 0, 0])); + const renderer = new ServerRenderer(canvas, state as ServerStateSource, jest.fn()); + const listener = jest.fn(); + renderer.addCameraChangedListener(listener); + Object.defineProperty(element, 'clientHeight', { configurable: true, value: 301 }); + + resizeListeners.forEach((callback) => callback()); + + expect(listener).toHaveBeenCalledTimes(1); + expect(listener.mock.calls[0][0].viewPortHeight).toBe(451); + expect(listener.mock.calls[0][0].unitsPerPixel).toBeCloseTo(0.004434589800443459, 12); + }); +}); diff --git a/src/ansys/visor/visor-client/src/jest-tests/ServerRendererPick.test.ts b/src/ansys/visor/visor-client/src/jest-tests/ServerRendererPick.test.ts index 9f5ab8f7..6853a92b 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/ServerRendererPick.test.ts +++ b/src/ansys/visor/visor-client/src/jest-tests/ServerRendererPick.test.ts @@ -58,6 +58,7 @@ function stubCanvas() { clicks.push(callback); return () => {}; }, + addResizeListener: () => () => {}, }; return { canvas, click: (c: CanvasClick) => clicks.forEach((callback) => callback(c)) }; } diff --git a/src/ansys/visor/visor-client/src/rca/ServerCanvas.ts b/src/ansys/visor/visor-client/src/rca/ServerCanvas.ts index 810f3180..156b757c 100644 --- a/src/ansys/visor/visor-client/src/rca/ServerCanvas.ts +++ b/src/ansys/visor/visor-client/src/rca/ServerCanvas.ts @@ -39,6 +39,7 @@ export interface IServerCanvas { readonly frameSize: FrameSize | null; addFrameListener(callback: (fps: number) => void): () => void; addClickListener(callback: (click: CanvasClick) => void): () => void; + addResizeListener(callback: () => void): () => void; } /** One message on the stream topic: an encoded frame for one area. */ @@ -116,6 +117,15 @@ export class ServerCanvas implements IServerCanvas { return remover; } + /** Call callback after each non-zero element size is sent to the area; returns its remover. */ + addResizeListener(callback: () => void): () => void { + const remover = () => { + this.#resizeListeners.delete(remover); + }; + this.#resizeListeners.set(remover, callback); + return remover; + } + /** Unsubscribe, stop observing the size, remove DOM listeners and revoke the current frame URL. */ dispose(): void { this.#subscription.unsubscribe(); @@ -132,6 +142,7 @@ export class ServerCanvas implements IServerCanvas { element.removeEventListener('contextmenu', this.#onContextMenu); this.#frameListeners.clear(); this.#clickListeners.clear(); + this.#resizeListeners.clear(); this.#revokeFrameUrl(); } @@ -143,6 +154,7 @@ export class ServerCanvas implements IServerCanvas { readonly #frameListeners = new Map<() => void, (fps: number) => void>(); readonly #frameTimes: number[] = []; readonly #clickListeners = new Map<() => void, (click: CanvasClick) => void>(); + readonly #resizeListeners = new Map<() => void, () => void>(); readonly #buttons = new MouseButtonState(); readonly #clickTracker = new ClickTracker(); #frameSize: FrameSize | null = null; @@ -201,7 +213,7 @@ export class ServerCanvas implements IServerCanvas { console.error('[VISOR] frame decode failed'); }; - /** Send each non-zero element size to the area; the server resizes its window and renders a frame. */ + /** Send each non-zero element size to the area, then tell the resize listeners; the server resizes its window and renders a frame. */ #onResize = (entries: ResizeObserverEntry[]): void => { for (const entry of entries) { const w = Math.round(entry.contentRect.width); @@ -213,6 +225,9 @@ export class ServerCanvas implements IServerCanvas { this.#session .call('trame.rca.size', [this.#areaName, MESSAGE_ORIGIN, { w, h, p }]) .catch((err) => console.error('[VISOR] trame.rca.size send failed', err)); + for (const callback of this.#resizeListeners.values()) { + callback(); + } } }; diff --git a/src/ansys/visor/visor-client/src/renderer/ServerRenderer.ts b/src/ansys/visor/visor-client/src/renderer/ServerRenderer.ts index 79012e66..7a4f0449 100644 --- a/src/ansys/visor/visor-client/src/renderer/ServerRenderer.ts +++ b/src/ansys/visor/visor-client/src/renderer/ServerRenderer.ts @@ -154,6 +154,7 @@ export class ServerRenderer implements IRenderer { // `watch` also calls back at once with the key's current value. this.#unwatch = state.watch([SERVER_CAMERA_STATE_KEY], this.#onCameraState); this.#removeClickListener = canvas.addClickListener(this.#onCanvasClick); + this.#removeResizeListener = canvas.addResizeListener(this.#onCanvasResize); } readonly #canvas: IServerCanvas; @@ -161,6 +162,7 @@ export class ServerRenderer implements IRenderer { readonly #sends: SceneTriggerSends; readonly #unwatch: () => void; readonly #removeClickListener: () => void; + readonly #removeResizeListener: () => void; readonly #camera = new CameraRecordCache(); readonly #cameraChangedListeners = new Map<() => void, (state: VisorCameraState) => void>(); readonly #viewerClicks = new ViewerClickDispatcher(); @@ -207,6 +209,11 @@ export class ServerRenderer implements IRenderer { .catch((err) => console.error('[VISOR] pick_part_at failed', err)); }; + /** Give every camera listener the snapshot at the canvas's new height. */ + #onCanvasResize = (): void => { + this.#fireCameraChanged(); + }; + /** The render-window pixel under a normalised coordinate on an axis of the given size, clamped to the window. */ static #framePixel(norm: number, size: number): number { return Math.min(Math.max(Math.floor(norm * size), 0), size - 1); @@ -238,9 +245,9 @@ export class ServerRenderer implements IRenderer { } } - /** The view's height in device pixels. */ + /** The render window's height in device pixels: CSS height times pixel ratio, truncated as the server sizes it. */ #viewPortHeight(): number { - return this.domElement.clientHeight * (window.devicePixelRatio ?? 1); + return Math.floor(this.domElement.clientHeight * (window.devicePixelRatio ?? 1)); } /** The full snapshot: record plus distance, orthographic, orthographicScale, unitsPerPixel, viewPortHeight. */ @@ -270,12 +277,13 @@ export class ServerRenderer implements IRenderer { /** The server renders after every write it receives. */ async renderAsync(): Promise {} - /** The canvas reports its own size to the server. */ + /** The canvas reports its own size to the server and tells this renderer, which refreshes the camera listeners. */ async resizeAsync(): Promise {} dispose(): void { this.#unwatch(); this.#removeClickListener(); + this.#removeResizeListener(); this.#cameraChangedListeners.clear(); this.#viewerClicks.clear(); } diff --git a/tests/unit/app/test_server_app.py b/tests/unit/app/test_server_app.py index ba027fd6..7b312834 100644 --- a/tests/unit/app/test_server_app.py +++ b/tests/unit/app/test_server_app.py @@ -1,18 +1,23 @@ """Unit tests for ``ServerApp``: its trigger set, its mode seed, its two -server-only triggers, and the stream binding made when the server is ready. +server-only triggers, the stream binding made when the server is ready, and the +render-window size a client's size report gives. The shared triggers' coordinator and payload behaviour is pinned in ``test_local_app.py`` and its sibling modules and is not restated here. ``RemoteControlledArea`` is replaced by a double, so nothing here builds a -render scheduler; the real stream is exercised in +real render scheduler; the real stream is exercised in ``tests/integration/test_server_stream.py``. """ +import threading from unittest.mock import MagicMock, patch +import numpy as np import pytest +from trame_rca.view_adapter import RcaViewAdapter from ansys.visor.viewer.app.trame.server_app import ServerApp +from ansys.visor.viewer.renderer.server_window import VisorRcaWindow # The exact 20 trigger names ServerApp registers: the 18 shared with every # rendering mode, plus its own two. Written out by hand, not derived from the @@ -180,3 +185,59 @@ def test_server_ready_builds_handler_and_binds(mock_server, area_cls): ) bridge.bind.assert_called_once_with(area.create_view_handler.return_value) + +class _RecordingScheduler: + """A render scheduler over one area that only counts render requests.""" + + def __init__(self, rca) -> None: + self.rca = rca + self.renders = 0 + + def set_push_callback(self, _callback) -> None: + pass + + def schedule_render(self) -> None: + self.renders += 1 + + +class _SizeRecordingRenderer: + """The part of a ServerRenderer the RCA window uses; records each viewport size.""" + + background_rgb = (0.0, 0.0, 0.0) + + def __init__(self) -> None: + self.sizes: list[tuple[int, int]] = [] + + def set_viewport_size(self, width: int, height: int) -> None: + self.sizes.append((width, height)) + + +class _StillCapture: + """A capture that returns one literal frame and accepts events.""" + + img_cols_rows = (np.zeros((1, 1, 3), dtype=np.uint8), 1, 1) + + def process_interaction_event(self, _event: dict) -> None: + pass + + +def test_client_resize_sizes_render_window(mock_server, area_cls): + """A client size of 401 x 301 CSS px at pixel ratio 1.5 gives the render + window 601 x 451 device pixels at the next frame: the handler ServerApp + builds neither rescales nor caps the size.""" + renderer = _SizeRecordingRenderer() + rca_window = VisorRcaWindow(_StillCapture(), renderer, threading.RLock(), lambda: None) + bridge = MagicMock(name="bridge") + area_cls.return_value.create_view_handler.side_effect = lambda window, **_: RcaViewAdapter( + window, "view", scheduler=_RecordingScheduler(window) + ) + _build(mock_server, rca_window=rca_window, bridge=bridge) + for handler in mock_server.controller.handlers["on_server_ready"]: + handler() + view_handler = bridge.bind.call_args.args[0] + + view_handler.update_size("ServerCanvas", {"w": 401, "h": 301, "p": 1.5}) + rca_window.img_cols_rows + + assert renderer.sizes == [(601, 451)] + From 87083b3d3678693dc587d680d1013443eb1fc762 Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Thu, 22 Oct 2026 14:49:27 -0700 Subject: [PATCH 2/4] feat: resize orientation widget with the canvas --- .../visor/viewer/renderer/server_renderer.py | 3 +- .../viewer/vtk/widgets/visor_orientation.py | 14 +++++++++ .../visor/visor-client/src/VisorFrontend.tsx | 4 --- .../WasmRendererWidgetTriggers.test.tsx | 30 +++++++++++++++++- .../visor-client/src/renderer/WasmRenderer.ts | 7 +++++ .../src/widgets/orientationWidget.ts | 31 ++++++++++++------- tests/unit/renderer/test_server_renderer.py | 6 ++++ 7 files changed, 77 insertions(+), 18 deletions(-) diff --git a/src/ansys/visor/viewer/renderer/server_renderer.py b/src/ansys/visor/viewer/renderer/server_renderer.py index 7ca9e46b..af63c26e 100644 --- a/src/ansys/visor/viewer/renderer/server_renderer.py +++ b/src/ansys/visor/viewer/renderer/server_renderer.py @@ -75,11 +75,12 @@ def render_window(self) -> vtkRenderWindow: return self._render_window def set_viewport_size(self, width: int, height: int) -> None: - """Resize the window and its interactor to the client canvas in device pixels. + """Resize the window and its interactor to the client canvas in device pixels, and fit the orientation widget to it. Requests no frame: the resize path that calls this schedules its own. """ self._render_window_interactor.UpdateSize(width, height) + self._orientation_widget.fit_to_window(width, height) def set_background_rgb(self, background_rgb: tuple[float, float, float]) -> None: """Clear to this colour from the next frame. diff --git a/src/ansys/visor/viewer/vtk/widgets/visor_orientation.py b/src/ansys/visor/viewer/vtk/widgets/visor_orientation.py index 2b692204..e801362d 100644 --- a/src/ansys/visor/viewer/vtk/widgets/visor_orientation.py +++ b/src/ansys/visor/viewer/vtk/widgets/visor_orientation.py @@ -9,6 +9,9 @@ logger = VisorDefaultLogger(__name__) +SIZE_FRACTION: float = 0.15 +"""Widget edge as a fraction of the window's shorter side.""" + class VisorOrientationWidget: """ @@ -20,6 +23,7 @@ class VisorOrientationWidget: Methods: register_with_local_view(local_view) + fit_to_window(width, height) Properties: is_on (bool): Whether the widget is on or not. @@ -47,6 +51,16 @@ def is_on(self): """Return whether the widget is on or not.""" return self.__widget.GetEnabled() == 1 + def fit_to_window(self, width: int, height: int) -> None: + """Size the widget to SIZE_FRACTION of the window's shorter side, in render-window pixels.""" + size = self.size_for_window(width, height) + self.__widget.GetRepresentation().SetSize(size, size) + + @staticmethod + def size_for_window(width: int, height: int) -> int: + """Square edge for a window of this size, rounded half up.""" + return int(min(width, height) * SIZE_FRACTION + 0.5) + def animate_off(self) -> None: """Snap the camera on an axis click instead of animating it over interactor timer events.""" self.__widget.AnimateOff() diff --git a/src/ansys/visor/visor-client/src/VisorFrontend.tsx b/src/ansys/visor/visor-client/src/VisorFrontend.tsx index bc90f6cb..bff015cf 100644 --- a/src/ansys/visor/visor-client/src/VisorFrontend.tsx +++ b/src/ansys/visor/visor-client/src/VisorFrontend.tsx @@ -12,7 +12,6 @@ import { IRenderer, VisorCameraState } from './renderer/IRenderer'; import type { TrameTriggerSender } from './renderer/IRenderer'; import { Panel_TopRight_Util } from './components/ui-panels/Panel_TopRight_Util.tsx'; import { Panel_TopLeft_Util } from './components/ui-panels/Panel_TopLeft_Util.tsx'; -import { OrientationWidget } from './widgets/orientationWidget.ts'; import { FullscreenWidget } from './widgets/fullscreenWidget.ts'; import { UiScaffoldUtil } from './components/UiScaffold.tsx'; @@ -62,8 +61,6 @@ export class VisorFrontend { // use "void" here to suppress the "no await" IDE warning // void orientationWidget.SetShouldResetCamera(false); - // const orientationWidget = new OrientationWidget(vtkScene, vtkInfo.orientationWidgetWasmId); - let themeDarkCss: string | null | undefined = null; let themeLightCss: string | null | undefined = null; let themeStyleElem: HTMLStyleElement | null | undefined = null; @@ -79,7 +76,6 @@ export class VisorFrontend { }; this.resizeAsync = async () => { await renderer.resizeAsync(); - // await orientationWidget.resizeAsync(); }; this.domElement = renderer.domElement; this.getCameraStateAsync = () => renderer.getCameraStateAsync(); diff --git a/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx b/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx index 50cf9a20..2c73a02f 100644 --- a/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx +++ b/src/ansys/visor/visor-client/src/jest-tests/WasmRendererWidgetTriggers.test.tsx @@ -28,6 +28,9 @@ import type { TrameTriggerSender } from '../renderer/IRenderer'; * throw, and a failed widget send is logged under its own prefix, naming the * trigger and carrying no node id. * + * Also pinned: `resizeAsync` fits the orientation widget to the window as it + * stands after the scene resize. + * * Modelled on `WasmRendererPartTriggers.test.tsx`, which does the same for * the six per-part sends. */ @@ -108,10 +111,14 @@ function makeFakeWasmObjects() { // above. Both are observed on `EndInteractionEvent`, and one double for // both cannot tell those two registrations apart: the plane's own test // asserts exactly one such registration, and would see two. + const orientationRepresentation = { + SetSize: jest.fn(async () => undefined), + }; const orientationWidget = { observe: jest.fn(), + GetRepresentation: jest.fn(async () => orientationRepresentation), }; - return { actor, property, widget, orientationWidget }; + return { actor, property, widget, orientationWidget, orientationRepresentation }; } async function makeRenderer( @@ -125,9 +132,16 @@ async function makeRenderer( ParallelProjectionOn: jest.fn(async () => undefined), ParallelProjectionOff: jest.fn(async () => undefined), }; + let windowSize = [300, 300]; const scene = { canvasDiv: document.createElement('div'), render: jest.fn(), + resizeAsync: jest.fn(async () => { + windowSize = [1200, 700]; + }), + renderWindow: { + GetSize: jest.fn(async () => windowSize), + }, clearObserversAndEventListeners: jest.fn(), // The tracker itself is `#private` to VtkScene, so what the renderer // can reach is this one passthrough, and this is what the orientation @@ -388,3 +402,17 @@ describe('WasmRenderer marks an orientation-widget move as a gesture', () => { expect(sender).not.toHaveBeenCalled(); }); }); + +describe('WasmRenderer fits the orientation widget on resize', () => { + // The scene double's resize moves the window from 300x300 to 1200x700, so + // a fit that read the window before the resize would carry [45, 45]. + test('resizeAsync sizes the orientation widget from the resized window', async () => { + const { renderer, scene, orientationRepresentation } = await makeRenderer(null); + + await renderer.resizeAsync(); + + expect(scene.resizeAsync).toHaveBeenCalledTimes(1); + expect(orientationRepresentation.SetSize).toHaveBeenCalledTimes(1); + expect(orientationRepresentation.SetSize).toHaveBeenCalledWith([105, 105]); + }); +}); diff --git a/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts index 3f744b6d..4fbfdf65 100644 --- a/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts +++ b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts @@ -14,6 +14,7 @@ import { CrossSectionWidget } from '../widgets/crossSectionWidget.ts'; import { BoundingBoxWidget } from '../widgets/boundingBoxWidget.ts'; import { OrthographicWidget } from '../widgets/orthographicWidget.ts'; import { EdgesWidget } from '../widgets/edgesWidget.ts'; +import { OrientationWidget } from '../widgets/orientationWidget.ts'; import { SceneTriggerSends } from './SceneTriggerSends'; const SELECTION_TINT_RGB: Readonly = [0 / 255, 62 / 255, 111 / 255]; @@ -59,6 +60,10 @@ export class WasmRenderer implements IRenderer { outlineQueryId: annotation.widgets.boundingBoxOutlineActorId, }); this.#edgesWidget = new EdgesWidget(this); + this.#orientationWidget = new OrientationWidget( + vtkScene, + annotation.widgets.orientationWidgetId + ); const planeWidget = vtkScene.getVtkObject(wasmPlaneWidgetId); planeWidget.observe('InteractionEvent', this.#crossSectionWidget.interactionHandler); @@ -150,6 +155,7 @@ export class WasmRenderer implements IRenderer { readonly #orthographicWidget: OrthographicWidget; readonly #boundingBoxWidget: BoundingBoxWidget; readonly #edgesWidget: EdgesWidget; + readonly #orientationWidget: OrientationWidget; // ---- Canvas / lifecycle ------------------------------------------------- readonly domElement: HTMLDivElement; @@ -160,6 +166,7 @@ export class WasmRenderer implements IRenderer { async resizeAsync(): Promise { await this.#vtkScene.resizeAsync(); + await this.#orientationWidget.fitToWindowAsync(); this.#vtkScene.render(); } diff --git a/src/ansys/visor/visor-client/src/widgets/orientationWidget.ts b/src/ansys/visor/visor-client/src/widgets/orientationWidget.ts index 34dbdc04..303d4f52 100644 --- a/src/ansys/visor/visor-client/src/widgets/orientationWidget.ts +++ b/src/ansys/visor/visor-client/src/widgets/orientationWidget.ts @@ -1,21 +1,28 @@ import VtkScene from '../wasm/VtkScene'; import { type FrontendVtkObject } from '../wasm/RemoteVtkScene'; +/** Widget edge as a fraction of the render window's shorter side. */ +const SIZE_FRACTION = 0.15; + export class OrientationWidget { constructor(vtkScene: VtkScene, orientationWidgetWasmId: number) { this.#vtkScene = vtkScene; - this.widget = vtkScene.getVtkObject(orientationWidgetWasmId); + this.#widget = vtkScene.getVtkObject(orientationWidgetWasmId); } - widget: FrontendVtkObject; - #rep: FrontendVtkObject; - #vtkScene: VtkScene; - resizeAsync = async (widgetSize?: number) => { - this.#rep ??= await this.widget.GetRepresentation(); - if (widgetSize == null) { - const windowSize = await this.#vtkScene.renderWindow.GetSize(); - widgetSize = Math.round(Math.min(windowSize[0], windowSize[1]) * 0.13); - } - await this.#rep.SetSize([widgetSize, widgetSize]); - }; + readonly #widget: FrontendVtkObject; + readonly #vtkScene: VtkScene; + + /** Size the widget to SIZE_FRACTION of the render window's shorter side. */ + async fitToWindowAsync(): Promise { + const [width, height] = await this.#vtkScene.renderWindow.GetSize(); + const size = OrientationWidget.sizeForWindow(width, height); + const representation = await this.#widget.GetRepresentation(); + await representation.SetSize([size, size]); + } + + /** Square edge for a window of this size, rounded half up. */ + static sizeForWindow(width: number, height: number): number { + return Math.floor(Math.min(width, height) * SIZE_FRACTION + 0.5); + } } diff --git a/tests/unit/renderer/test_server_renderer.py b/tests/unit/renderer/test_server_renderer.py index aced6ae6..3b415483 100644 --- a/tests/unit/renderer/test_server_renderer.py +++ b/tests/unit/renderer/test_server_renderer.py @@ -205,6 +205,12 @@ def test_set_viewport_size_resizes_window(renderer): assert renderer.render_window.GetSize() == (64, 48) +def test_set_viewport_size_fits_orientation_widget(renderer): + renderer.set_viewport_size(1200, 700) + representation = renderer._orientation_widget._VisorOrientationWidget__widget.GetRepresentation() + assert representation.GetSize() == (105, 105) + + def test_set_background_rgb_sets_vtk_background(renderer): renderer.set_background_rgb((0.25, 0.5, 0.75)) assert renderer._vtk_renderer.GetBackground() == (0.25, 0.5, 0.75) From 7fe0d05bb7f82dba2df354bf1704ac7683cc3289 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Thu, 22 Oct 2026 22:49:21 +0000 Subject: [PATCH 3/4] chore: adding changelog file 189.added.md [dependabot-skip] --- doc/changelog.d/189.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/189.added.md diff --git a/doc/changelog.d/189.added.md b/doc/changelog.d/189.added.md new file mode 100644 index 00000000..5b65110d --- /dev/null +++ b/doc/changelog.d/189.added.md @@ -0,0 +1 @@ +[Remote rendering 4.6] Keep view fitted to the canvas on resize From 9c8ee7b87a52d03df6d636bcdbad275eddd05e96 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:52:35 +0000 Subject: [PATCH 4/4] chore: adding changelog file 189.added.md [dependabot-skip] --- doc/changelog.d/189.added.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/changelog.d/189.added.md b/doc/changelog.d/189.added.md index 5b65110d..6f2588f3 100644 --- a/doc/changelog.d/189.added.md +++ b/doc/changelog.d/189.added.md @@ -1 +1 @@ -[Remote rendering 4.6] Keep view fitted to the canvas on resize +Remote rendering 4.6: Keep view fitted to the canvas on resize