Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
38c5e9e
add per-part write path to the dataset registry
LKasianAnsys Aug 28, 2026
ac71d50
remove story/increment reference in comment
LKasianAnsys Aug 28, 2026
ee0b843
chore: adding changelog file 50.added.md [dependabot-skip]
pyansys-ci-bot Aug 28, 2026
ff688aa
chore: adding changelog file 50.added.md [dependabot-skip]
pyansys-ci-bot Aug 28, 2026
ccc2f0f
implement visibility, opacity and diffuse-color applies on the local …
LKasianAnsys Aug 28, 2026
c62294f
move selection and color-variable applies onto VtkNodePipeline
LKasianAnsys Aug 28, 2026
361c20e
chore: adding changelog file 51.added.md [dependabot-skip]
pyansys-ci-bot Aug 28, 2026
0c50362
move remaining three vtk calls to node pipeline
LKasianAnsys Aug 31, 2026
174ee07
register per-part trigger handlers and serialize VTK access with a lock
LKasianAnsys Aug 28, 2026
34b27f0
replace hand-written payloads in LocalApp with Pydantic models and de…
LKasianAnsys Aug 28, 2026
77f9a9d
comment out wasm flush
LKasianAnsys Aug 28, 2026
44233a4
chore: adding changelog file 52.added.md [dependabot-skip]
pyansys-ci-bot Aug 28, 2026
85b6630
remove TODOs and update comment on flush_wasm_state
LKasianAnsys Aug 31, 2026
42efe36
route client per-part mutations through the server triggers
LKasianAnsys Aug 28, 2026
a5a38ce
chore: adding changelog file 53.added.md [dependabot-skip]
pyansys-ci-bot Aug 28, 2026
f16d5de
prettier fixes
LKasianAnsys Aug 28, 2026
5d5530c
update regression test reference to match updated behaviour
LKasianAnsys Aug 31, 2026
47fc0a0
Revert "update regression test reference to match updated behaviour"
LKasianAnsys Aug 31, 2026
707ffb1
reorder regression tests to get clean initial baseline
LKasianAnsys Aug 31, 2026
42e6a3d
reorder regression test color by order to end up with magnitude
LKasianAnsys Aug 31, 2026
786211f
Merge branch 'main' into feat/3.1a-registry-per-part-write-path
LKasianAnsys Aug 31, 2026
3cc3e94
Merge branch 'feat/3.1a-registry-per-part-write-path' into feat/3.1b-…
LKasianAnsys Aug 31, 2026
0477060
Merge branch 'feat/3.1b-per-part-apply-logic' into feat/3.1c-triggers…
LKasianAnsys Aug 31, 2026
ef4700b
Merge branch 'feat/3.1c-triggers-lock-and-payload-validation' into fe…
LKasianAnsys Aug 31, 2026
4a62fc0
Merge branch 'main' into feat/3.1d-client-routes-mutations-to-triggers
LKasianAnsys Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changelog.d/53.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Remote rendering 3.1d - route client per-part mutations through the server triggers
3 changes: 2 additions & 1 deletion src/ansys/visor/visor-client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ function App() {
}
const renderer = await WasmRenderer.createAsync(
wasmView.current.vtkScene,
requireWasmAnnotation(sceneDetails.vtkInfo.rendererAnnotation)
requireWasmAnnotation(sceneDetails.vtkInfo.rendererAnnotation),
wasmView.current.trameTriggerAsync
);
const newFrontend = new VisorFrontend(renderer, sceneDetails.vtkInfo.sceneGraph);
if (visorArgs.current.darkMode != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
import { CreateVisorSceneGraph, VisorSceneNodeExtended } from '../state/VisorSceneGraph.tsx';
import VisorVtkDataArray from '../state/appstate/vtkInfo/VisorVtkDataArray.tsx';
import type { IRenderer } from '../renderer/IRenderer';

/**
* The scene graph's six user actions, each of which must now do two things:
* apply to the client's own objects, as it always did, and send the matching
* mutation to the server.
*
* Every action is covered by a *pair* of tests, one per half. That is not
* redundancy: a single test asserting "both happened" would still pass if the
* two were fused, and the failure this pairing exists to catch is a silently
* dropped send, which the render cannot reveal because the client is still
* painting the same result itself.
*
* Expected values here are hand-written literals. In particular the
* colour-variable descriptor is written out rather than read back from the
* spectrum, so the test cannot agree with the code by making the same mistake.
*/

const ROOT_ID = 0;
const PART_A_ID = 1;
const PART_B_ID = 2;

const VARIABLE_ID = 'POINT::pressure::1';

function makePressureArray(): VisorVtkDataArray {
return new VisorVtkDataArray({
indexForType: 0,
type: 'POINT',
name: 'pressure',
numComponents: 1,
magnitudeRange: [0, 10],
ranges: [[2, 8]],
});
}

function makeRendererDouble() {
return {
// The per-part applies that already existed.
setVisibilityAsync: jest.fn(async () => undefined),
setSelectedAsync: jest.fn(async () => undefined),
setOpacityAsync: jest.fn(async () => undefined),
setDiffuseColorRgbAsync: jest.fn(async () => undefined),
resetDiffuseColorAsync: jest.fn(async () => undefined),
setColorVariableAsync: jest.fn(async () => undefined),
clearColorVariableAsync: jest.fn(async () => undefined),
setEdgeVisibilityAsync: jest.fn(async () => undefined),
setScalarRangeAsync: jest.fn(async () => undefined),
// The per-part sends added for the server path.
sendPartVisibilityAsync: jest.fn(async () => undefined),
sendPartOpacityAsync: jest.fn(async () => undefined),
sendPartDiffuseColorAsync: jest.fn(async () => undefined),
sendPartSelectedAsync: jest.fn(async () => undefined),
sendPartColorVariableAsync: jest.fn(async () => undefined),
sendClearPartColorVariableAsync: jest.fn(async () => undefined),
};
}

type RendererDouble = ReturnType<typeof makeRendererDouble>;

function makeGraph(renderer: RendererDouble): VisorSceneNodeExtended {
return CreateVisorSceneGraph(
{
id: ROOT_ID,
dataArrays: [],
name: '',
isGroupNode: true,
isActorNode: false,
nodeType: 'root',
diffuseColor: [1, 1, 1],
bounds: [],
children: [
{
id: PART_A_ID,
dataArrays: [makePressureArray()],
name: 'part-a',
isGroupNode: false,
isActorNode: true,
nodeType: 'vtkUnstructuredGrid',
diffuseColor: [1, 1, 1],
bounds: [],
children: [],
},
{
id: PART_B_ID,
dataArrays: [makePressureArray()],
name: 'part-b',
isGroupNode: false,
isActorNode: true,
nodeType: 'vtkUnstructuredGrid',
diffuseColor: [1, 1, 1],
bounds: [],
children: [],
},
],
},
undefined,
renderer as unknown as IRenderer
);
}

function setUp() {
const renderer = makeRendererDouble();
const graph = makeGraph(renderer);
const partA = graph.descendantActorNodesOrSelfDictionary[PART_A_ID];
return { renderer, graph, partA };
}

describe('hide / show', () => {
test('sends set_part_visibility for the part', async () => {
const { renderer, partA } = setUp();

await partA.setVisibilityAsync(false);

expect(renderer.sendPartVisibilityAsync).toHaveBeenCalledWith(PART_A_ID, false);
});

test('still applies visibility to the client renderer', async () => {
const { renderer, partA } = setUp();

await partA.setVisibilityAsync(false);

expect(renderer.setVisibilityAsync).toHaveBeenCalledWith(PART_A_ID, false);
});
});

describe('opacity', () => {
test('sends set_part_opacity for the part', async () => {
const { renderer, partA } = setUp();

await partA.setOpacityAsync(0.25);

expect(renderer.sendPartOpacityAsync).toHaveBeenCalledWith(PART_A_ID, 0.25);
});

test('still applies opacity to the client renderer', async () => {
const { renderer, partA } = setUp();

await partA.setOpacityAsync(0.25);

expect(renderer.setOpacityAsync).toHaveBeenCalledWith(PART_A_ID, 0.25);
});
});

describe('custom colour, set by rgb', () => {
test('sends the normalised colour', async () => {
const { renderer, partA } = setUp();

await partA.setDiffuseColorRgbAsync(0.25, 0.5, 0.75);

expect(renderer.sendPartDiffuseColorAsync).toHaveBeenCalledWith(
PART_A_ID,
[0.25, 0.5, 0.75]
);
});

test('still applies the colour to the client renderer', async () => {
const { renderer, partA } = setUp();

await partA.setDiffuseColorRgbAsync(0.25, 0.5, 0.75);

expect(renderer.setDiffuseColorRgbAsync).toHaveBeenCalledWith(
PART_A_ID,
0.25,
0.5,
0.75,
false
);
});
});

describe('custom colour, set by hex', () => {
test('sends the normalised colour', async () => {
const { renderer, partA } = setUp();

await partA.setDiffuseColorHexAsync('#ff0000');

expect(renderer.sendPartDiffuseColorAsync).toHaveBeenCalledWith(PART_A_ID, [1, 0, 0]);
});

test('still applies the colour to the client renderer', async () => {
const { renderer, partA } = setUp();

await partA.setDiffuseColorHexAsync('#ff0000');

expect(renderer.setDiffuseColorRgbAsync).toHaveBeenCalledWith(PART_A_ID, 1, 0, 0, false);
});
});

describe('custom colour, reset', () => {
test('sends a null colour, not the default colour value', async () => {
const { renderer, partA } = setUp();

await partA.resetDiffuseColorAsync();

expect(renderer.sendPartDiffuseColorAsync).toHaveBeenCalledWith(PART_A_ID, null);
});

test('still applies the reset to the client renderer', async () => {
const { renderer, partA } = setUp();

await partA.resetDiffuseColorAsync();

expect(renderer.resetDiffuseColorAsync).toHaveBeenCalledWith(PART_A_ID, [1, 1, 1], false);
});
});

describe('colour by variable', () => {
test('sends the descriptor for the part', async () => {
const { renderer, partA } = setUp();

await partA.setColorVariableAsync(VARIABLE_ID, 0);

expect(renderer.sendPartColorVariableAsync).toHaveBeenCalledWith(PART_A_ID, {
spectrumId: 'POINT::pressure::1',
spectrumType: 'POINT',
spectrumName: 'pressure',
component: 0,
min: 2,
max: 8,
});
});

test('still applies the colour variable to the client renderer', async () => {
const { renderer, partA } = setUp();

await partA.setColorVariableAsync(VARIABLE_ID, 0);

expect(renderer.setColorVariableAsync).toHaveBeenCalledWith(PART_A_ID, {
spectrumId: 'POINT::pressure::1',
spectrumType: 'POINT',
spectrumName: 'pressure',
component: 0,
min: 2,
max: 8,
});
});
});

describe('clear colour by variable', () => {
test('sends clear_part_color_variable for the part', async () => {
const { renderer, partA } = setUp();

await partA.clearColorVariableAsync();

expect(renderer.sendClearPartColorVariableAsync).toHaveBeenCalledWith(PART_A_ID);
});

test('still clears the colour variable on the client renderer', async () => {
const { renderer, partA } = setUp();

await partA.clearColorVariableAsync();

expect(renderer.clearColorVariableAsync).toHaveBeenCalledWith(PART_A_ID);
});
});

describe('selection', () => {
test('sends set_part_selected for the part, with no colour', async () => {
const { renderer, partA } = setUp();

await partA.setSelectedAsync(true);

expect(renderer.sendPartSelectedAsync).toHaveBeenCalledWith(PART_A_ID, true);
});

test('still applies the selection to the client renderer, with its colour', async () => {
const { renderer, partA } = setUp();

await partA.setSelectedAsync(true);

expect(renderer.setSelectedAsync).toHaveBeenCalledWith(PART_A_ID, true, [1, 1, 1]);
});
});

describe('group fan-out', () => {
test('a group action sends for exactly the actor nodes beneath it', async () => {
// Asserted as the set of node ids the sender saw, not as a count: the
// design is deliberately insensitive to how many messages an action
// produces, so a count would pin the wrong property.
const { renderer, graph } = setUp();

await graph.setVisibilityAsync(false);

const ids = renderer.sendPartVisibilityAsync.mock.calls.map(
(call) => (call as unknown as [number, boolean])[0]
);
expect(new Set(ids)).toEqual(new Set([PART_A_ID, PART_B_ID]));
expect(ids).not.toContain(ROOT_ID);
});

test('a group action still applies to exactly the actor nodes beneath it', async () => {
const { renderer, graph } = setUp();

await graph.setVisibilityAsync(false);

const ids = renderer.setVisibilityAsync.mock.calls.map(
(call) => (call as unknown as [number, boolean])[0]
);
expect(new Set(ids)).toEqual(new Set([PART_A_ID, PART_B_ID]));
expect(ids).not.toContain(ROOT_ID);
});
});
Loading
Loading