imodel图形显示简单介绍
背景
通过简单了解iModel中模型的显示过程,有助于我们理解并添加FeatureOverride等一些特性,从而实现一些需求。
理解
以视口V为例,基本过程如下所示:
其中视口V的ViewState如何向SceneContext的实例sc填充内容呢?
以SpatialViewState为例,其中ViewState2d过程类似。
我们知道每个SpatialViewState实例都维护着一个ModelSelectorState实例,其中包含着视口将要显示的Model的ID。然后,根据其所包含的ModelState(确切地说是GeometricModelState实例)开始调用createTileTreeReference以创建每个ModelState所需要的TileTreeReference的实例,然后利用TileTreeReference的实例将其引用的TileTree所产生的图像内容填充到sc实例中即可。
大概流程如下所示:
因此,也可以简单的理解为以下关系:
应用
这样的话,我们就可以通过自定义TiledGraphicsProvider以及FeatureOverrideProvider 从而实现一些功能。(即在对比不同的2个版本过程中,可以将图形中新增的,已删除,更改的元素分别以不同的颜色显示,以更直观的方式显示出2个版本之间的差异性)。
以下是通过自定义TiledGraphicsProvider以及FeatureOverrideProvider 并结合上述思想实现的简单模拟版本对比功能,具体代码如下所示:
import {
SpatialModelTileTrees,
TiledGraphicsProvider,
FeatureOverrideProvider,
Viewport,
IModelApp,
Tool,
IModelConnection,
FeatureSymbology,
SpatialViewState,
SnapshotConnection,
TileTreeReference,
ChangeFlags,
SpatialModelState,
TileTree,
} from "@bentley/imodeljs-frontend";
import { BeTimePoint, assert } from "@bentley/bentleyjs-core";
import {
FeatureAppearance,
ColorDef,
RgbColor,
} from "@bentley/imodeljs-common";
interface ChangedElems {
deleted: Set<string>;
inserted: Set<string>;
updated: Set<string>;
}
export class VersionComparisonTool extends Tool {
public static toolId = "VersionComparisonTool";
public run(_args: any[]): boolean {
const vp = IModelApp.viewManager.selectedView;
if (undefined !== vp) {
emulateVersionComparison(vp);
}
return true;
}
}
class Trees extends SpatialModelTileTrees {
private readonly _provider: Provider;
public constructor(provider: Provider) {
super(provider.viewport.view as SpatialViewState);
this._provider = provider;
}
protected get _iModel() {
return this._provider.iModel;
}
protected createTileTreeReference(
model: SpatialModelState
): TileTreeReference | undefined {
// ###TODO: If model contains no deleted elements, ignore
return new Reference(
model.createTileTreeReference(this._provider.viewport.view),
this._provider
);
}
}
/** A proxy reference to a TileTreeReference originating from the secondary IModelConnection. */
class Reference extends TileTreeReference {
private readonly _ref: TileTreeReference;
private readonly _provider: Provider;
public constructor(ref: TileTreeReference, provider: Provider) {
super();
this._ref = ref;
this._provider = provider;
}
public get castsShadows() {
return this._ref.castsShadows;
}
public get treeOwner() {
return this._ref.treeOwner;
}
protected getSymbologyOverrides(_tree: TileTree) {
return this._provider.overrides;
}
}
/** Returns true if version comparison is currently activated for the specified viewport. */
export function isVersionComparisonEnabled(vp: Viewport): boolean {
return undefined !== vp.findFeatureOverrideProviderOfType<Provider>(Provider);
}
export async function emulateVersionComparison(vp: Viewport): Promise<void> {
if (isVersionComparisonEnabled(vp)) {
await disableVersionComparison(vp);
return;
}
await Provider.create(vp);
}
async function getElems(iModel: IModelConnection): Promise<Set<string>> {
const elems = new Set<string>();
const ecsql =
"SELECT ECInstanceId FROM BisCore.SpatialElement WHERE GeometryStream IS NOT NULL";
for await (const row of iModel.query(ecsql)) elems.add(row.id);
return elems;
}
/** The most brain-dead, inefficient way of determining which elements exist only in one iModel or the other possible.
* Any element present in only one or the other iModel is treated as an insertion or deletion.
* Additionally, selection set is used as the set of updated elements.
*/
async function determineChangedElems(
iModel: IModelConnection,
revision: IModelConnection
): Promise<ChangedElems> {
const inserted = await getElems(iModel);
const deleted = await getElems(revision);
for (const elem of deleted) {
if (inserted.has(elem)) {
deleted.delete(elem);
inserted.delete(elem);
}
}
const updated = new Set<string>();
for (const selected of iModel.selectionSet.elements)
if (!inserted.has(selected) && !deleted.has(selected))
updated.add(selected);
iModel.selectionSet.emptyAll();
return { inserted, deleted, updated };
}
const changedTransparency = 16; // NB: This will appear more transparent due to use of "fade-out" mode (flat alpha weight).
const unchangedAppearance = FeatureAppearance.fromJSON({
rgb: RgbColor.fromColorDef(ColorDef.blue),
transparency: 0.7,
nonLocatable: true,
});
/** Added to a Viewport to supply graphics from the secondary IModelConnection. */
class Provider implements TiledGraphicsProvider, FeatureOverrideProvider {
private readonly _trees: SpatialModelTileTrees;
public readonly iModel: IModelConnection;
public overrides: FeatureSymbology.Overrides;
public readonly changedElems: ChangedElems;
public readonly viewport: Viewport;
private readonly _removals: Array<() => void> = [];
public constructor(
vp: Viewport,
iModel: IModelConnection,
elems: ChangedElems
) {
this.iModel = iModel;
this.changedElems = elems;
this.viewport = vp;
this._trees = new Trees(this);
this.overrides = this.initOverrides();
this._removals.push(
vp.onViewportChanged.addListener((_vp, flags) =>
this.handleViewportChanged(flags)
)
);
vp.addTiledGraphicsProvider(this);
vp.addFeatureOverrideProvider(this);
vp.isFadeOutActive = true;
}
public dispose(): void {
for (const removal of this._removals) removal();
this._removals.length = 0;
this.viewport.dropFeatureOverrideProvider(this);
this.viewport.isFadeOutActive = false;
this.viewport.dropTiledGraphicsProvider(this);
// closing the iModel will do this - but let's not wait.
this.iModel.tiles.purge(BeTimePoint.now());
this.iModel.close(); // eslint-disable-line @typescript-eslint/no-floating-promises
}
public static async create(vp: Viewport): Promise<Provider | undefined> {
try {
const view = vp.view as SpatialViewState;
assert(view.isSpatialView());
// Open the "revision" iModel.
// const filename = vp.iModel.getRpcProps().key + ".rev";
const filename = vp.iModel.getRpcProps().key;
const iModel = await SnapshotConnection.openFile(filename);
// ###TODO determine which model(s) contain the deleted elements - don't need tiles for any others.
await iModel.models.load(view.modelSelector.models);
const changedElems = await determineChangedElems(vp.iModel, iModel);
return new Provider(vp, iModel, changedElems);
} catch (err) {
alert(err.toString());
return undefined;
}
}
public forEachTileTreeRef(
_vp: Viewport,
func: (ref: TileTreeReference) => void
): void {
this._trees.forEach(func);
}
/** The overrides applied to the *primary* IModelConnection, to hilite inserted/updated elements. */
public addFeatureOverrides(
overrides: FeatureSymbology.Overrides,
_viewport: Viewport
): void {
overrides.setDefaultOverrides(unchangedAppearance);
for (const elem of this.changedElems.deleted) overrides.setNeverDrawn(elem);
const inserted = FeatureAppearance.fromRgba(
// ColorDef.from(0, 0xff, 0, changedTransparency)
ColorDef.red
);
for (const elem of this.changedElems.inserted)
overrides.overrideElement(elem, inserted);
const updated = FeatureAppearance.fromRgba(
// ColorDef.from(0, 0x7f, 0xff, changedTransparency)
ColorDef.blue
);
for (const elem of this.changedElems.updated)
overrides.overrideElement(elem, updated);
}
/** The overrides applied to the tiles from the *secondary* IModelConnection, to draw only deleted elements. */
private initOverrides(): FeatureSymbology.Overrides {
const ovrs = new FeatureSymbology.Overrides(this.viewport);
ovrs.neverDrawn.clear();
ovrs.alwaysDrawn.clear();
ovrs.setAlwaysDrawnSet(this.changedElems.deleted, true, false); // really "only-drawn" - only draw our deleted elements - unless their subcategory is turned off.
const red = ColorDef.from(0xff, 0, 0, changedTransparency);
ovrs.setDefaultOverrides(FeatureAppearance.fromRgba(red));
return ovrs;
}
private handleViewportChanged(flags: ChangeFlags): void {
if (flags.viewState && !this.viewport.view.isSpatialView()) {
// Switched to a 2d view. Terminate version comparison.
this.dispose();
return;
}
if (flags.areFeatureOverridesDirty) {
this.overrides = this.initOverrides();
this.viewport.invalidateScene();
}
if (flags.viewedModels) {
this._trees.markDirty();
this.viewport.invalidateScene();
const models = (this.viewport.view as SpatialViewState).modelSelector
.models;
const unloaded = this.iModel.models.filterLoaded(models);
if (undefined === unloaded) return;
this.iModel.models
.load(unloaded)
.then(() => {
this._trees.markDirty();
this.viewport.invalidateScene();
})
.catch((_) => undefined);
}
}
}
/** Turn off version comparison if it is enabled. */
export async function disableVersionComparison(vp: Viewport): Promise<void> {
const existing = vp.findFeatureOverrideProviderOfType<Provider>(Provider);
if (undefined !== existing) {
existing.dispose();
await existing.iModel.close();
}
}
补充
其中,在从TileTreeReference获取其所引用的TileTree使用了较复杂的几个类层次并涉及地形,图像图层等,其关系简单罗列如下所示: