Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance applySavedView API #99

Merged
merged 2 commits into from
Nov 14, 2024
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
3 changes: 1 addition & 2 deletions packages/saved-views-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ This package hosts client-side code and TypeScript types for interacting with [i
import { ITwinSavedViewsClient } from "@itwin/saved-views-client";

const client = new ITwinSavedViewsClient({
// auth_token should have access to savedviews:read and savedviews:modify OIDC scopes
getAccessToken: async () => "<auth_token>",
getAccessToken: async () => "<itwin_platform_auth_token>",
});

const { savedView } = await client.getSavedViewMinimal({ savedViewId: "<saved_view_id>" });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ export interface ITwinSavedViewsClientParams {
/** @default "https://api.bentley.com/savedviews" */
baseUrl?: string;

/**
* Authorization token that grants access to iTwin Saved Views API. The token should be valid for `savedviews:read`
* and `savedviews:modify` OIDC scopes.
*/
/** Authorization token that grants access to iTwin Platform API. */
getAccessToken: () => Promise<string>;
}

Expand Down
10 changes: 10 additions & 0 deletions packages/saved-views-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
* Move `viewData` and `extensions` properties to `SavedViewData` type
* Type of `viewData` has changed and definition has moved to `@itwin/saved-views-react`
* Change type of `creationTime` and `lastModified` properties to `Date | undefined`
* Make `applySavedView` settings easier to understand
* Remove `"reset"` from `ApplyStrategy` union, instead make `"clear"` a valid value for `emphasis` and `perModelCategoryVisibility` properties
* Remove `all` property which set default `ApplyStrategy` of all settings
* Update documentation
* `useSavedViews` hook rework
* No longer implements optimistic behaviour
* Lazily loads Saved View thumbnails and `SavedViewData`
Expand Down Expand Up @@ -43,6 +47,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
* Thumbnails that were specified as image URLs now need to be explicitly passed as `<img src={url} />`
* `onRename` callback will no longer send `undefined` for `newName` argument

### Minor changes

* `applySavedView` enhancements
* Accept custom `viewChangeOptions` that the function will internally pass through to `viewport.changeView` call
* Add `camera` setting that controls how camera data is applied. Allow supplying a custom `ViewPose` or ignoring Saved View data to keep the camera in place.

### Fixes

* Fix `captureSavedViewData` failing with blank iModel connections
Expand Down
25 changes: 5 additions & 20 deletions packages/saved-views-react/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,22 @@ A collection of utilities and React components for building iTwin applications t

### [captureSavedViewData](./src/captureSavedViewData.ts)

Captures current viewport state into serializable format. The returned data can later be used to restore viewport's view.
Capture current viewport state into serializable format. You can use this data later to restore the view.

```ts
const { viewData, extensions = [] } = await captureSavedViewData({ viewport });
extensions.push(myCustomExtension(viewport));
const { viewData, extensions } = await captureSavedViewData({ viewport });
console.log({ viewData, extensions }); /*
{
viewData: { itwin3dView: {...} },
extensions: {
{ extensionName: "EmphasizeElements", data: "{...}" },
{ extensionName: "MyCustomExtension", data: "my_custom_extension_data" },
}
} */
```

### [captureSavedViewThumbnail](./src/captureSavedViewThumbnail.ts)

Generates Saved View thumbnail based on what is currently displayed on the viewport.
Generate Saved View thumbnail based on what is currently displayed on the viewport.

```ts
const thumbnail = captureSavedViewThumbnail(viewport);
Expand All @@ -34,7 +32,7 @@ console.log(thumbnail); // "data:image/png;base64,iVBORw0KGoAAAANSUhEUg..."

### [applySavedView](./src/applySavedView.ts)

Updates viewport state to match captured Saved View.
Update viewport state to match captured Saved View.

```ts
// Capture viewport state
Expand All @@ -43,18 +41,6 @@ const savedViewData = await captureSavedViewData({ viewport });
await applySavedView(iModel, viewport, savedViewData);
```

### [createViewState](./src/createViewState.ts)

Creates ViewState object out of Saved View data. It provides a lower-level access to view data for advanced use.

```ts
const viewState = await createViewState(iModel, savedViewData.viewData);
await applySavedView(iModel, viewport, savedViewData, { viewState });

// The two lines above are equivalent to
await applySavedView(iModel, viewport, savedViewData);
```

### React components

* [SavedViewTile](./src/SavedViewTile/SavedViewTile.tsx)
Expand Down Expand Up @@ -99,8 +85,7 @@ export function SavedViewsWidget(props) {
import { useSavedViews, ITwinSavedViewsClient } from "@itwin/saved-views-react";

const client = new ITwinSavedViewsClient({
// auth_token should have access to savedviews:read and savedviews:modify OIDC scopes
getAccessToken: async () => "auth_token",
getAccessToken: async () => "itwin_platform_auth_token",
});

export function SavedViewsWidget(props) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ import type {
} from "./SavedViewsClient.js";

interface ITwinSavedViewsClientParams {
/**
* Authorization token that grants access to iTwin Saved Views API. The token should be valid for `savedviews:read`
* and `savedviews:modify` OIDC scopes.
*/
/** Authorization token that grants access to iTwin Platform API. */
getAccessToken: () => Promise<string>;

/** @default "https://api.bentley.com/savedviews" */
Expand Down
165 changes: 113 additions & 52 deletions packages/saved-views-react/src/applySavedView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,21 @@
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import { ViewState, type IModelConnection, type Viewport } from "@itwin/core-frontend";
import { ViewChangeOptions, ViewPose, ViewState, type IModelConnection, type Viewport } from "@itwin/core-frontend";

import type { SavedViewData } from "./SavedView.js";
import type { SavedViewData, SavedViewExtension } from "./SavedView.js";
import { createViewState } from "./createViewState.js";
import { extensionHandlers, type ExtensionHandler } from "./translation/SavedViewsExtensionHandlers.js";
import { extensionHandlers } from "./translation/SavedViewsExtensionHandlers.js";

export interface ApplySavedViewSettings {
/**
* Strategy to use when other setting does not specify one. By default, viewport is reset to default state and then
* all captured Saved View data is applied on top.
* @default "apply"
* How to make use of captured {@link ViewState} data. The default behavior is
* to generate a new `ViewState` object out of {@linkcode SavedViewData.viewData}
* and apply it to viewport.
*
* @example
* await applySavedView(iModel, viewport, savedView, { all: "keep", viewState: "apply" });
*/
all?: ApplyStrategy | undefined;

/**
* How to handle captured {@link ViewState} data. The default behavior is to generate a new `ViewState` object out of
* {@linkcode SavedViewData.viewData} and apply it to viewport.
* You can optionally provide a pre-made `ViewState` instance.
*
* You can optionally provide a pre-made `ViewState` instance to conserve resources. It is usually obtained from
* {@link createViewState} result.
* @default "apply"
*
* @example
* import { applySavedView, createViewState } from "@itwin/saved-views-react";
Expand All @@ -34,36 +26,47 @@ export interface ApplySavedViewSettings {
* applySavedView(iModel, viewport1, savedView, { viewState }),
* applySavedView(iModel, viewport2, savedView, { viewState }),
* ]);
*
* @remarks
* When neither `SavedView.viewData` nor `ViewState` is provided, current {@linkcode Viewport.view} is preserved.
*/
viewState?: ApplyStrategy | ViewState | undefined;

/**
* How to handle visibility of models and categories that exist in iModel but are not captured in Saved View data. Has
* effect only when `modelAndCategoryVisibility` strategy is set to `"apply"`.
* How to handle camera pose in captured data.
* @default "apply"
*/
camera?: ApplyStrategy | ViewPose | undefined;

/**
* How to handle element emphasis in captured data.
* @default "apply"
*/
emphasis?: ApplyStrategy | "clear" | undefined;

/**
* How to handle captured {@link Viewport.perModelCategoryVisibility} data.
* @default "apply"
*/
perModelCategoryVisibility?: ApplyStrategy | "clear" | undefined;

/**
* How to handle visibility of models and categories that exist in iModel but
* are not captured in Saved View data.
* @default "hidden"
*/
modelAndCategoryVisibilityFallback?: "visible" | "hidden" | undefined;

/** How to handle captured element emphasis data. In default state emphasis is turned off. */
emphasis?: ApplyStrategy | undefined;

/**
* How to handle captured {@link Viewport.perModelCategoryVisibility} data. In default state no overrides are present.
* Options forwarded to {@link Viewport.changeView}.
* @default undefined
*/
perModelCategoryVisibility?: ApplyStrategy | undefined;
viewChangeOptions?: ViewChangeOptions | undefined;
}

/**
* Controls how viewport state is going to be altered.
*
* * `"apply"` – Apply captured Saved View state. Falls back to `"reset"` on failure (e.g. missing Saved View data).
* * `"reset"` – Reset to the default viewport state
* * `"keep"` – Keep the current viewport state
* * `"apply"` – Apply captured Saved View state to viewport
* * `"keep"` – Preserve current viewport state
*/
type ApplyStrategy = "apply" | "reset" | "keep";
type ApplyStrategy = "apply" | "keep";

/**
* Updates {@linkcode viewport} state to match captured Saved View.
Expand All @@ -83,33 +86,91 @@ export async function applySavedView(
savedViewData: SavedViewData,
settings: ApplySavedViewSettings | undefined = {},
): Promise<void> {
const defaultStrategy = settings.all ?? "apply";

if ((settings.viewState ?? defaultStrategy) !== "keep") {
if (settings.viewState instanceof ViewState) {
viewport.changeView(settings.viewState);
} else if (savedViewData.viewData) {
const { modelAndCategoryVisibilityFallback } = settings;
const viewState = await createViewState(iModel, savedViewData.viewData, { modelAndCategoryVisibilityFallback });
viewport.changeView(viewState);
if (settings.viewState !== "keep") {
// We use "hidden" as the default value for modelAndCategoryVisibilityFallback
// because users expect modelSelector.enabled and categorySelector.enabled to
// act as exclusive whitelists when modelSelector.disabled or categorySelector.disabled
// arrays are empty, respectively.
const { modelAndCategoryVisibilityFallback = "hidden" } = settings;
const viewState = settings.viewState instanceof ViewState
? settings.viewState
: await createViewState(
iModel,
savedViewData.viewData,
{ modelAndCategoryVisibilityFallback },
);

if (settings.camera instanceof ViewPose) {
viewState.applyPose(settings.camera);
} else if (settings.camera === "keep") {
viewState.applyPose(viewport.view.savePose());
}

viewport.changeView(viewState, settings.viewChangeOptions);
}

const extensions = new Map(savedViewData.extensions?.map(({ extensionName, data }) => [extensionName, data]));
const processExtension = (extensionHandler: ExtensionHandler, strategy: ApplyStrategy = defaultStrategy) => {
if (strategy === "keep") {
return;
const extensions = findKnownExtensions(savedViewData.extensions ?? []);
if (extensions.emphasis) {
if (settings.emphasis !== "keep") {
extensionHandlers.emphasizeElements.reset(viewport);
}

extensionHandler.reset(viewport);
if (strategy === "apply") {
const extensionData = extensions.get(extensionHandler.extensionName);
if (extensionData) {
extensionHandler.apply(extensionData, viewport);
}
if (settings.emphasis === "apply") {
extensionHandlers.emphasizeElements.apply(extensions.emphasis, viewport);
}
}

if (extensions.perModelCategoryVisibility) {
if (settings.perModelCategoryVisibility !== "keep") {
extensionHandlers.perModelCategoryVisibility.reset(viewport);
}

if (settings.perModelCategoryVisibility === "apply") {
extensionHandlers.perModelCategoryVisibility.apply(
extensions.perModelCategoryVisibility,
viewport,
);
}
}
}

interface FindKnownExtensionsResult {
emphasis: string | undefined;
perModelCategoryVisibility: string | undefined;
}

/** Finds first occurences of known extensions. */
function findKnownExtensions(extensions: SavedViewExtension[]): FindKnownExtensionsResult {
const result: FindKnownExtensionsResult = {
emphasis: undefined,
perModelCategoryVisibility: undefined,
};

processExtension(extensionHandlers.emphasizeElements, settings.emphasis);
processExtension(extensionHandlers.perModelCategoryVisibility, settings.perModelCategoryVisibility);
for (const extension of extensions) {
if (
result.emphasis === undefined &&
extension.extensionName === extensionHandlers.emphasizeElements.extensionName
) {
result.emphasis = extension.data;
if (result.perModelCategoryVisibility) {
break;
}
}

if (
result.perModelCategoryVisibility === undefined &&
extension.extensionName === extensionHandlers.perModelCategoryVisibility.extensionName
) {
result.perModelCategoryVisibility = extension.data;
if (result.emphasis) {
break;
}
}

if (result.emphasis && result.perModelCategoryVisibility) {
break;
}
}

return result;
}
Loading
Loading