From 832ab1bf8645e3149e13989f0f2d4fea1dbf760a Mon Sep 17 00:00:00 2001 From: Laura Kasian Date: Sun, 20 Sep 2026 20:28:55 -0700 Subject: [PATCH 1/2] feat: fold per-part edge visibility into the global toggle --- src/ansys/visor/viewer/renderer/base.py | 4 -- .../visor/viewer/renderer/local_renderer.py | 3 -- .../visor/viewer/renderer/null_renderer.py | 3 -- .../ui-panels/Panel_BottomMiddle.tsx | 3 +- .../WasmRendererWidgetTriggers.test.tsx | 49 +++++++++++++++---- .../visor-client/src/renderer/WasmRenderer.ts | 2 +- .../src/state/VisorSceneGraph.tsx | 21 -------- .../visor-client/src/widgets/edgesWidget.ts | 11 ++++- tests/unit/renderer/test_local_renderer.py | 10 ++-- 9 files changed, 54 insertions(+), 52 deletions(-) diff --git a/src/ansys/visor/viewer/renderer/base.py b/src/ansys/visor/viewer/renderer/base.py index 878594c2..5821b150 100644 --- a/src/ansys/visor/viewer/renderer/base.py +++ b/src/ansys/visor/viewer/renderer/base.py @@ -105,10 +105,6 @@ def apply_diffuse_color( ) -> None: """Set the actor-property diffuse colour for *node_id*.""" - @abstractmethod - def apply_edge_visibility(self, node_id: int, edge_visible: bool) -> None: - """Toggle edge / wireframe visibility for *node_id*.""" - @abstractmethod def apply_selected( self, node_id: int, selected: bool, diffuse_rgb: list diff --git a/src/ansys/visor/viewer/renderer/local_renderer.py b/src/ansys/visor/viewer/renderer/local_renderer.py index 5195b78a..e2d28c85 100644 --- a/src/ansys/visor/viewer/renderer/local_renderer.py +++ b/src/ansys/visor/viewer/renderer/local_renderer.py @@ -204,9 +204,6 @@ def apply_diffuse_color( return pipe.set_diffuse_color(r, g, b) - def apply_edge_visibility(self, node_id: int, edge_visible: bool) -> None: - """No-op in Story 1.2. Phase 3 populates.""" - def apply_selected( self, node_id: int, selected: bool, diffuse_rgb: list ) -> None: diff --git a/src/ansys/visor/viewer/renderer/null_renderer.py b/src/ansys/visor/viewer/renderer/null_renderer.py index 44c3b278..2b4b2065 100644 --- a/src/ansys/visor/viewer/renderer/null_renderer.py +++ b/src/ansys/visor/viewer/renderer/null_renderer.py @@ -79,9 +79,6 @@ def apply_opacity(self, node_id: int, opacity: float) -> None: def apply_diffuse_color(self, node_id: int, r: float, g: float, b: float) -> None: pass - def apply_edge_visibility(self, node_id: int, edge_visible: bool) -> None: - pass - def apply_selected(self, node_id: int, selected: bool, diffuse_rgb: list) -> None: pass diff --git a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomMiddle.tsx b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomMiddle.tsx index d59d5131..2da23c74 100644 --- a/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomMiddle.tsx +++ b/src/ansys/visor/visor-client/src/components/ui-panels/Panel_BottomMiddle.tsx @@ -1,4 +1,4 @@ -import { FC, RefObject, useEffect, useRef } from 'react'; +import { FC, useEffect } from 'react'; import { AwcIcons } from '../AwcIcons'; import { VisorFrontend } from '../../VisorFrontend.tsx'; import { makeTooltip } from '../../utils/Tooltip'; @@ -10,7 +10,6 @@ export const Panel_BottomMiddle: FC<{ visorState: VisorFrontend; onLoad: (util: Panel_BottomMiddle_Util) => void; }> = ({ visorState, onLoad }) => { - const edgeVisibility: RefObject = useRef(false); const tableElemId = `tableElem-${crypto.randomUUID()}`; const selectionModeContainerId = `selMode-${crypto.randomUUID()}`; useEffect(() => { 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 28cdc63d..4c87d189 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 @@ -45,6 +45,9 @@ const BOUNDING_BOX_ALGORITHM_ID = 205; const BOUNDING_BOX_OUTLINE_ID = 206; const BOUNDING_BOX_AXES_ID = 207; +/** Three actor node ids for the edge fan-out. Hand-written literals. */ +const FAN_OUT_ACTOR_IDS = [11, 12, 13]; + /** What the wasm camera reports for `GetParallelProjection()`. */ const PARALLEL = 1; const PERSPECTIVE = 0; @@ -102,21 +105,23 @@ function makeFakeWasmObjects() { /** * A scene-graph stand-in for `attachSceneGraph`. * - * `EdgesWidget` fans out through the graph node's own - * `setEdgeVisibilityAsync`; `BoundingBoxWidget` enumerates - * `descendantActorNodesOrSelfArray` when recomputing bounds. Neither is the - * subject here -- what is pinned is the *send* that follows the local write. + * `EdgesWidget` enumerates `descendantActorNodesOrSelfArray` and calls the + * renderer's own per-actor `setEdgeVisibilityAsync` for each node's `id`; + * `BoundingBoxWidget` enumerates the same array when recomputing bounds. + * Neither is the subject of the send tests -- what those pin is the *send* + * that follows the local write, and they pass an empty actor list so the + * fan-out reaches nothing. */ -function makeSceneGraphDouble() { +function makeSceneGraphDouble(actorIds: number[] = []) { return { - setEdgeVisibilityAsync: jest.fn(async () => undefined), - descendantActorNodesOrSelfArray: [], + descendantActorNodesOrSelfArray: actorIds.map((id) => ({ id })), }; } async function makeRenderer( sender: TrameTriggerSender | null, - parallelProjection: number = PERSPECTIVE + parallelProjection: number = PERSPECTIVE, + actorIds: number[] = [] ) { const objects = makeFakeWasmObjects(); const camera = { @@ -145,7 +150,7 @@ async function makeRenderer( makeAnnotation(), sender ); - const sceneGraph = makeSceneGraphDouble(); + const sceneGraph = makeSceneGraphDouble(actorIds); renderer.attachSceneGraph(sceneGraph as unknown as VisorSceneNodeExtended); return { renderer, camera, sceneGraph, ...objects }; } @@ -241,6 +246,32 @@ describe('WasmRenderer widget sends: no sender, and a failing sender', () => { }); }); +describe('WasmRenderer edge fan-out reaches every actor on every call', () => { + test('setEdgeVisibilityGlobalAsync issues one per-actor call per actor on every call, not only when the value changes', async () => { + // The per-node cache that D1 deleted lived on the scene-graph node + // and had no invalidation, so a second toggle to the same value was + // a no-op and three of the six pairs below were absent. This is an + // id set with its value, not a call count: a count would not say + // which actors were reached. + const { renderer } = await makeRenderer(makeSender(), PERSPECTIVE, FAN_OUT_ACTOR_IDS); + const perActor = jest.spyOn(renderer, 'setEdgeVisibilityAsync'); + + await renderer.setEdgeVisibilityGlobalAsync(true); + await renderer.setEdgeVisibilityGlobalAsync(true); + + expect(perActor.mock.calls).toEqual([ + [11, true], + [12, true], + [13, true], + [11, true], + [12, true], + [13, true], + ]); + + perActor.mockRestore(); + }); +}); + describe('WasmRenderer.createAsync seeds the orthographic flag from the wasm camera', () => { test('isOrthographicEnabled is true when the wasm camera reports parallel, before any setter runs', async () => { const { renderer } = await makeRenderer(makeSender(), PARALLEL); diff --git a/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts index 126710a9..8998e36f 100644 --- a/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts +++ b/src/ansys/visor/visor-client/src/renderer/WasmRenderer.ts @@ -131,7 +131,7 @@ export class WasmRenderer implements IRenderer { boxAlgorithmWasmId: this.#boundingBoxBoxAlgorithmWasmId, outlineQueryId: this.#wasmBoundingBoxActorId, }); - this.#edgesWidget = new EdgesWidget(sceneGraph); + this.#edgesWidget = new EdgesWidget(sceneGraph, this); } async renderAsync(): Promise { diff --git a/src/ansys/visor/visor-client/src/state/VisorSceneGraph.tsx b/src/ansys/visor/visor-client/src/state/VisorSceneGraph.tsx index 9f9e8ab3..769e9917 100644 --- a/src/ansys/visor/visor-client/src/state/VisorSceneGraph.tsx +++ b/src/ansys/visor/visor-client/src/state/VisorSceneGraph.tsx @@ -31,7 +31,6 @@ export type VisorSceneNodeExtended = Readonly<{ diffuseRgb: Readonly; visible: boolean; selected: boolean; - edgeVisibility: boolean; opacity: number; descendantsOrSelfArray: VisorSceneNodeExtended[]; descendantsOrSelfDictionary: Record; @@ -44,7 +43,6 @@ export type VisorSceneNodeExtended = Readonly<{ setDiffuseColorRgbAsync: (r: number, g: number, b: number) => Promise; setVisibilityAsync: (visible: boolean) => Promise; setSelectedAsync: (selected: boolean) => Promise; - setEdgeVisibilityAsync: (edgeVisibility: boolean) => Promise; setOpacityAsync: (opacity: number) => Promise; clearColorVariableAsync: () => Promise; setColorVariableAsync: (id: string, component?: number | null) => Promise; @@ -99,7 +97,6 @@ export const CreateVisorSceneGraph = (() => { } let _visible: boolean = true; let _selected: boolean = false; - let _edgeVisibility: boolean = false; let _opacity: number = 1; let _spectrumId: string | null = null; let _spectrumComponent: number = -1; @@ -146,9 +143,6 @@ export const CreateVisorSceneGraph = (() => { get selected() { return _selected; }, - get edgeVisibility() { - return _edgeVisibility; - }, get opacity() { return _opacity; }, @@ -269,21 +263,6 @@ export const CreateVisorSceneGraph = (() => { // stored colour from its own record. await renderer!.sendPartSelectedAsync(nodeId, _selected); }, - async setEdgeVisibilityAsync(edgeVisibility: boolean) { - if (_edgeVisibility === edgeVisibility) { - return; - } - _edgeVisibility = edgeVisibility; - if (node.isGroupNode) { - const promises = []; - for (const n of node.descendantActorNodesOrSelfArray) { - promises.push(n.setEdgeVisibilityAsync(edgeVisibility)); - } - await Promise.all(promises); - return; - } - await renderer!.setEdgeVisibilityAsync(nodeId, edgeVisibility); - }, async setOpacityAsync(opacity: number) { if (_opacity === opacity) { return; diff --git a/src/ansys/visor/visor-client/src/widgets/edgesWidget.ts b/src/ansys/visor/visor-client/src/widgets/edgesWidget.ts index f445a09a..7cccd914 100644 --- a/src/ansys/visor/visor-client/src/widgets/edgesWidget.ts +++ b/src/ansys/visor/visor-client/src/widgets/edgesWidget.ts @@ -1,8 +1,10 @@ import { VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx'; +import type { IRenderer } from '../renderer/IRenderer.ts'; export class EdgesWidget { - constructor(sceneGraph: VisorSceneNodeExtended) { + constructor(sceneGraph: VisorSceneNodeExtended, renderer: IRenderer) { this.#sceneGraph = sceneGraph; + this.#renderer = renderer; } #enabled: boolean = false; @@ -11,9 +13,14 @@ export class EdgesWidget { } #sceneGraph: VisorSceneNodeExtended; + #renderer: IRenderer; setEdgesVisibleAsync = async (enable?: boolean | null): Promise => { this.#enabled = enable ?? !this.#enabled; - await this.#sceneGraph.setEdgeVisibilityAsync(this.#enabled); + const promises = []; + for (const n of this.#sceneGraph.descendantActorNodesOrSelfArray) { + promises.push(this.#renderer.setEdgeVisibilityAsync(n.id, this.#enabled)); + } + await Promise.all(promises); }; } diff --git a/tests/unit/renderer/test_local_renderer.py b/tests/unit/renderer/test_local_renderer.py index 67e1591e..ca9119a1 100644 --- a/tests/unit/renderer/test_local_renderer.py +++ b/tests/unit/renderer/test_local_renderer.py @@ -436,16 +436,12 @@ def test_deregister_all_empties_registry_and_detaches_all_actors(self, renderer) # =========================================================================== class TestPerPartMutations: - """The two permanently un-implemented methods accept their arguments. + """The permanently un-implemented method accepts its arguments. - Both stay no-ops beyond this story: edge visibility is a global display - toggle, and the colour-variable range is not held per part. + It stays a no-op beyond this story: the colour-variable range is not + held per part. """ - def test_apply_edge_visibility(self, renderer): - assert renderer.apply_edge_visibility(1, False) is None - - def test_refresh_color_variable_range(self, renderer): assert ( renderer.refresh_color_variable_range(1, "sp-1", "CELL", "temp", 0) From 93267a1b66553cf3bb57db2cd756bfb02b6f2a36 Mon Sep 17 00:00:00 2001 From: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com> Date: Tue, 22 Sep 2026 21:52:19 +0000 Subject: [PATCH 2/2] chore: adding changelog file 138.added.md [dependabot-skip] --- doc/changelog.d/138.added.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 doc/changelog.d/138.added.md diff --git a/doc/changelog.d/138.added.md b/doc/changelog.d/138.added.md new file mode 100644 index 00000000..f40ab4a1 --- /dev/null +++ b/doc/changelog.d/138.added.md @@ -0,0 +1 @@ +[Remote rendering 3.3b] fold per-part edge visibility into the global toggle