Skip to content
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