Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/67.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Apply selected to the scene-graph node and seed tree rows from node state
31 changes: 23 additions & 8 deletions src/ansys/visor/visor-client/src/VisorFrontend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,6 @@ export class VisorFrontend {
panelTopRight.selectTab(uiState.panelTopRightTabIndex);
}
})();
const selectedNodeIds: number[] = [];
const promises: Promise<void>[] = [];
const sceneState = appState.scene;
if (sceneState.unit !== undefined) {
Expand Down Expand Up @@ -425,15 +424,30 @@ export class VisorFrontend {
node = sceneGraph.descendantActorNodesOrSelfDictionary[part_state.id];
}
if (node != null) {
if (part_state.selected === true) {
const idNumber = parseInt(part_state.id);
isNaN(idNumber) || selectedNodeIds.push(idNumber);
}
// Narrow once: `node` is a `let`, so the null check
// above does not survive into the callbacks below.
const partNode = node;
// The diffuse-colour path and the selection path both
// write this actor's ambient/diffuse properties with
// different values, so they must be sequenced for this
// node rather than raced as independent promises.
let partPromise: Promise<void> = Promise.resolve();
if (part_state.diffuseRgb !== undefined) {
const [r, g, b] = part_state.diffuseRgb;
const promise = node.setDiffuseColorRgbAsync(r, g, b);
promises.push(promise);
partPromise = partPromise.then(() =>
partNode.setDiffuseColorRgbAsync(r, g, b)
);
}
// `selected` is applied to the node, like every other
// part field, so the node owns the value and both the
// renderer and the tree read it from there. `=== true`
// holds the current semantics: a part state carrying
// null (never touched) deselects, and the node always
// receives a boolean.
partPromise = partPromise.then(() =>
partNode.setSelectedAsync(part_state.selected === true)
);
promises.push(partPromise);
if (part_state.opacity !== undefined) {
const promise = node.setOpacityAsync(part_state.opacity);
promises.push(promise);
Expand Down Expand Up @@ -483,7 +497,8 @@ export class VisorFrontend {
await panelTopRightUtilPromise;
const treeViewUtil: TreeViewUtil<VisorSceneNodeExtended> =
await treeViewUtilPromise;
treeViewUtil.setSelection(selectedNodeIds);
// Selection and visibility are both applied to the nodes
// above; the tree derives its rows from them here.
treeViewUtil.synchronize();
}
await renderer.resizeAsync();
Expand Down
231 changes: 138 additions & 93 deletions src/ansys/visor/visor-client/src/jest-tests/TreeView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
VisorSceneNodeExtended,
VisorSceneNodeSimple,
} from '../state/VisorSceneGraph.tsx';
import { NullRenderer } from '../renderer/NullRenderer.ts';

describe('TreeView', () => {
it('renders without a scene graph', () => {
Expand Down Expand Up @@ -69,6 +70,42 @@ describe('TreeView', () => {
fireEvent.click(rootRow.hideButton);
expect(rootRow.visible).toBe(false);
});
test('a row seeds its icon from a node that is already hidden', async () => {
const sceneGraph = getLargeSceneGraphWithRenderer();
const meshA = sceneGraph.descendantActorNodesOrSelfDictionaryByPartName['mesh A'];
await meshA.setVisibilityAsync(false);
let util: TreeViewUtil<VisorSceneNodeExtended> = null!;
render(<TreeView sceneGraph={sceneGraph} onLoad={(u) => (util = u)} />);
const meshARow = util.rowUtilsMap.get(meshA.id)!;
// showButton carries the crossed-out eye ("click to show"), so its
// presence in the rendered row is the hidden icon.
expect(meshARow.tableElem.contains(meshARow.showButton)).toBe(true);
expect(meshARow.tableElem.contains(meshARow.hideButton)).toBe(false);
expect(meshARow.visible).toBe(false);
});
test('sibling rows stay visible when one node is hidden', async () => {
const sceneGraph = getLargeSceneGraphWithRenderer();
const meshA = sceneGraph.descendantActorNodesOrSelfDictionaryByPartName['mesh A'];
const meshB = sceneGraph.descendantActorNodesOrSelfDictionaryByPartName['mesh B'];
await meshA.setVisibilityAsync(false);
let util: TreeViewUtil<VisorSceneNodeExtended> = null!;
render(<TreeView sceneGraph={sceneGraph} onLoad={(u) => (util = u)} />);
const meshBRow = util.rowUtilsMap.get(meshB.id)!;
expect(meshBRow.visible).toBe(true);
// The group above them still has a visible descendant, so it stays shown.
expect(util.rows[0].visible).toBe(true);
});
test('a row seeds its selection from a node that is already selected', async () => {
const sceneGraph = getLargeSceneGraphWithRenderer();
const meshC = sceneGraph.descendantActorNodesOrSelfDictionaryByPartName['mesh C'];
await meshC.setSelectedAsync(true);
let util: TreeViewUtil<VisorSceneNodeExtended> = null!;
render(<TreeView sceneGraph={sceneGraph} onLoad={(u) => (util = u)} />);
const meshCRow = util.rowUtilsMap.get(meshC.id)!;
expect(meshCRow.selected).toBe(true);
expect(meshCRow.tableElem.classList.contains('theme-selected-background')).toBe(true);
expect(util.selectedNodes).toContain(meshC);
});
});

function getSmallSceneGraph(): VisorSceneNodeExtended {
Expand Down Expand Up @@ -99,98 +136,106 @@ function getSmallSceneGraph(): VisorSceneNodeExtended {

function getLargeSceneGraph(): VisorSceneNodeExtended {
return CreateVisorSceneGraph(getSceneNodeSimple());
}

function getSceneNodeSimple(): VisorSceneNodeSimple {
return {
id: 0,
dataArrays: [],
name: '',
isGroupNode: true,
isActorNode: false,
nodeType: 'root',
diffuseColor: [1, 1, 1],
bounds: [],
children: [
{
id: 1,
dataArrays: [],
name: 'some-multiblock-file.vtm',
isGroupNode: true,
isActorNode: false,
nodeType: 'vtkMultiBlockDataSet',
diffuseColor: [1, 1, 1],
bounds: [],
children: [
{
id: 2,
dataArrays: [],
name: 'PolyData Container',
isGroupNode: true,
isActorNode: false,
nodeType: 'vtkMultiBlockDataSet',
diffuseColor: [1, 1, 1],
bounds: [],
children: [
{
id: 3,
dataArrays: [],
name: 'mesh A',
isGroupNode: false,
isActorNode: true,
nodeType: 'vtkPolyData',
diffuseColor: [1, 1, 1],
bounds: [],
children: [],
},
{
id: 4,
dataArrays: [],
name: 'mesh B',
isGroupNode: false,
isActorNode: true,
nodeType: 'vtkPolyData',
diffuseColor: [1, 1, 1],
bounds: [],
children: [],
},
],
},
{
id: 5,
dataArrays: [],
name: 'mesh C',
isGroupNode: false,
isActorNode: true,
nodeType: 'vtkUnstructuredGrid',
diffuseColor: [1, 1, 1],
bounds: [],
children: [],
},
{
id: 6,
dataArrays: [],
name: 'mesh D',
isGroupNode: false,
isActorNode: true,
nodeType: 'vtkUnstructuredGrid',
diffuseColor: [1, 1, 1],
bounds: [],
children: [],
},
{
id: 7,
dataArrays: [],
name: 'mesh E',
isGroupNode: false,
isActorNode: true,
nodeType: 'vtkUnstructuredGrid',
diffuseColor: [1, 1, 1],
bounds: [],
children: [],
},
],
},
],
};
}
/**
* The same graph, built with a real no-op renderer so that the node's own
* setVisibilityAsync/setSelectedAsync can run in a test.
*/
function getLargeSceneGraphWithRenderer(): VisorSceneNodeExtended {
return CreateVisorSceneGraph(getSceneNodeSimple(), undefined, new NullRenderer());
}

function getSceneNodeSimple(): VisorSceneNodeSimple {
return {
id: 0,
dataArrays: [],
name: '',
isGroupNode: true,
isActorNode: false,
nodeType: 'root',
diffuseColor: [1, 1, 1],
bounds: [],
children: [
{
id: 1,
dataArrays: [],
name: 'some-multiblock-file.vtm',
isGroupNode: true,
isActorNode: false,
nodeType: 'vtkMultiBlockDataSet',
diffuseColor: [1, 1, 1],
bounds: [],
children: [
{
id: 2,
dataArrays: [],
name: 'PolyData Container',
isGroupNode: true,
isActorNode: false,
nodeType: 'vtkMultiBlockDataSet',
diffuseColor: [1, 1, 1],
bounds: [],
children: [
{
id: 3,
dataArrays: [],
name: 'mesh A',
isGroupNode: false,
isActorNode: true,
nodeType: 'vtkPolyData',
diffuseColor: [1, 1, 1],
bounds: [],
children: [],
},
{
id: 4,
dataArrays: [],
name: 'mesh B',
isGroupNode: false,
isActorNode: true,
nodeType: 'vtkPolyData',
diffuseColor: [1, 1, 1],
bounds: [],
children: [],
},
],
},
{
id: 5,
dataArrays: [],
name: 'mesh C',
isGroupNode: false,
isActorNode: true,
nodeType: 'vtkUnstructuredGrid',
diffuseColor: [1, 1, 1],
bounds: [],
children: [],
},
{
id: 6,
dataArrays: [],
name: 'mesh D',
isGroupNode: false,
isActorNode: true,
nodeType: 'vtkUnstructuredGrid',
diffuseColor: [1, 1, 1],
bounds: [],
children: [],
},
{
id: 7,
dataArrays: [],
name: 'mesh E',
isGroupNode: false,
isActorNode: true,
nodeType: 'vtkUnstructuredGrid',
diffuseColor: [1, 1, 1],
bounds: [],
children: [],
},
],
},
],
};
}
23 changes: 20 additions & 3 deletions src/ansys/visor/visor-client/src/treeview/TreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export interface ITreeViewNode<T extends ITreeViewNode<T>> {
name: string;
/** Whether the node should currently be visible in the underlying scene. */
visible: boolean;
/** Whether the node is currently selected in the underlying scene. */
selected: boolean;
/** Whether the node acts as a group that may contain child rows. */
isGroupNode: boolean;
/** Child nodes rendered beneath this node. */
Expand Down Expand Up @@ -177,6 +179,10 @@ export const TreeView = <T extends ITreeViewNode<T>>(props: TreeViewProps<T>) =>
// IIFE so we don't pollute the outer scope
const rootRow = createRowUtil(sceneGraph, 0, null);
treeContainer.appendChild(rootRow.nodeElem);
// Rows seed themselves from their node, but group rows are derived
// from their descendants, so reconcile once the tree is built.
updateVisibleNodesArray(null);
updateSelectedNodesArray(null);
const searchInput = document.createElement('input');
searchInput.type = 'text';
searchInput.placeholder = 'Search...';
Expand Down Expand Up @@ -297,12 +303,17 @@ export const TreeView = <T extends ITreeViewNode<T>>(props: TreeViewProps<T>) =>
}
}

/** Synchronizes every row's visibility from its source node. */
/** Synchronizes every row's visibility and selection from its source node. */
function synchronize() {
rowUtilsArr.forEach((u) => {
u.node.visible ? u.show() : u.hide();
// The node owns the value; the row derives from it. `=== false`
// and `=== true` keep a node that omits the field on the
// previous defaults (visible, not selected).
u.node.visible === false ? u.hide() : u.show();
u.node.selected === true ? u.selectRow() : u.deselectRow();
});
updateVisibleNodesArray(null);
updateSelectedNodesArray(null);
}

/**
Expand Down Expand Up @@ -615,7 +626,13 @@ export const TreeView = <T extends ITreeViewNode<T>>(props: TreeViewProps<T>) =>
runVisibilityChangeListeners(visibleNodes);
e.stopImmediatePropagation();
};
rowUtil.show();
// Seed the row from the node, which owns the value. Testing for
// `=== false` / `=== true` rather than truthiness keeps a node that
// omits the field on the previous defaults (visible, not selected).
node.visible === false ? rowUtil.hide() : rowUtil.show();
if (node.selected === true) {
rowUtil.selectRow();
}
rowUtilsArr.push(rowUtil);
cells[1].style.paddingLeft = `${fontSize / 4}px`;
depth++;
Expand Down
Loading