Skip to content

Commit 889f460

Browse files
feat: Remote rendering 3.1d - route client per-part mutations through the server triggers (#53)
Co-authored-by: pyansys-ci-bot <92810346+pyansys-ci-bot@users.noreply.github.com>
1 parent a27249d commit 889f460

9 files changed

Lines changed: 1032 additions & 89 deletions

File tree

‎doc/changelog.d/53.added.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Remote rendering 3.1d - route client per-part mutations through the server triggers

‎src/ansys/visor/visor-client/src/App.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,8 @@ function App() {
128128
}
129129
const renderer = await WasmRenderer.createAsync(
130130
wasmView.current.vtkScene,
131-
requireWasmAnnotation(sceneDetails.vtkInfo.rendererAnnotation)
131+
requireWasmAnnotation(sceneDetails.vtkInfo.rendererAnnotation),
132+
wasmView.current.trameTriggerAsync
132133
);
133134
const newFrontend = new VisorFrontend(renderer, sceneDetails.vtkInfo.sceneGraph);
134135
if (visorArgs.current.darkMode != null) {
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
import { CreateVisorSceneGraph, VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx';
2+
import VisorVtkDataArray from '../state/appstate/vtkInfo/VisorVtkDataArray.tsx';
3+
import type { IRenderer } from '../renderer/IRenderer';
4+
5+
/**
6+
* The scene graph's six user actions, each of which must now do two things:
7+
* apply to the client's own objects, as it always did, and send the matching
8+
* mutation to the server.
9+
*
10+
* Every action is covered by a *pair* of tests, one per half. That is not
11+
* redundancy: a single test asserting "both happened" would still pass if the
12+
* two were fused, and the failure this pairing exists to catch is a silently
13+
* dropped send, which the render cannot reveal because the client is still
14+
* painting the same result itself.
15+
*
16+
* Expected values here are hand-written literals. In particular the
17+
* colour-variable descriptor is written out rather than read back from the
18+
* spectrum, so the test cannot agree with the code by making the same mistake.
19+
*/
20+
21+
const ROOT_ID = 0;
22+
const PART_A_ID = 1;
23+
const PART_B_ID = 2;
24+
25+
const VARIABLE_ID = 'POINT::pressure::1';
26+
27+
function makePressureArray(): VisorVtkDataArray {
28+
return new VisorVtkDataArray({
29+
indexForType: 0,
30+
type: 'POINT',
31+
name: 'pressure',
32+
numComponents: 1,
33+
magnitudeRange: [0, 10],
34+
ranges: [[2, 8]],
35+
});
36+
}
37+
38+
function makeRendererDouble() {
39+
return {
40+
// The per-part applies that already existed.
41+
setVisibilityAsync: jest.fn(async () => undefined),
42+
setSelectedAsync: jest.fn(async () => undefined),
43+
setOpacityAsync: jest.fn(async () => undefined),
44+
setDiffuseColorRgbAsync: jest.fn(async () => undefined),
45+
resetDiffuseColorAsync: jest.fn(async () => undefined),
46+
setColorVariableAsync: jest.fn(async () => undefined),
47+
clearColorVariableAsync: jest.fn(async () => undefined),
48+
setEdgeVisibilityAsync: jest.fn(async () => undefined),
49+
setScalarRangeAsync: jest.fn(async () => undefined),
50+
// The per-part sends added for the server path.
51+
sendPartVisibilityAsync: jest.fn(async () => undefined),
52+
sendPartOpacityAsync: jest.fn(async () => undefined),
53+
sendPartDiffuseColorAsync: jest.fn(async () => undefined),
54+
sendPartSelectedAsync: jest.fn(async () => undefined),
55+
sendPartColorVariableAsync: jest.fn(async () => undefined),
56+
sendClearPartColorVariableAsync: jest.fn(async () => undefined),
57+
};
58+
}
59+
60+
type RendererDouble = ReturnType<typeof makeRendererDouble>;
61+
62+
function makeGraph(renderer: RendererDouble): VisorSceneNodeExtended {
63+
return CreateVisorSceneGraph(
64+
{
65+
id: ROOT_ID,
66+
dataArrays: [],
67+
name: '',
68+
isGroupNode: true,
69+
isActorNode: false,
70+
nodeType: 'root',
71+
diffuseColor: [1, 1, 1],
72+
bounds: [],
73+
children: [
74+
{
75+
id: PART_A_ID,
76+
dataArrays: [makePressureArray()],
77+
name: 'part-a',
78+
isGroupNode: false,
79+
isActorNode: true,
80+
nodeType: 'vtkUnstructuredGrid',
81+
diffuseColor: [1, 1, 1],
82+
bounds: [],
83+
children: [],
84+
},
85+
{
86+
id: PART_B_ID,
87+
dataArrays: [makePressureArray()],
88+
name: 'part-b',
89+
isGroupNode: false,
90+
isActorNode: true,
91+
nodeType: 'vtkUnstructuredGrid',
92+
diffuseColor: [1, 1, 1],
93+
bounds: [],
94+
children: [],
95+
},
96+
],
97+
},
98+
undefined,
99+
renderer as unknown as IRenderer
100+
);
101+
}
102+
103+
function setUp() {
104+
const renderer = makeRendererDouble();
105+
const graph = makeGraph(renderer);
106+
const partA = graph.descendantActorNodesOrSelfDictionary[PART_A_ID];
107+
return { renderer, graph, partA };
108+
}
109+
110+
describe('hide / show', () => {
111+
test('sends set_part_visibility for the part', async () => {
112+
const { renderer, partA } = setUp();
113+
114+
await partA.setVisibilityAsync(false);
115+
116+
expect(renderer.sendPartVisibilityAsync).toHaveBeenCalledWith(PART_A_ID, false);
117+
});
118+
119+
test('still applies visibility to the client renderer', async () => {
120+
const { renderer, partA } = setUp();
121+
122+
await partA.setVisibilityAsync(false);
123+
124+
expect(renderer.setVisibilityAsync).toHaveBeenCalledWith(PART_A_ID, false);
125+
});
126+
});
127+
128+
describe('opacity', () => {
129+
test('sends set_part_opacity for the part', async () => {
130+
const { renderer, partA } = setUp();
131+
132+
await partA.setOpacityAsync(0.25);
133+
134+
expect(renderer.sendPartOpacityAsync).toHaveBeenCalledWith(PART_A_ID, 0.25);
135+
});
136+
137+
test('still applies opacity to the client renderer', async () => {
138+
const { renderer, partA } = setUp();
139+
140+
await partA.setOpacityAsync(0.25);
141+
142+
expect(renderer.setOpacityAsync).toHaveBeenCalledWith(PART_A_ID, 0.25);
143+
});
144+
});
145+
146+
describe('custom colour, set by rgb', () => {
147+
test('sends the normalised colour', async () => {
148+
const { renderer, partA } = setUp();
149+
150+
await partA.setDiffuseColorRgbAsync(0.25, 0.5, 0.75);
151+
152+
expect(renderer.sendPartDiffuseColorAsync).toHaveBeenCalledWith(
153+
PART_A_ID,
154+
[0.25, 0.5, 0.75]
155+
);
156+
});
157+
158+
test('still applies the colour to the client renderer', async () => {
159+
const { renderer, partA } = setUp();
160+
161+
await partA.setDiffuseColorRgbAsync(0.25, 0.5, 0.75);
162+
163+
expect(renderer.setDiffuseColorRgbAsync).toHaveBeenCalledWith(
164+
PART_A_ID,
165+
0.25,
166+
0.5,
167+
0.75,
168+
false
169+
);
170+
});
171+
});
172+
173+
describe('custom colour, set by hex', () => {
174+
test('sends the normalised colour', async () => {
175+
const { renderer, partA } = setUp();
176+
177+
await partA.setDiffuseColorHexAsync('#ff0000');
178+
179+
expect(renderer.sendPartDiffuseColorAsync).toHaveBeenCalledWith(PART_A_ID, [1, 0, 0]);
180+
});
181+
182+
test('still applies the colour to the client renderer', async () => {
183+
const { renderer, partA } = setUp();
184+
185+
await partA.setDiffuseColorHexAsync('#ff0000');
186+
187+
expect(renderer.setDiffuseColorRgbAsync).toHaveBeenCalledWith(PART_A_ID, 1, 0, 0, false);
188+
});
189+
});
190+
191+
describe('custom colour, reset', () => {
192+
test('sends a null colour, not the default colour value', async () => {
193+
const { renderer, partA } = setUp();
194+
195+
await partA.resetDiffuseColorAsync();
196+
197+
expect(renderer.sendPartDiffuseColorAsync).toHaveBeenCalledWith(PART_A_ID, null);
198+
});
199+
200+
test('still applies the reset to the client renderer', async () => {
201+
const { renderer, partA } = setUp();
202+
203+
await partA.resetDiffuseColorAsync();
204+
205+
expect(renderer.resetDiffuseColorAsync).toHaveBeenCalledWith(PART_A_ID, [1, 1, 1], false);
206+
});
207+
});
208+
209+
describe('colour by variable', () => {
210+
test('sends the descriptor for the part', async () => {
211+
const { renderer, partA } = setUp();
212+
213+
await partA.setColorVariableAsync(VARIABLE_ID, 0);
214+
215+
expect(renderer.sendPartColorVariableAsync).toHaveBeenCalledWith(PART_A_ID, {
216+
spectrumId: 'POINT::pressure::1',
217+
spectrumType: 'POINT',
218+
spectrumName: 'pressure',
219+
component: 0,
220+
min: 2,
221+
max: 8,
222+
});
223+
});
224+
225+
test('still applies the colour variable to the client renderer', async () => {
226+
const { renderer, partA } = setUp();
227+
228+
await partA.setColorVariableAsync(VARIABLE_ID, 0);
229+
230+
expect(renderer.setColorVariableAsync).toHaveBeenCalledWith(PART_A_ID, {
231+
spectrumId: 'POINT::pressure::1',
232+
spectrumType: 'POINT',
233+
spectrumName: 'pressure',
234+
component: 0,
235+
min: 2,
236+
max: 8,
237+
});
238+
});
239+
});
240+
241+
describe('clear colour by variable', () => {
242+
test('sends clear_part_color_variable for the part', async () => {
243+
const { renderer, partA } = setUp();
244+
245+
await partA.clearColorVariableAsync();
246+
247+
expect(renderer.sendClearPartColorVariableAsync).toHaveBeenCalledWith(PART_A_ID);
248+
});
249+
250+
test('still clears the colour variable on the client renderer', async () => {
251+
const { renderer, partA } = setUp();
252+
253+
await partA.clearColorVariableAsync();
254+
255+
expect(renderer.clearColorVariableAsync).toHaveBeenCalledWith(PART_A_ID);
256+
});
257+
});
258+
259+
describe('selection', () => {
260+
test('sends set_part_selected for the part, with no colour', async () => {
261+
const { renderer, partA } = setUp();
262+
263+
await partA.setSelectedAsync(true);
264+
265+
expect(renderer.sendPartSelectedAsync).toHaveBeenCalledWith(PART_A_ID, true);
266+
});
267+
268+
test('still applies the selection to the client renderer, with its colour', async () => {
269+
const { renderer, partA } = setUp();
270+
271+
await partA.setSelectedAsync(true);
272+
273+
expect(renderer.setSelectedAsync).toHaveBeenCalledWith(PART_A_ID, true, [1, 1, 1]);
274+
});
275+
});
276+
277+
describe('group fan-out', () => {
278+
test('a group action sends for exactly the actor nodes beneath it', async () => {
279+
// Asserted as the set of node ids the sender saw, not as a count: the
280+
// design is deliberately insensitive to how many messages an action
281+
// produces, so a count would pin the wrong property.
282+
const { renderer, graph } = setUp();
283+
284+
await graph.setVisibilityAsync(false);
285+
286+
const ids = renderer.sendPartVisibilityAsync.mock.calls.map(
287+
(call) => (call as unknown as [number, boolean])[0]
288+
);
289+
expect(new Set(ids)).toEqual(new Set([PART_A_ID, PART_B_ID]));
290+
expect(ids).not.toContain(ROOT_ID);
291+
});
292+
293+
test('a group action still applies to exactly the actor nodes beneath it', async () => {
294+
const { renderer, graph } = setUp();
295+
296+
await graph.setVisibilityAsync(false);
297+
298+
const ids = renderer.setVisibilityAsync.mock.calls.map(
299+
(call) => (call as unknown as [number, boolean])[0]
300+
);
301+
expect(new Set(ids)).toEqual(new Set([PART_A_ID, PART_B_ID]));
302+
expect(ids).not.toContain(ROOT_ID);
303+
});
304+
});

0 commit comments

Comments
 (0)