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使用了较复杂的几个类层次并涉及地形,图像图层等,其关系简单罗列如下所示: