From 6d393c4c19bcd9cf3f3f6aad71f19d41f7291ff4 Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Thu, 12 May 2022 15:21:51 -0400 Subject: [PATCH 01/16] multi-viewport sync. --- core/frontend/src/TwoWayViewportSync.ts | 46 ++++++++++++++++++------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/core/frontend/src/TwoWayViewportSync.ts b/core/frontend/src/TwoWayViewportSync.ts index 804066e1e53a..1b8a8eb3b77e 100644 --- a/core/frontend/src/TwoWayViewportSync.ts +++ b/core/frontend/src/TwoWayViewportSync.ts @@ -8,6 +8,38 @@ import { Viewport } from "./Viewport"; +export type SynchronizeViewports = (source: Viewport, target: Viewport) => void; + +export function connectViewports(viewports: Iterable, sync: (source: Viewport) => SynchronizeViewports): () => void { + const disconnect: VoidFunction[] = []; + + let echo = false; + const synchronize = (source: Viewport) => { + if (echo) + return; + + echo = true; + try { + const doSync = sync(source); + for (const vp of viewports) + if (vp !== source) + doSync(source, vp); + } finally { + echo = false; + } + }; + + for (const vp of viewports) + disconnect.push(vp.onViewChanged.addListener(() => synchronize(vp))); + + return () => { + for (const f of disconnect) + f(); + + disconnect.length = 0; + }; +} + /** Forms a bidirectional connection between two [[Viewport]]s such that the [[ViewState]]s of each are synchronized with one another. * For example, panning in one viewport will cause the other viewport to pan by the same distance, and changing the [RenderMode]($common) of one viewport * will change it in the other viewport. @@ -20,16 +52,6 @@ import { Viewport } from "./Viewport"; */ export class TwoWayViewportSync { protected readonly _disconnect: VoidFunction[] = []; - private _isEcho = false; - - private syncView(source: Viewport, target: Viewport) { - if (this._isEcho) - return; - - this._isEcho = true; // so we don't react to the echo of this sync - this.syncViewports(source, target); - this._isEcho = false; - } /** Invoked from [[connect]] to set up the initial synchronization between the two viewports. * `target` should be modified to match `source`. @@ -59,9 +81,7 @@ export class TwoWayViewportSync { this.connectViewports(viewport1, viewport2); - // listen to the onViewChanged events from both views - this._disconnect.push(viewport1.onViewChanged.addListener(() => this.syncView(viewport1, viewport2))); - this._disconnect.push(viewport2.onViewChanged.addListener(() => this.syncView(viewport2, viewport1))); + this._disconnect.push(connectViewports([viewport1, viewport2], () => (source, target) => this.syncViewports(source, target))); } /** Remove the connection between the two views. */ From 392dc96ef53b1f77e064d0c59a5d6551960c1579 Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Thu, 12 May 2022 17:17:20 -0400 Subject: [PATCH 02/16] multi sync --- core/frontend/src/TwoWayViewportSync.ts | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/frontend/src/TwoWayViewportSync.ts b/core/frontend/src/TwoWayViewportSync.ts index 1b8a8eb3b77e..ad903b84e13e 100644 --- a/core/frontend/src/TwoWayViewportSync.ts +++ b/core/frontend/src/TwoWayViewportSync.ts @@ -40,6 +40,26 @@ export function connectViewports(viewports: Iterable, sync: (source: V }; } +export function synchronizeViewportViews(source: Viewport): SynchronizeViewports { + return (_source, target) => target.applyViewState(source.view.clone(target.iModel)); +} + +export function synchronizeViewportFrusta(source: Viewport): SynchronizeViewports { + const pose = source.view.savePose(); + return (_source, target) => { + const view = target.view.applyPose(pose); + target.applyViewState(view); + }; +} + +export function connectViewportFrusta(viewports: Iterable): () => void { + return connectViewports(viewports, (source) => synchronizeViewportFrusta(source)); +} + +export function connectViewportViews(viewports: Iterable): () => void { + return connectViewports(viewports, (source) => synchronizeViewportViews(source)); +} + /** Forms a bidirectional connection between two [[Viewport]]s such that the [[ViewState]]s of each are synchronized with one another. * For example, panning in one viewport will cause the other viewport to pan by the same distance, and changing the [RenderMode]($common) of one viewport * will change it in the other viewport. From 909827e5667d0f9090fa2160b3a4081dd887b53e Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Thu, 12 May 2022 20:06:43 -0400 Subject: [PATCH 03/16] wip tests. --- core/frontend/src/TwoWayViewportSync.ts | 10 +- .../src/test/TwoWayViewportSync.test.ts | 95 ++++++++++++++++--- 2 files changed, 93 insertions(+), 12 deletions(-) diff --git a/core/frontend/src/TwoWayViewportSync.ts b/core/frontend/src/TwoWayViewportSync.ts index ad903b84e13e..5f545dc2af05 100644 --- a/core/frontend/src/TwoWayViewportSync.ts +++ b/core/frontend/src/TwoWayViewportSync.ts @@ -29,8 +29,16 @@ export function connectViewports(viewports: Iterable, sync: (source: V } }; - for (const vp of viewports) + let firstViewport: Viewport | undefined; + for (const vp of viewports) { + if (!firstViewport) + firstViewport = vp; + disconnect.push(vp.onViewChanged.addListener(() => synchronize(vp))); + } + + if (firstViewport) + synchronize(firstViewport); return () => { for (const f of disconnect) diff --git a/core/frontend/src/test/TwoWayViewportSync.test.ts b/core/frontend/src/test/TwoWayViewportSync.test.ts index c64d439f538d..a894a463bfc9 100644 --- a/core/frontend/src/test/TwoWayViewportSync.test.ts +++ b/core/frontend/src/test/TwoWayViewportSync.test.ts @@ -7,13 +7,20 @@ import { expect } from "chai"; import { ViewFlags } from "@itwin/core-common"; import { IModelApp } from "../IModelApp"; import { StandardViewId } from "../StandardView"; -import { ScreenViewport } from "../Viewport"; -import { TwoWayViewportFrustumSync, TwoWayViewportSync } from "../TwoWayViewportSync"; +import { Viewport } from "../Viewport"; +import { + connectViewports, SynchronizeViewports, synchronizeViewportFrusta, synchronizeViewportViews, TwoWayViewportFrustumSync, TwoWayViewportSync, +} from "../TwoWayViewportSync"; import { openBlankViewport } from "./openBlankViewport"; -describe("TwoWayViewportSync", () => { - let vp1: ScreenViewport; - let vp2: ScreenViewport; +function rotate(vp: Viewport, id: StandardViewId) { + vp.view.setStandardRotation(id); + vp.synchWithView(); +} + +describe.only("TwoWayViewportSync", () => { + let vp1: Viewport; + let vp2: Viewport; before(async () => IModelApp.startup()); after(async () => IModelApp.shutdown()); @@ -32,11 +39,6 @@ describe("TwoWayViewportSync", () => { return vp1.getFrustum().isSame(vp2.getFrustum()); } - function rotate(vp: ScreenViewport, id: StandardViewId) { - vp.view.setStandardRotation(id); - vp.synchWithView(); - } - it("synchronizes frusta", () => { for (const type of [TwoWayViewportSync, TwoWayViewportFrustumSync]) { rotate(vp1, StandardViewId.Top); @@ -114,7 +116,7 @@ describe("TwoWayViewportSync", () => { }); it("synchronizes selectors", () => { - function categories(vp: ScreenViewport): string[] { + function categories(vp: Viewport): string[] { return Array.from(vp.view.categorySelector.categories).sort(); } @@ -172,3 +174,74 @@ describe("TwoWayViewportSync", () => { } }); }); + +describe.only("connectViewports", () => { + const nVps = 4; + let vps: Viewport[] = []; + + before(async () => IModelApp.startup()); + after(async () => IModelApp.shutdown()); + + beforeEach(() => { + for (let i = 0; i < nVps; i++) + vps.push(openBlankViewport()); + }); + + afterEach(() => { + for (const vp of vps) + vp.dispose(); + + vps.length = 0; + }); + + function getTargets(source: Viewport) { + return vps.filter((x) => x !== source); + } + + function sameFrusta(source: Viewport) { + return getTargets(source).every((x) => x.getFrustum().isSame(source.getFrustum())); + } + + function allSameFrustum() { + const allSame = vps.every((x) => sameFrusta(x)); + const anySame = vps.some((x) => sameFrusta(x)); + expect(allSame).to.equal(anySame); + return allSame; + } + + function connectFrusta() { + return connectViewports(vps, synchronizeViewportFrusta); + } + + function connectViews() { + return connectViewports(vps, synchronizeViewportViews); + } + + it("synchronizes frusta", () => { + for (const connect of [connectFrusta, connectViews]) { + rotate(vps[0], StandardViewId.Top); + rotate(vps[1], StandardViewId.Bottom); + rotate(vps[2], StandardViewId.Left); + rotate(vps[3], StandardViewId.Right); + + expect(allSameFrustum()).to.be.false; + + const disconnect = connect(); + expect(allSameFrustum()).to.be.true; + + disconnect(); + } + }); + + it("synchronizes camera", () => { + }); + + it("synchronizes display style", () => { + }); + + it("synchronizes selectors", () => { + }); + + it("disconnects", () => { + }); +}); From 58c0f94f21fb79876ecd914382edfe7faada8cee Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Thu, 12 May 2022 20:22:37 -0400 Subject: [PATCH 04/16] wip more tests. --- .../src/test/TwoWayViewportSync.test.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/core/frontend/src/test/TwoWayViewportSync.test.ts b/core/frontend/src/test/TwoWayViewportSync.test.ts index a894a463bfc9..2dece705fdf0 100644 --- a/core/frontend/src/test/TwoWayViewportSync.test.ts +++ b/core/frontend/src/test/TwoWayViewportSync.test.ts @@ -229,14 +229,63 @@ describe.only("connectViewports", () => { const disconnect = connect(); expect(allSameFrustum()).to.be.true; + let prevFrust = vps[2].getFrustum(); + rotate(vps[2], StandardViewId.Iso); + expect(prevFrust.isSame(vps[2].getFrustum())).to.be.false; + expect(allSameFrustum()).to.be.true; + disconnect(); } }); + it("synchronizes initially to the first viewport", () => { + }); + it("synchronizes camera", () => { + for (const connect of [connectFrusta, connectViews]) { + expect(vps.every((x) => x.isCameraOn)).to.be.false; + + vps[1].turnCameraOn(); + expect(vps.every((x) => x.isCameraOn === (x === vps[1]))).to.be.true; + + const disconnect = connect(); + expect(vps.every((x) => !x.isCameraOn)).to.be.true; // because the first viewport is the one we initially sync the others to. + expect(allSameFrustum()).to.be.true; + + vps[2].turnCameraOn(); + expect(vps.every((x) => x.isCameraOn)).to.be.true; + expect(allSameFrustum()).to.be.true; + + vps[3].turnCameraOff(); + expect(vps.every((x) => x.isCameraOn)).to.be.false; + expect(allSameFrustum()).to.be.true; + + disconnect(); + } }); it("synchronizes display style", () => { + function test(connect: () => VoidFunction, expectSync: boolean) { + vps[0].viewFlags = new ViewFlags(); + vps[3].viewFlags = new ViewFlags(); + expect(vps[0].viewFlags.grid).to.be.false; + vps[0].viewFlags = vps[0].viewFlags.with("grid", true); + expect(vps[3].viewFlags.acsTriad).to.be.false; + vps[3].viewFlags = vps[3].viewFlags.with("acsTriad", true); + + const disconnect = connect(); + expect(vps.every((x) => x.viewFlags.grid)).to.equal(expectSync); + expect(vps.some((x) => x.viewFlags.acsTriad)).not.to.equal(expectSync); + + vps[1].viewFlags = vps[1].viewFlags.with("acsTriad", true); + vps[1].synchWithView(); + expect(vps.every((x) => x.viewFlags.acsTriad)).to.equal(expectSync); + + disconnect(); + } + + test(connectViews, true); + test(connectFrusta, false); }); it("synchronizes selectors", () => { From 7e767f438647b6f8b7ed2fd119b11f95f35cda7e Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Fri, 13 May 2022 06:47:14 -0400 Subject: [PATCH 05/16] finish tests. --- .../src/test/TwoWayViewportSync.test.ts | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/core/frontend/src/test/TwoWayViewportSync.test.ts b/core/frontend/src/test/TwoWayViewportSync.test.ts index 2dece705fdf0..e6e72616d195 100644 --- a/core/frontend/src/test/TwoWayViewportSync.test.ts +++ b/core/frontend/src/test/TwoWayViewportSync.test.ts @@ -18,7 +18,7 @@ function rotate(vp: Viewport, id: StandardViewId) { vp.synchWithView(); } -describe.only("TwoWayViewportSync", () => { +describe("TwoWayViewportSync", () => { let vp1: Viewport; let vp2: Viewport; @@ -175,7 +175,7 @@ describe.only("TwoWayViewportSync", () => { }); }); -describe.only("connectViewports", () => { +describe("connectViewports", () => { const nVps = 4; let vps: Viewport[] = []; @@ -217,13 +217,16 @@ describe.only("connectViewports", () => { return connectViewports(vps, synchronizeViewportViews); } + function makeUniqueFrusta() { + rotate(vps[0], StandardViewId.Top); + rotate(vps[1], StandardViewId.Bottom); + rotate(vps[2], StandardViewId.Left); + rotate(vps[3], StandardViewId.Right); + } + it("synchronizes frusta", () => { for (const connect of [connectFrusta, connectViews]) { - rotate(vps[0], StandardViewId.Top); - rotate(vps[1], StandardViewId.Bottom); - rotate(vps[2], StandardViewId.Left); - rotate(vps[3], StandardViewId.Right); - + makeUniqueFrusta(); expect(allSameFrustum()).to.be.false; const disconnect = connect(); @@ -239,6 +242,22 @@ describe.only("connectViewports", () => { }); it("synchronizes initially to the first viewport", () => { + for (const connect of [connectFrusta, connectViews]) { + const test = (reorder: () => void) => { + makeUniqueFrusta(); + reorder(); + + const frust = vps[0].getFrustum(); + expect(vps.every((x) => x.getFrustum().isSame(frust) === (x === vps[0]))).to.be.true; + + const disconnect = connect(); + expect(vps.every((x) => x.getFrustum().isSame(frust))).to.be.true; + disconnect(); + }; + + test(() => undefined); + test(() => { vps.reverse(); }); + } }); it("synchronizes camera", () => { @@ -288,9 +307,15 @@ describe.only("connectViewports", () => { test(connectFrusta, false); }); - it("synchronizes selectors", () => { - }); - it("disconnects", () => { + for (const connect of [connectFrusta, connectViews]) { + makeUniqueFrusta(); + const disconnect = connect(); + expect(allSameFrustum()).to.be.true; + + disconnect(); + makeUniqueFrusta(); + expect(allSameFrustum()).to.be.false; + } }); }); From f288d5ac5801b2061388bce587b985924b3d6d56 Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Fri, 13 May 2022 06:49:22 -0400 Subject: [PATCH 06/16] rename files. --- core/frontend/src/{TwoWayViewportSync.ts => ViewportSync.ts} | 0 core/frontend/src/core-frontend.ts | 2 +- .../test/{TwoWayViewportSync.test.ts => ViewportSync.test.ts} | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename core/frontend/src/{TwoWayViewportSync.ts => ViewportSync.ts} (100%) rename core/frontend/src/test/{TwoWayViewportSync.test.ts => ViewportSync.test.ts} (99%) diff --git a/core/frontend/src/TwoWayViewportSync.ts b/core/frontend/src/ViewportSync.ts similarity index 100% rename from core/frontend/src/TwoWayViewportSync.ts rename to core/frontend/src/ViewportSync.ts diff --git a/core/frontend/src/core-frontend.ts b/core/frontend/src/core-frontend.ts index 56236149408b..649a741a1fac 100644 --- a/core/frontend/src/core-frontend.ts +++ b/core/frontend/src/core-frontend.ts @@ -58,7 +58,6 @@ export * from "./StandardView"; export * from "./SubCategoriesCache"; export * from "./TentativePoint"; export * from "./Tiles"; -export * from "./TwoWayViewportSync"; export * from "./UserPreferences"; export * from "./ViewAnimation"; export * from "./ViewContext"; @@ -66,6 +65,7 @@ export * from "./ViewGlobalLocation"; export * from "./ViewingSpace"; export * from "./ViewManager"; export * from "./Viewport"; +export * from "./ViewportSync"; export * from "./ViewPose"; export * from "./ViewRect"; export * from "./ViewState"; diff --git a/core/frontend/src/test/TwoWayViewportSync.test.ts b/core/frontend/src/test/ViewportSync.test.ts similarity index 99% rename from core/frontend/src/test/TwoWayViewportSync.test.ts rename to core/frontend/src/test/ViewportSync.test.ts index e6e72616d195..af5ee41b123a 100644 --- a/core/frontend/src/test/TwoWayViewportSync.test.ts +++ b/core/frontend/src/test/ViewportSync.test.ts @@ -10,7 +10,7 @@ import { StandardViewId } from "../StandardView"; import { Viewport } from "../Viewport"; import { connectViewports, SynchronizeViewports, synchronizeViewportFrusta, synchronizeViewportViews, TwoWayViewportFrustumSync, TwoWayViewportSync, -} from "../TwoWayViewportSync"; +} from "../ViewportSync"; import { openBlankViewport } from "./openBlankViewport"; function rotate(vp: Viewport, id: StandardViewId) { From 5a336c8740a6df584c0b284225d1ab17b9971083 Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Fri, 13 May 2022 07:17:58 -0400 Subject: [PATCH 07/16] docs --- core/frontend/src/ViewportSync.ts | 57 +++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/core/frontend/src/ViewportSync.ts b/core/frontend/src/ViewportSync.ts index 5f545dc2af05..945caaaaf67a 100644 --- a/core/frontend/src/ViewportSync.ts +++ b/core/frontend/src/ViewportSync.ts @@ -8,8 +8,42 @@ import { Viewport } from "./Viewport"; +/** A function used by [[connectViewports]] to obtain another function that can synchronize each target [[Viewport]] with + * changes in the state of a source Viewport. + * The source viewport is the viewport in the connection whose state has changed. + * The function will be invoked once for each target viewport in the connection. + * @public + */ export type SynchronizeViewports = (source: Viewport, target: Viewport) => void; +/** Forms a connection between two or more [[Viewport]]s such that a change in any one of the viewports is reflected in all of the others. + * When the connection is first formed, all of the viewports are synchronized to the current state of the **first** viewport in `viewports`. + * Thereafter, an event listener registered with each viewport's [[Viewport.onViewChanged]] event is invoked when anything about that viewport's state changes. + * Each time such an event occurs, the initating ("source") viewport is passed to `sync` to obtain a function that can be invoked to synchronize each of the other + * ("target") viewports with the source viewport's new state. The function returned by `sync` can choose to synchronize any or all aspects of the viewports' states, such as + * the viewed volume, display style, viewed categories or models, or anything else. + * + * To sever the connection, invoke the function returned by this function. For example: + * ```ts + * // set up the connection. + * const disconnect = connectViewports([viewport0, viewport1, viewport2], (source) => synchronizeViewportFrusta(source)); + * // some time later, sever the connection. + * disconnect(); + * ``` + * + * @note [[Viewport.onViewChanged]] can be invoked **very** frequently - sometimes multiple times per frame. Try to avoid performing excessive computations within your synchronization functions. + * + * @param viewports The viewports to be connected. It should contain at least two viewports and no duplicate viewports. The initial state of each viewport will be synchronized with + * the state of the first viewport in this iterable. + * @param sync A function to be invoked whenever the state of any viewport in `viewports` changes, returning a function that can be used to synchronize the + * state of each viewport. + * @returns a function that can be invoked to sever the connection between the viewports. + * @see [[connectViewportFrusta]] to synchronize the [Frustum]($common) of each viewport. + * @see [[connectViewportViews]] to synchronize every aspect of the viewports. + * @see [[TwoWayViewportSync]] to synchronize the state of exactly two viewports. + * @see [Multiple Viewport Sample](https://www.itwinjs.org/sample-showcase/?group=Viewer+Features&sample=multi-viewport-sample&imodel=Metrostation+Sample) + * @public + */ export function connectViewports(viewports: Iterable, sync: (source: Viewport) => SynchronizeViewports): () => void { const disconnect: VoidFunction[] = []; @@ -18,6 +52,7 @@ export function connectViewports(viewports: Iterable, sync: (source: V if (echo) return; + // Ignore onViewChanged events resulting from synchronization. echo = true; try { const doSync = sync(source); @@ -48,10 +83,19 @@ export function connectViewports(viewports: Iterable, sync: (source: V }; } +/** A function that returns a [[SynchronizeViewports]] function that synchronizes every aspect of the viewports' states, including + * display style, model and category selectors, [Frustum]($common), etc. + * @see [[connectViewportViews]] to establish a connection between viewports using this synchronization strategy. + * @public + */ export function synchronizeViewportViews(source: Viewport): SynchronizeViewports { return (_source, target) => target.applyViewState(source.view.clone(target.iModel)); } +/** A function that returns a [[SynchronizeViewports]] function that synchronizes the viewed volumes of each viewport. + * @see [[connectViewportFrusta]] to establish a connection between viewports using this synchronization strategy. + * @public + */ export function synchronizeViewportFrusta(source: Viewport): SynchronizeViewports { const pose = source.view.savePose(); return (_source, target) => { @@ -60,10 +104,21 @@ export function synchronizeViewportFrusta(source: Viewport): SynchronizeViewport }; } +/** Form a connection between two or more [[Viewport]]s such that they all view the same volume. For example, zooming out in one viewport + * will zoom out by the same distance in all of the other viewports. + * @see [[connectViewports]] to customize how the viewports are synchronized. + * @public + */ export function connectViewportFrusta(viewports: Iterable): () => void { return connectViewports(viewports, (source) => synchronizeViewportFrusta(source)); } +/** Form a connection between two or more [[Viewport]]s such that every aspect of the viewports are kept in sync. For example, if the set of models + * or categories visible in one viewport is changed, the same set of models and categories will be visible in the other viewports. + * @see [[connectViewportFrusta]] to synchronize only the [Frustum]($common) of each viewport. + * @see [[connectViewports]] to customize how the viewports are synchronized. + * @public + */ export function connectViewportViews(viewports: Iterable): () => void { return connectViewports(viewports, (source) => synchronizeViewportViews(source)); } @@ -76,6 +131,7 @@ export function connectViewportViews(viewports: Iterable): () => void * @see [Multiple Viewport Sample](https://www.itwinjs.org/sample-showcase/?group=Viewer+Features&sample=multi-viewport-sample&imodel=Metrostation+Sample) * for an interactive demonstration. * @see [[TwoWayViewportFrustumSync]] to synchronize only the frusta of the viewports. + * @see [[connectViewportViews]] to synchronize the state of more than two viewports. * @public */ export class TwoWayViewportSync { @@ -123,6 +179,7 @@ export class TwoWayViewportSync { * For example, zooming out in one viewport will zoom out by the same distance in the other viewport. * No other aspects of the viewports are synchronized - they may have entirely different display styles, category/model selectors, etc. * @see [[TwoWayViewportSync]] to synchronize all aspects of the viewports. + * @see [[connectViewportFrusta]] to synchronize the frusta of more than two viewports. * @public */ export class TwoWayViewportFrustumSync extends TwoWayViewportSync { From 880701222c18eb28d73795bc408693ac5929d041 Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Fri, 13 May 2022 07:18:44 -0400 Subject: [PATCH 08/16] extract-api --- common/api/core-frontend.api.md | 18 ++++++++++++++++++ common/api/summary/core-frontend.exports.csv | 6 ++++++ 2 files changed, 24 insertions(+) diff --git a/common/api/core-frontend.api.md b/common/api/core-frontend.api.md index 13ef3e7cbea9..cf24be470b52 100644 --- a/common/api/core-frontend.api.md +++ b/common/api/core-frontend.api.md @@ -1919,6 +1919,15 @@ export interface ComputeChordToleranceArgs { readonly graphic: GraphicBuilder; } +// @public +export function connectViewportFrusta(viewports: Iterable): () => void; + +// @public +export function connectViewports(viewports: Iterable, sync: (source: Viewport) => SynchronizeViewports): () => void; + +// @public +export function connectViewportViews(viewports: Iterable): () => void; + // @internal (undocumented) export enum ContextMode { // (undocumented) @@ -10142,6 +10151,15 @@ export class SuspendedToolState { stop(): void; } +// @public +export function synchronizeViewportFrusta(source: Viewport): SynchronizeViewports; + +// @public +export type SynchronizeViewports = (source: Viewport, target: Viewport) => void; + +// @public +export function synchronizeViewportViews(source: Viewport): SynchronizeViewports; + // @internal (undocumented) export abstract class Target extends RenderTarget implements RenderTargetDebugControl, WebGLDisposable { protected constructor(rect?: ViewRect); diff --git a/common/api/summary/core-frontend.exports.csv b/common/api/summary/core-frontend.exports.csv index 98d6f4a31119..8821dc7a423e 100644 --- a/common/api/summary/core-frontend.exports.csv +++ b/common/api/summary/core-frontend.exports.csv @@ -89,6 +89,9 @@ public;Cluster beta;CollectTileStatus = "accept" | "reject" | "continue" alpha;CompassMode public;ComputeChordToleranceArgs +public;connectViewportFrusta(viewports: Iterable +public;connectViewports(viewports: Iterable +public;connectViewportViews(viewports: Iterable internal;ContextMode public;ContextRealityModelState public;ContextRotationId @@ -584,6 +587,9 @@ internal;SubCategoriesCache internal;SubCategoriesRequest beta;SurveyLengthDescription internal;SuspendedToolState +public;synchronizeViewportFrusta(source: Viewport): SynchronizeViewports +public;SynchronizeViewports = (source: Viewport, target: Viewport) => void +public;synchronizeViewportViews(source: Viewport): SynchronizeViewports internal;class Target internal;TentativeOrAccuSnap public;TentativePoint From 180ed685dbfdd3233ea6789a5b3fa88ea5876ec6 Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Fri, 13 May 2022 07:21:02 -0400 Subject: [PATCH 09/16] @extensions --- core/frontend/src/ViewportSync.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/frontend/src/ViewportSync.ts b/core/frontend/src/ViewportSync.ts index 945caaaaf67a..1384abb11a90 100644 --- a/core/frontend/src/ViewportSync.ts +++ b/core/frontend/src/ViewportSync.ts @@ -13,6 +13,7 @@ import { Viewport } from "./Viewport"; * The source viewport is the viewport in the connection whose state has changed. * The function will be invoked once for each target viewport in the connection. * @public + * @extensions */ export type SynchronizeViewports = (source: Viewport, target: Viewport) => void; @@ -43,6 +44,7 @@ export type SynchronizeViewports = (source: Viewport, target: Viewport) => void; * @see [[TwoWayViewportSync]] to synchronize the state of exactly two viewports. * @see [Multiple Viewport Sample](https://www.itwinjs.org/sample-showcase/?group=Viewer+Features&sample=multi-viewport-sample&imodel=Metrostation+Sample) * @public + * @extensions */ export function connectViewports(viewports: Iterable, sync: (source: Viewport) => SynchronizeViewports): () => void { const disconnect: VoidFunction[] = []; @@ -87,6 +89,7 @@ export function connectViewports(viewports: Iterable, sync: (source: V * display style, model and category selectors, [Frustum]($common), etc. * @see [[connectViewportViews]] to establish a connection between viewports using this synchronization strategy. * @public + * @extensions */ export function synchronizeViewportViews(source: Viewport): SynchronizeViewports { return (_source, target) => target.applyViewState(source.view.clone(target.iModel)); @@ -95,6 +98,7 @@ export function synchronizeViewportViews(source: Viewport): SynchronizeViewports /** A function that returns a [[SynchronizeViewports]] function that synchronizes the viewed volumes of each viewport. * @see [[connectViewportFrusta]] to establish a connection between viewports using this synchronization strategy. * @public + * @extensions */ export function synchronizeViewportFrusta(source: Viewport): SynchronizeViewports { const pose = source.view.savePose(); @@ -108,6 +112,7 @@ export function synchronizeViewportFrusta(source: Viewport): SynchronizeViewport * will zoom out by the same distance in all of the other viewports. * @see [[connectViewports]] to customize how the viewports are synchronized. * @public + * @extensions */ export function connectViewportFrusta(viewports: Iterable): () => void { return connectViewports(viewports, (source) => synchronizeViewportFrusta(source)); @@ -118,6 +123,7 @@ export function connectViewportFrusta(viewports: Iterable): () => void * @see [[connectViewportFrusta]] to synchronize only the [Frustum]($common) of each viewport. * @see [[connectViewports]] to customize how the viewports are synchronized. * @public + * @extensions */ export function connectViewportViews(viewports: Iterable): () => void { return connectViewports(viewports, (source) => synchronizeViewportViews(source)); @@ -133,6 +139,7 @@ export function connectViewportViews(viewports: Iterable): () => void * @see [[TwoWayViewportFrustumSync]] to synchronize only the frusta of the viewports. * @see [[connectViewportViews]] to synchronize the state of more than two viewports. * @public + * @extensions */ export class TwoWayViewportSync { protected readonly _disconnect: VoidFunction[] = []; @@ -181,6 +188,7 @@ export class TwoWayViewportSync { * @see [[TwoWayViewportSync]] to synchronize all aspects of the viewports. * @see [[connectViewportFrusta]] to synchronize the frusta of more than two viewports. * @public + * @extensions */ export class TwoWayViewportFrustumSync extends TwoWayViewportSync { /** @internal override */ From f4cb5aec3f7ad2b3f6af574122ebf389507ae32e Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Fri, 13 May 2022 07:23:17 -0400 Subject: [PATCH 10/16] lint --- core/frontend/src/test/ViewportSync.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/frontend/src/test/ViewportSync.test.ts b/core/frontend/src/test/ViewportSync.test.ts index af5ee41b123a..5477a3782a17 100644 --- a/core/frontend/src/test/ViewportSync.test.ts +++ b/core/frontend/src/test/ViewportSync.test.ts @@ -9,7 +9,7 @@ import { IModelApp } from "../IModelApp"; import { StandardViewId } from "../StandardView"; import { Viewport } from "../Viewport"; import { - connectViewports, SynchronizeViewports, synchronizeViewportFrusta, synchronizeViewportViews, TwoWayViewportFrustumSync, TwoWayViewportSync, + connectViewports, synchronizeViewportFrusta, synchronizeViewportViews, TwoWayViewportFrustumSync, TwoWayViewportSync, } from "../ViewportSync"; import { openBlankViewport } from "./openBlankViewport"; @@ -177,7 +177,7 @@ describe("TwoWayViewportSync", () => { describe("connectViewports", () => { const nVps = 4; - let vps: Viewport[] = []; + const vps: Viewport[] = []; before(async () => IModelApp.startup()); after(async () => IModelApp.shutdown()); @@ -232,7 +232,7 @@ describe("connectViewports", () => { const disconnect = connect(); expect(allSameFrustum()).to.be.true; - let prevFrust = vps[2].getFrustum(); + const prevFrust = vps[2].getFrustum(); rotate(vps[2], StandardViewId.Iso); expect(prevFrust.isSame(vps[2].getFrustum())).to.be.false; expect(allSameFrustum()).to.be.true; From ea45102dc316c564bc101324fb5aba1d99bb68f5 Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Fri, 13 May 2022 07:52:43 -0400 Subject: [PATCH 11/16] NextVersion --- docs/changehistory/NextVersion.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index 670d8ee58545..71bbaf4a60c4 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -6,6 +6,7 @@ publish: false Table of contents: - [Display](#display) + - [Multi-way viewport sync](#multi-way-viewport-sync) - [Batching of pickable graphics](#batching-of-pickable-graphics) - [Detecting integrated graphics](#detecting-integrated-graphics) - [Improved polyface edges](#improved-polyface-edges) @@ -36,6 +37,25 @@ Table of contents: ## Display +### Multi-way viewport sync + +[TwoWayViewportSync]($frontend) is useful for synchronizing the states of two or more [Viewport]($frontend)s such that navigations made in one viewport are reflected in the other viewport. But what if you want to synchronize more than two viewports? iTwin.js 3.2 introduces [connectViewports]($frontend) to establish a connection between any number of viewports. You supply the set of viewports to be connected and a function that implements the logic for synchronizing the viewports when any of their states change. You can sever the connection by invoking the function returned by `connectViewports`. [connectViewportViews]($frontend) and [connectViewportFrusta]($frontend) are supplied as alternatives to [TwoWayViewportSync]($frontend) and [TwoViewportFrustumSync]($frontend), respectively, that can operate on any number of viewports. + +Here's a simple example that keeps the viewports' [ViewFlags]($common) in sync: + +```ts + // Establish the connection. + const disconnect = connectViewports([viewport1, viewport2, viewport3, (changedViewport: Viewport) => { + // Supply a function that will synchronize the state of the other viewports with that of the changed viewport. + return (source: Viewport, target: Viewport) => { + target.viewFlags = source.viewFlags; + }; + }; + + // Some time later, sever the connection. + disconnect(); +``` + ### Batching of pickable graphics [Pickable decorations](../learning/frontend/ViewDecorations#pickable-view-graphic-decorations) associate an [Id64String]($bentley) with a [RenderGraphic]($frontend), enabling the graphic to be interacted with using mouse or touch inputs and to have its [appearance overridden](../learning/display/SymbologyOverrides.md). Previously, a [GraphicBuilder]($frontend) accepted only a single pickable Id. [Decorator]($frontend)s that produce many pickable objects were therefore required to create a separate graphic for each pickable Id. This can negatively impact display performance by increasing the number of draw calls. From a8ec08833d4ecc6c939eb29b15c449cf0a857acd Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Fri, 13 May 2022 07:56:59 -0400 Subject: [PATCH 12/16] clearer parameter name --- common/api/core-frontend.api.md | 2 +- core/frontend/src/ViewportSync.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/common/api/core-frontend.api.md b/common/api/core-frontend.api.md index c166cec46e1b..3a66be6a68ce 100644 --- a/common/api/core-frontend.api.md +++ b/common/api/core-frontend.api.md @@ -1924,7 +1924,7 @@ export interface ComputeChordToleranceArgs { export function connectViewportFrusta(viewports: Iterable): () => void; // @public -export function connectViewports(viewports: Iterable, sync: (source: Viewport) => SynchronizeViewports): () => void; +export function connectViewports(viewports: Iterable, sync: (changedViewport: Viewport) => SynchronizeViewports): () => void; // @public export function connectViewportViews(viewports: Iterable): () => void; diff --git a/core/frontend/src/ViewportSync.ts b/core/frontend/src/ViewportSync.ts index 1384abb11a90..e8659a6d5fb1 100644 --- a/core/frontend/src/ViewportSync.ts +++ b/core/frontend/src/ViewportSync.ts @@ -27,7 +27,7 @@ export type SynchronizeViewports = (source: Viewport, target: Viewport) => void; * To sever the connection, invoke the function returned by this function. For example: * ```ts * // set up the connection. - * const disconnect = connectViewports([viewport0, viewport1, viewport2], (source) => synchronizeViewportFrusta(source)); + * const disconnect = connectViewports([viewport0, viewport1, viewport2], (changedViewport) => synchronizeViewportFrusta(changedViewport)); * // some time later, sever the connection. * disconnect(); * ``` @@ -46,7 +46,7 @@ export type SynchronizeViewports = (source: Viewport, target: Viewport) => void; * @public * @extensions */ -export function connectViewports(viewports: Iterable, sync: (source: Viewport) => SynchronizeViewports): () => void { +export function connectViewports(viewports: Iterable, sync: (changedViewport: Viewport) => SynchronizeViewports): () => void { const disconnect: VoidFunction[] = []; let echo = false; From b92997758aefbde2daffdb93c99d9cacedac7a8f Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Fri, 13 May 2022 08:16:39 -0400 Subject: [PATCH 13/16] typo --- docs/changehistory/NextVersion.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changehistory/NextVersion.md b/docs/changehistory/NextVersion.md index 71bbaf4a60c4..2cee1be6d6a9 100644 --- a/docs/changehistory/NextVersion.md +++ b/docs/changehistory/NextVersion.md @@ -39,7 +39,7 @@ Table of contents: ### Multi-way viewport sync -[TwoWayViewportSync]($frontend) is useful for synchronizing the states of two or more [Viewport]($frontend)s such that navigations made in one viewport are reflected in the other viewport. But what if you want to synchronize more than two viewports? iTwin.js 3.2 introduces [connectViewports]($frontend) to establish a connection between any number of viewports. You supply the set of viewports to be connected and a function that implements the logic for synchronizing the viewports when any of their states change. You can sever the connection by invoking the function returned by `connectViewports`. [connectViewportViews]($frontend) and [connectViewportFrusta]($frontend) are supplied as alternatives to [TwoWayViewportSync]($frontend) and [TwoViewportFrustumSync]($frontend), respectively, that can operate on any number of viewports. +[TwoWayViewportSync]($frontend) is useful for synchronizing the states of two or more [Viewport]($frontend)s such that navigations made in one viewport are reflected in the other viewport. But what if you want to synchronize more than two viewports? iTwin.js 3.2 introduces [connectViewports]($frontend) to establish a connection between any number of viewports. You supply the set of viewports to be connected and a function that implements the logic for synchronizing the viewports when any of their states change. You can sever the connection by invoking the function returned by `connectViewports`. [connectViewportViews]($frontend) and [connectViewportFrusta]($frontend) are supplied as alternatives to [TwoWayViewportSync]($frontend) and [TwoWayViewportFrustumSync]($frontend), respectively, that can operate on any number of viewports. Here's a simple example that keeps the viewports' [ViewFlags]($common) in sync: From 0a0a7f0901446aa667b7bdc1c76c564bd8c9cf23 Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Fri, 13 May 2022 08:27:59 -0400 Subject: [PATCH 14/16] dta keyins can operate on more than 2 viewports. --- .../src/frontend/SyncViewportsTool.ts | 88 +++++++++++-------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/test-apps/display-test-app/src/frontend/SyncViewportsTool.ts b/test-apps/display-test-app/src/frontend/SyncViewportsTool.ts index dc09ecccb7f7..fc374d43604a 100644 --- a/test-apps/display-test-app/src/frontend/SyncViewportsTool.ts +++ b/test-apps/display-test-app/src/frontend/SyncViewportsTool.ts @@ -3,73 +3,85 @@ * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ -import { IModelApp, Tool, TwoWayViewportFrustumSync, TwoWayViewportSync, Viewport } from "@itwin/core-frontend"; +import { + connectViewportFrusta, connectViewportViews, IModelApp, Tool, Viewport, +} from "@itwin/core-frontend"; class State { - constructor(public vp1: Viewport, public vp2: Viewport, public sync: TwoWayViewportSync) { } + private readonly _viewportIds: number[]; + + constructor(viewports: Viewport[], public readonly disconnect: () => void) { + this._viewportIds = viewports.map((x) => x.viewportId).sort(); + } + + public equals(viewports: Viewport[]) { + if (viewports.length !== this._viewportIds.length) + return false; + + const ids = viewports.map((x) => x.viewportId).sort(); + return ids.every((val, idx) => val === this._viewportIds[idx]); + } } -/** Connect or disconnect two viewports using TwoWayViewportSync. */ +/** Connect or disconnect two or more viewports using connectViewports. */ export class SyncViewportsTool extends Tool { public static override toolId = "SyncViewports"; public static override get minArgs() { return 0; } - public static override get maxArgs() { return 2; } + public static override get maxArgs() { return undefined; } - protected get syncType(): typeof TwoWayViewportSync { return TwoWayViewportSync; } + protected get syncType(): "frustum" | "view" { return "view"; } private static _state?: State; private static _removeListeners?: VoidFunction; - public override async run(vp1?: Viewport, vp2?: Viewport): Promise { + public override async run(vps?: Viewport[]): Promise { const that = SyncViewportsTool; - if (!vp1 && !vp2) { + if (!vps || vps.length < 2) { that.disconnect(); - } else if (vp1 && vp2 && vp1 !== vp2) { - if (that._state && that._state.vp1 === vp1 && that._state.vp2 === vp2) + } else { + if (that._state && that._state.equals(vps)) that.disconnect(); else - that.connect(vp1, vp2, this.syncType); + that.connect(vps, this.syncType); } return true; } public override async parseAndRun(...args: string[]): Promise { - switch (args.length) { - case 0: - return this.run(); - case 2: - const vpId1 = Number.parseInt(args[0], 10); - const vpId2 = Number.parseInt(args[1], 10); - if (Number.isNaN(vpId1) || Number.isNaN(vpId2) || vpId1 === vpId2) - return true; - - let vp1, vp2; - for (const vp of IModelApp.viewManager) { - if (vp.viewportId === vpId1) - vp1 = vp; - else if (vp.viewportId === vpId2) - vp2 = vp; - } - - if (vp1 && vp2) - return this.run(vp1, vp2); + if (args.length === 0) + return this.run(); + + const allVps = Array.from(IModelApp.viewManager); + if (args.length === 1) + return args[0].toLowerCase() === "all" ? this.run(allVps) : false; + + const vps: Viewport[] = []; + for (const arg of args) { + const vpId = Number.parseInt(arg, 10); + if (Number.isNaN(vpId)) + return false; + + const vp = allVps.find((x) => x.viewportId === vpId); + if (!vp) + return false; + + vps.push(vp); } - return true; + return this.run(vps); } - private static connect(vp1: Viewport, vp2: Viewport, type: typeof TwoWayViewportSync): void { + private static connect(vps: Viewport[], syncType: "view" | "frustum"): void { this.disconnect(); - this._state = new State(vp1, vp2, new type()); - this._state.sync.connect(vp1, vp2); - const dispose1 = vp1.onDisposed.addOnce(() => this.disconnect()); - const dispose2 = vp2.onDisposed.addOnce(() => this.disconnect()); - this._removeListeners = () => { dispose1(); dispose2(); }; + const connect = "view" === syncType ? connectViewportViews : connectViewportFrusta; + this._state = new State(vps, connect(vps)); + const dispose = vps.map((x) => x.onDisposed.addOnce(() => this.disconnect())); + this._removeListeners = () => dispose.forEach((x) => x()); } private static disconnect(): void { - this._state?.sync.disconnect(); + this._state?.disconnect(); this._state = undefined; if (this._removeListeners) { @@ -83,5 +95,5 @@ export class SyncViewportsTool extends Tool { export class SyncViewportFrustaTool extends SyncViewportsTool { public static override toolId = "SyncFrusta"; - protected override get syncType() { return TwoWayViewportFrustumSync; } + protected override get syncType() { return "frustum" as const; } } From 14b1e0b943c76c0ef6cc114edec15874e28e8604 Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Fri, 13 May 2022 08:28:53 -0400 Subject: [PATCH 15/16] README --- test-apps/display-test-app/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-apps/display-test-app/README.md b/test-apps/display-test-app/README.md index 9669d5286bb3..258ecc585e3e 100644 --- a/test-apps/display-test-app/README.md +++ b/test-apps/display-test-app/README.md @@ -251,7 +251,7 @@ display-test-app has access to all key-ins defined in the `@itwin/core-frontend` * `gridsPerRef=number` Specify number of grid lines to display per reference line. * `orientation=0|1|2|3|4` Value for GridOrientationType. * `dta model transform` - Apply a display transform to all models currently displayed in the selected viewport. Origin is specified like `x=1 y=2 z=3`; pitch and roll as `p=45 r=90` in degrees. Any argument can be omitted. Omitting all arguments clears the display transform. Snapping intentionally does not take the display transform into account. -* `dta viewport sync *viewportId1* *viewportId2*` - Synchronize the contents of two viewports, specifying them by integer Id displayed in their title bars. Omit the Ids to disconnect two previously synchronized viewports. +* `dta viewport sync viewportIds` - Synchronize the contents of two or more viewports, specifying them by integer Id displayed in their title bars, or "all" to apply to all open viewports. Omit the Ids to disconnect previously synchronized viewports. * `dta frustum sync *viewportId1* *viewportId2*` - Like `dta viewport sync but synchronizes only the frusta of the viewports. * `dta gen tile *modelId=* *contentId=*` - Trigger a request to obtain tile content for the specified tile. This is chiefly useful for breaking in the debugger during that process to diagnose issues. * `dta gen graphics` - Trigger a requestElementGraphics call to generate graphics for a single element. This is chiefly useful for breaking in the debugger during that process to diagnose issues. From 9e02c36ff816f558872944c93e14977185b2e04d Mon Sep 17 00:00:00 2001 From: Paul Connelly <22944042+pmconne@users.noreply.github.com> Date: Fri, 13 May 2022 08:34:58 -0400 Subject: [PATCH 16/16] inaccurate doc: --- core/frontend/src/ViewportSync.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/frontend/src/ViewportSync.ts b/core/frontend/src/ViewportSync.ts index e8659a6d5fb1..4056fdd04187 100644 --- a/core/frontend/src/ViewportSync.ts +++ b/core/frontend/src/ViewportSync.ts @@ -8,7 +8,7 @@ import { Viewport } from "./Viewport"; -/** A function used by [[connectViewports]] to obtain another function that can synchronize each target [[Viewport]] with +/** A function used by [[connectViewports]] that can synchronize the state of a target [[Viewport]] with * changes in the state of a source Viewport. * The source viewport is the viewport in the connection whose state has changed. * The function will be invoked once for each target viewport in the connection.