Skip to content

Commit

Permalink
Cancel hilite set request on selection change (#354)
Browse files Browse the repository at this point in the history
* Cancel ongoing hilite set request when unified selection changes

* Change

* Clear hilited elements when selection changes

* Cancel ongoing request on dispose

* Update comment

* Cleanup

* Update test to verify ViewportSelectionHandler is disposed

* Add better handling when keys are added/removed to/from unified selection

* Fix zoom to elements

* Change to patch bump
  • Loading branch information
saskliutas authored Nov 29, 2023
1 parent 6d5fb05 commit 4917117
Show file tree
Hide file tree
Showing 7 changed files with 689 additions and 345 deletions.
5 changes: 5 additions & 0 deletions .changeset/dry-scissors-rule.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@itwin/presentation-components": patch
---

Cancel ongoing hilite set request when unified selection changes.
1 change: 1 addition & 0 deletions apps/test-app/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-resize-detector": "^8.1.0",
"rxjs": "^7.8.1",
"sass": "^1.66.1",
"sass-loader": "^13.3.2",
"style-loader": "^3.3.3",
Expand Down
38 changes: 30 additions & 8 deletions apps/test-app/frontend/src/components/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@
import "./App.css";
import "@bentley/icons-generic-webfont/dist/bentley-icons-generic-webfont.css";
import { useEffect, useRef, useState } from "react";
import { from, reduce, Subject, takeUntil } from "rxjs";
import { Id64String } from "@itwin/core-bentley";
import { IModelApp, IModelConnection } from "@itwin/core-frontend";
import { Geometry } from "@itwin/core-geometry";
import { UnitSystemKey } from "@itwin/core-quantity";
import { ElementSeparator, Orientation } from "@itwin/core-react";
import { ThemeProvider, ToggleSwitch } from "@itwin/itwinui-react";
import { SchemaMetadataContextProvider, UnifiedSelectionContextProvider } from "@itwin/presentation-components";
import { Presentation, SelectionChangeEventArgs } from "@itwin/presentation-frontend";
import { HiliteSet, Presentation, SelectionChangeEventArgs } from "@itwin/presentation-frontend";
import { MyAppFrontend, MyAppSettings } from "../../api/MyAppFrontend";
import { IModelSelector } from "../imodel-selector/IModelSelector";
import { PropertiesWidget } from "../properties-widget/PropertiesWidget";
Expand Down Expand Up @@ -65,7 +67,9 @@ export function App() {
};

useEffect(() => {
return Presentation.selection.selectionChange.addListener(async (args: SelectionChangeEventArgs) => {
const cancel = new Subject<void>();
const removeListener = Presentation.selection.selectionChange.addListener(async (args: SelectionChangeEventArgs) => {
cancel.next();
if (!IModelApp.viewManager.selectedView) {
// no viewport to zoom in
return;
Expand All @@ -77,13 +81,31 @@ export function App() {
}

// determine what the viewport is hiliting
const hiliteSet = await Presentation.selection.getHiliteSet(args.imodel);
if (hiliteSet.elements) {
// note: the hilite list may contain models and subcategories as well - we don't
// care about them at this moment
await IModelApp.viewManager.selectedView.zoomToElements(hiliteSet.elements);
}
const selectedView = IModelApp.viewManager.selectedView;
from(Presentation.selection.getHiliteSetIterator(args.imodel))
.pipe(
takeUntil(cancel),
reduce<HiliteSet, { elements: Id64String[] }>(
(acc, curr) => {
// note: the hilite list may contain models and subcategories as well - we don't
// care about them at this moment
acc.elements.push(...(curr.elements ?? []));
return acc;
},
{ elements: [] },
),
)
.subscribe({
next: (set) => {
void selectedView.zoomToElements(set.elements);
},
});
});

return () => {
cancel.next();
removeListener();
};
}, []);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/

import { from, Subject, takeUntil } from "rxjs";
import { IDisposable, using } from "@itwin/core-bentley";
import { IModelConnection } from "@itwin/core-frontend";
import { KeySet, SelectionInfo } from "@itwin/presentation-common";
import { ISelectionProvider, Presentation, SelectionChangeEventArgs, SelectionHandler } from "@itwin/presentation-frontend";
import { AsyncTasksTracker } from "../common/Utils";
import { KeySet } from "@itwin/presentation-common";
import { HiliteSet, HiliteSetProvider, Presentation, SelectionChangeEventArgs, SelectionChangeType, SelectionHandler } from "@itwin/presentation-frontend";

/** @internal */
export interface ViewportSelectionHandlerProps {
Expand All @@ -26,8 +26,7 @@ export interface ViewportSelectionHandlerProps {
export class ViewportSelectionHandler implements IDisposable {
private _imodel: IModelConnection;
private _selectionHandler: SelectionHandler;
private _lastPendingSelectionChange?: { info: SelectionInfo; selection: Readonly<KeySet> };
private _asyncsTracker = new AsyncTasksTracker();
private _cancelOngoingChanges = new Subject<void>();

public constructor(props: ViewportSelectionHandlerProps) {
this._imodel = props.imodel;
Expand All @@ -47,14 +46,11 @@ export class ViewportSelectionHandler implements IDisposable {
}

public dispose() {
this._cancelOngoingChanges.next();
this._selectionHandler.manager.setSyncWithIModelToolSelection(this._imodel, false);
this._selectionHandler.dispose();
}

public get selectionHandler() {
return this._selectionHandler;
}

public get imodel() {
return this._imodel;
}
Expand All @@ -69,54 +65,54 @@ export class ViewportSelectionHandler implements IDisposable {
this._imodel.hilited.wantSyncWithSelectionSet = false;
this._selectionHandler.imodel = value;

void this.applyCurrentSelection();
this.applyCurrentSelection();
}

/** note: used only it tests */
public get pendingAsyncs() {
return this._asyncsTracker.pendingAsyncs;
public applyCurrentSelection() {
this._cancelOngoingChanges.next();
this.applyCurrentHiliteSet(this._imodel);
}

public async applyCurrentSelection() {
await this.applyUnifiedSelection(this._imodel, { providerName: "" }, this.selectionHandler.getSelection());
}
private handleUnifiedSelectionChange(imodel: IModelConnection, changeType: SelectionChangeType, keys: Readonly<KeySet>) {
if (changeType === SelectionChangeType.Clear || changeType === SelectionChangeType.Replace) {
this.applyCurrentHiliteSet(imodel);
return;
}

private async applyUnifiedSelection(imodel: IModelConnection, selectionInfo: SelectionInfo, selection: Readonly<KeySet>) {
if (this._asyncsTracker.pendingAsyncs.size > 0) {
this._lastPendingSelectionChange = { info: selectionInfo, selection };
const hiliteSetProvider = HiliteSetProvider.create({ imodel });
if (changeType === SelectionChangeType.Add) {
from(hiliteSetProvider.getHiliteSetIterator(keys))
.pipe(takeUntil(this._cancelOngoingChanges))
.subscribe({
next: (set) => {
this.applyHiliteSet(imodel, set);
},
});
return;
}

await using(this._asyncsTracker.trackAsyncTask(), async (_r) => {
const ids = await Presentation.selection.getHiliteSet(this._imodel);
using(Presentation.selection.suspendIModelToolSelectionSync(this._imodel), (_) => {
imodel.hilited.clear();
let shouldClearSelectionSet = true;
if (ids.models && ids.models.length) {
imodel.hilited.models.addIds(ids.models);
}
if (ids.subCategories && ids.subCategories.length) {
imodel.hilited.subcategories.addIds(ids.subCategories);
}
if (ids.elements && ids.elements.length) {
imodel.hilited.elements.addIds(ids.elements);
imodel.selectionSet.replace(ids.elements);
shouldClearSelectionSet = false;
}
if (shouldClearSelectionSet) {
imodel.selectionSet.emptyAll();
}
from(hiliteSetProvider.getHiliteSetIterator(keys))
.pipe(takeUntil(this._cancelOngoingChanges))
.subscribe({
next: (set) => {
if (set.models?.length) {
imodel.hilited.models.deleteIds(set.models);
}
if (set.subCategories?.length) {
imodel.hilited.subcategories.deleteIds(set.subCategories);
}
if (set.elements?.length) {
imodel.hilited.elements.deleteIds(set.elements);
imodel.selectionSet.remove(set.elements);
}
},
complete: () => {
this.applyCurrentHiliteSet(imodel, false);
},
});
});

if (this._lastPendingSelectionChange) {
const change = this._lastPendingSelectionChange;
this._lastPendingSelectionChange = undefined;
await this.applyUnifiedSelection(imodel, change.info, change.selection);
}
}

private onUnifiedSelectionChanged = async (args: SelectionChangeEventArgs, provider: ISelectionProvider): Promise<void> => {
private onUnifiedSelectionChanged = (args: SelectionChangeEventArgs) => {
// this component only cares about its own imodel
if (args.imodel !== this._imodel) {
return;
Expand All @@ -128,13 +124,41 @@ export class ViewportSelectionHandler implements IDisposable {
return;
}

const selection = provider.getSelection(args.imodel, 0);
const info: SelectionInfo = {
providerName: args.source,
level: args.level,
};
await this.applyUnifiedSelection(args.imodel, info, selection);
this._cancelOngoingChanges.next();
this.handleUnifiedSelectionChange(args.imodel, args.changeType, args.keys);
};

private applyCurrentHiliteSet(imodel: IModelConnection, clearBefore = true) {
if (clearBefore) {
using(Presentation.selection.suspendIModelToolSelectionSync(this._imodel), (_) => {
imodel.hilited.clear();
imodel.selectionSet.emptyAll();
});
}

from(Presentation.selection.getHiliteSetIterator(imodel))
.pipe(takeUntil(this._cancelOngoingChanges))
.subscribe({
next: (ids) => {
this.applyHiliteSet(imodel, ids);
},
});
}

private applyHiliteSet(imodel: IModelConnection, set: HiliteSet) {
using(Presentation.selection.suspendIModelToolSelectionSync(this._imodel), (_) => {
if (set.models && set.models.length) {
imodel.hilited.models.addIds(set.models);
}
if (set.subCategories && set.subCategories.length) {
imodel.hilited.subcategories.addIds(set.subCategories);
}
if (set.elements && set.elements.length) {
imodel.hilited.elements.addIds(set.elements);
imodel.selectionSet.add(set.elements);
}
});
}
}

let counter = 1;
Original file line number Diff line number Diff line change
Expand Up @@ -34,19 +34,29 @@ export function viewWithUnifiedSelection<P extends ViewportProps>(
const WithUnifiedSelection = memo<CombinedProps>((props) => {
const { selectionHandler, ...restProps } = props;
const imodel = restProps.imodel;
const [viewportSelectionHandler] = useState(() => selectionHandler ?? new ViewportSelectionHandler({ imodel }));
const [viewportSelectionHandler, setViewportSelectionHandler] = useState<ViewportSelectionHandler>();

// apply currentSelection when 'viewportSelectionHandler' is initialized (set to handler from props or new is created)
// 'viewportSelectionHandler' should never change because setter is not used.
useEffect(() => {
void viewportSelectionHandler.applyCurrentSelection();
if (selectionHandler) {
selectionHandler.applyCurrentSelection();
setViewportSelectionHandler(selectionHandler);
return;
}

const handler = new ViewportSelectionHandler({ imodel });
handler.applyCurrentSelection();
setViewportSelectionHandler(handler);
return () => {
viewportSelectionHandler.dispose();
handler.dispose();
};
}, [viewportSelectionHandler]);
}, [selectionHandler, imodel]);

// set new imodel on 'viewportSelectionHandler' when it changes
useEffect(() => {
if (!viewportSelectionHandler) {
return;
}
viewportSelectionHandler.imodel = imodel;
}, [viewportSelectionHandler, imodel]);

Expand Down
Loading

0 comments on commit 4917117

Please sign in to comment.