Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
c5c56e0
feat: server-owned variable records
LKasianAnsys Sep 29, 2026
b61c109
feat: clean up server side variable aggregation
LKasianAnsys Sep 29, 2026
59d564b
feat: clean up server side variable aggregation
LKasianAnsys Sep 29, 2026
380aec5
feat: add set_variable_range trigger and width check
LKasianAnsys Sep 29, 2026
10d9f66
chore: adding changelog file 152.added.md [dependabot-skip]
pyansys-ci-bot Oct 1, 2026
4275591
clean up docs
LKasianAnsys Oct 1, 2026
5c8ccca
pre-commit fix
LKasianAnsys Oct 1, 2026
e9b6132
self._renderer.serialize_part_state(node_id) in clear_part_color_vari…
LKasianAnsys Oct 5, 2026
2e821d9
reconcile_part_color_variables after loading
LKasianAnsys Oct 5, 2026
be7deda
adding dataset with wider variable value ranges
LKasianAnsys Oct 6, 2026
43acf7b
feat: move serialize_part_state to renderer
LKasianAnsys Oct 7, 2026
a92f394
chore: make the part re-serialize private to the local renderer
LKasianAnsys Oct 7, 2026
a2e3653
feat: client uses server-sourced variables list instead of building i…
LKasianAnsys Sep 29, 2026
e64b3f3
feat: client sends variable ranges to the server and drops its own ra…
LKasianAnsys Sep 30, 2026
bba2350
pre-commit fix
LKasianAnsys Oct 1, 2026
545a4b8
chore: adding changelog file 153.added.md [dependabot-skip]
pyansys-ci-bot Oct 1, 2026
c7fed6c
reuse lut when already exists
LKasianAnsys Oct 2, 2026
a83a547
update jest test for reusing mapper's LUT
LKasianAnsys Oct 2, 2026
dd73221
Revert "update jest test for reusing mapper's LUT"
LKasianAnsys Oct 5, 2026
857b098
Revert "reuse lut when already exists"
LKasianAnsys Oct 5, 2026
2c9af92
chore: tolerate unsupported variable widths and keep server listeners…
LKasianAnsys Oct 7, 2026
411a63d
Merge branch 'main' into feat/3.5b-client-uses-server-owned-variables
LKasianAnsys Oct 13, 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/153.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[Remote rendering 3.5b] client uses server owned variables
89 changes: 49 additions & 40 deletions src/ansys/visor/visor-client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,50 +113,59 @@ function App() {

wasmView.current.clearObserversAndEventListeners();
const oldFrontend: VisorFrontend = (window as any).__visorState;
const sceneDetailsJson = await wasmView.current.trameTriggerAsync(
'get_visor_scene_details_json'
);
let sceneDetails: VisorSceneDetails;
let newFrontend: VisorFrontend;
// The listeners cleared above are registered again however this
// block ends, so a failed rebuild still receives the next server
// update, and `set_state` keeps reaching the frontend named by
// `__visorState`, which is switched only after the first pass.
try {
sceneDetails = new VisorSceneDetails(sceneDetailsJson);
} catch (err) {
if (err instanceof SchemaVersionMismatchError) {
setSchemaMismatchError(err);
return;
const sceneDetailsJson = await wasmView.current.trameTriggerAsync(
'get_visor_scene_details_json'
);
let sceneDetails: VisorSceneDetails;
try {
sceneDetails = new VisorSceneDetails(sceneDetailsJson);
} catch (err) {
if (err instanceof SchemaVersionMismatchError) {
setSchemaMismatchError(err);
return;
}
throw err;
}
const renderer = await WasmRenderer.createAsync(
wasmView.current.vtkScene,
requireWasmAnnotation(sceneDetails.vtkInfo.rendererAnnotation),
wasmView.current.trameTriggerAsync
);
newFrontend = new VisorFrontend(
renderer,
sceneDetails.vtkInfo.sceneGraph,
wasmView.current.trameTriggerAsync
);
// Release the frontend being replaced, here and not later: the
// `VtkScene` behind both renderers is the same object across a
// rebuild, so an unreleased subscription stays live and the next
// gesture is reported once per surviving frontend. There is
// deliberately no `await` between the new frontend subscribing (the
// constructor above) and the old one releasing, so the window in
// which two subscriptions coexist contains no suspension point.
if (oldFrontend != null) {
oldFrontend.releaseCameraSettledListener();
}
throw err;
}
const renderer = await WasmRenderer.createAsync(
wasmView.current.vtkScene,
requireWasmAnnotation(sceneDetails.vtkInfo.rendererAnnotation),
wasmView.current.trameTriggerAsync
);
const newFrontend = new VisorFrontend(
renderer,
sceneDetails.vtkInfo.sceneGraph,
wasmView.current.trameTriggerAsync
);
// Release the frontend being replaced, here and not later: the
// `VtkScene` behind both renderers is the same object across a
// rebuild, so an unreleased subscription stays live and the next
// gesture is reported once per surviving frontend. There is
// deliberately no `await` between the new frontend subscribing (the
// constructor above) and the old one releasing, so the window in
// which two subscriptions coexist contains no suspension point.
if (oldFrontend != null) {
oldFrontend.releaseCameraSettledListener();
}

if (visorArgs.current.darkMode != null) {
// Explicit Dash prop takes precedence over the server's dark_mode value.
sceneDetails.appState.ui.setDarkTheme(visorArgs.current.darkMode);
if (visorArgs.current.darkMode != null) {
// Explicit Dash prop takes precedence over the server's dark_mode value.
sceneDetails.appState.ui.setDarkTheme(visorArgs.current.darkMode);
}
await newFrontend.setAppStateAsync(sceneDetails.appState, false);
(window as any).__visorState = newFrontend;
setSchemaMismatchError(null);
setVisorFrontend(newFrontend); // rebuild/remount React UI
} finally {
wasmView.current.addServerUpdatedListener(onServerUpdateAsync);
wasmView.current.addGetStateListener(onGetState);
wasmView.current.addSetStateListener(onSetStateAsync);
}
(window as any).__visorState = newFrontend;
await newFrontend.setAppStateAsync(sceneDetails.appState, false);
setVisorFrontend(newFrontend); // rebuild/remount React UI
wasmView.current.addServerUpdatedListener(onServerUpdateAsync);
wasmView.current.addGetStateListener(onGetState);
wasmView.current.addSetStateListener(onSetStateAsync);
if (oldFrontend != null) {
const oldAppState = await oldFrontend.getAppStateAsync();
await newFrontend.setAppStateAsync(oldAppState);
Expand Down
82 changes: 39 additions & 43 deletions src/ansys/visor/visor-client/src/VisorFrontend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { getPromiseResolver } from './utils/JsHelpers';
import VisorAppState from './state/appstate/VisorAppState.tsx';
import VisorDatasetState from './state/appstate/VisorDatasetState.tsx';
import VisorPartState from './state/appstate/VisorPartState.tsx';
import VisorVariableState from './state/appstate/VisorVariableState.tsx';
import { TreeViewUtil } from './treeview/TreeView.tsx';
import { StateInput } from './state/appstate/VisorStateCommon.tsx';
import VisorVtkSceneNode from './state/appstate/vtkInfo/VisorVtkSceneNode.tsx';
Expand Down Expand Up @@ -66,7 +65,6 @@ export class VisorFrontend {
// Respect base path prefix set by the Dash component (for reverse-proxy deployments).
const bp: string = ((window as any).__visorArgs?.basePath ?? '').replace(/\/$/, '');

const self = this;
this.globalVariableCollection = variableManager.globalVariableCollection;
this.#unit = '';
this.darkMode = darkMode;
Expand Down Expand Up @@ -100,6 +98,18 @@ export class VisorFrontend {
remover?.();
};
this.defaultActorColor = [];
// Reports one variable slot's range; errors are logged, not rethrown,
// since the client already applied the range locally.
this.sendVariableRangeAsync = async (variableId, component, min, max) => {
try {
await triggerSender('set_variable_range', { variableId, component, min, max });
} catch (err) {
console.error(
`[VISOR] variable range trigger send failed: variableId='${variableId}' component=${component}`,
err
);
}
};
this.setVariableRangeAsync = async (variableId, component, min, max) => {
const variable = variableManager.globalVariableCollection.getVariable(variableId);
if (variable == null) {
Expand All @@ -114,6 +124,7 @@ export class VisorFrontend {
await actorNode.setScalarRangeAsync(min, max);
}
}
await this.sendVariableRangeAsync(variableId, component, min, max);
};
this.addCameraChangedListener = (callback) => {
return renderer.addCameraChangedListener(callback);
Expand Down Expand Up @@ -305,29 +316,9 @@ export class VisorFrontend {
}
sceneState.copyDataset(datasetState);
}
const variableInfos = variableManager.globalVariableCollection;
for (const variableInfo of variableInfos.array) {
const variableState = new VisorVariableState();
variableState.setId(variableInfo.id.toString());
variableState.setArrayName(variableInfo.name);
variableState.setType(variableInfo.type);
variableState.setNumComponents(variableInfo.numComponents);
const magnitudeRange = variableInfo.getRangeInfo(-1);
if (magnitudeRange == null) {
throw new Error(`range at component ${-1} not found`);
}
variableState.setMagnitudeRange(magnitudeRange.customRange);
const ranges: number[][] = [];
for (let i = 0; i < variableInfo.numComponents; i++) {
const range = variableInfo.getRangeInfo(i);
if (range == null) {
throw new Error(`range at component ${i} not found`);
}
ranges.push(range.customRange);
}
variableState.setRanges(ranges);
sceneState.copyVariable(variableState);
}
// No variable block: the server owns the variable records and
// delivers them on every push. Carrying a copy across a rebuild
// would replace the delivered records with this one.
return appState;
};
this.setAppStateAsync = async (state, updateUI) => {
Expand Down Expand Up @@ -460,6 +451,13 @@ export class VisorFrontend {
const promise = renderer.setCrossSectionNormalAsync(crossSectionState.normal);
promises.push(promise);
}
// Held before the part loop: a part coloured below reads its
// range from these records. Guarded on presence, so a state with
// no variable block leaves the held records alone, while an empty
// block clears them.
if (sceneState.hasVariableStates) {
variableManager.setRecords(sceneState.getVariableStates());
}
for (const dataset_state of sceneState.getDatasetStates()) {
for (const part_state of dataset_state.getPartStates()) {
let node;
Expand Down Expand Up @@ -522,24 +520,7 @@ export class VisorFrontend {
}
}
}
// wait for all the parts to be updated before updating the variable ranges
await Promise.all(promises);
promises.length = 0;
for (const variable_state of sceneState.getVariableStates()) {
const idStr = variable_state.id;
if (variable_state.magnitudeRange !== undefined) {
const range = variable_state.magnitudeRange;
const promise = self.setVariableRangeAsync(idStr, -1, range[0], range[1]);
promises.push(promise);
}
for (let i = 0; i < variable_state.ranges.length; i++) {
const range = variable_state.ranges[i];
if (range !== undefined) {
const promise = self.setVariableRangeAsync(idStr, i, range[0], range[1]);
promises.push(promise);
}
}
}
// Every part is applied before the tree and the panel re-read them.
await Promise.all(promises);
if (updateUI) {
await panelTopRightUtilPromise;
Expand All @@ -549,6 +530,13 @@ export class VisorFrontend {
// above; the tree derives its rows from them here.
treeViewUtil.synchronize();
}
// After the tree has taken the delivered selection, so the panel
// re-reads that selection's ranges from the records just held.
// Skipped until the panel exists; its mount reads them itself.
if (isPanelTopRightUtilSet()) {
const panelTopRight = await panelTopRightUtilPromise;
await panelTopRight.refreshSelectionAsync();
}
await renderer.resizeAsync();
};
Object.freeze(this);
Expand Down Expand Up @@ -587,6 +575,14 @@ export class VisorFrontend {
toggleFullScreenAsync: () => Promise<void>;
addCameraChangedListener: (callback: (cameraState: VisorCameraState) => void) => () => void;
globalVariableCollection: VisorVariableCollection;
/** Reports one variable slot's range to the server, -1 being the magnitude; a failed send is logged, not rethrown. */
sendVariableRangeAsync: (
variableId: string,
component: number,
min: number,
max: number
) => Promise<void>;
/** Writes a variable slot's range to the held record, applies it to every part coloured by that slot, then reports it to the server. */
setVariableRangeAsync: (
variableId: string,
component: number,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -493,7 +493,8 @@ export const Panel_TopRight: FC<{
legendTabElem.click();
}
},
() => tabIndex
() => tabIndex,
() => onSelectionChangeAsync(treeViewUtil.selectedNodes)
);
visorState.setPanelTopRightUtil(util);
// Must stay on the line after the util handoff: it suppresses the three mount writes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ export class Panel_TopRight_Util {
getIsPanelCollapsed: () => boolean,
getIsLegendCollapsed: () => boolean,
selectTab: (tabIndex: number) => void,
getTabIndex: () => number
getTabIndex: () => number,
refreshSelectionAsync: () => Promise<void>
) {
this.expandPanel = expandPanel;
this.collapsePanel = collapsePanel;
Expand All @@ -17,6 +18,7 @@ export class Panel_TopRight_Util {
this.#getIsLegendCollapsed = getIsLegendCollapsed;
this.selectTab = selectTab;
this.#getTabIndex = getTabIndex;
this.refreshSelectionAsync = refreshSelectionAsync;
}

readonly collapsePanel: () => void;
Expand All @@ -40,4 +42,7 @@ export class Panel_TopRight_Util {
get tabIndex() {
return this.#getTabIndex();
}

/** Re-reads the current selection into the properties and legend panels. */
readonly refreshSelectionAsync: () => Promise<void>;
}
Loading
Loading