UNPKG

@itwin/core-frontend

Version:
311 lines • 14.6 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Bentley Systems, Incorporated. All rights reserved. * See LICENSE.md in the project root for license terms and full copyright notice. *--------------------------------------------------------------------------------------------*/ /** @packageDocumentation * @module IModelConnection */ import { assert, BeEvent, CompressedId64Set, Guid, IModelStatus, OpenMode } from "@itwin/core-bentley"; import { getPullChangesIpcChannel, IModelError, } from "@itwin/core-common"; import { BriefcaseTxns } from "./BriefcaseTxns"; import { GraphicalEditingScope } from "./GraphicalEditingScope"; import { IModelApp } from "./IModelApp"; import { IModelConnection } from "./IModelConnection"; import { IpcApp } from "./IpcApp"; import { disposeTileTreesForGeometricModels } from "./tile/internal"; import { Viewport } from "./Viewport"; /** Keeps track of changes to models, buffering them until synchronization points. * While a GraphicalEditingScope is open, the changes are buffered until the scope exits, at which point they are processed. * Otherwise, the buffered changes are processed after undo/redo, commit, or pull+merge changes. */ class ModelChangeMonitor { _editingScope; _briefcase; _deletedModels = new Set(); _modelIdToGuid = new Map(); _removals = []; constructor(briefcase) { this._briefcase = briefcase; // Buffer updated geometry guids. this._removals.push(briefcase.txns.onModelGeometryChanged.addListener((changes) => { for (const change of changes) { this._deletedModels.delete(change.id); this._modelIdToGuid.set(change.id, change.guid); } })); // Buffer deletions of models. this._removals.push(briefcase.txns.onModelsChanged.addListener((changes) => { if (changes.deleted) { for (const id of CompressedId64Set.iterable(changes.deleted)) { this._modelIdToGuid.delete(id); this._deletedModels.add(id); } } })); // Outside of an editing scope, we want to update viewport contents after commit, undo/redo, or merging changes. const maybeProcess = async () => { if (this.editingScope) return; const modelIds = Array.from(this._modelIdToGuid.keys()); if (modelIds.length > 0) await IModelApp.tileAdmin.purgeTileTrees(this._briefcase, modelIds); this.processBuffered(); }; this._removals.push(briefcase.txns.onCommitted.addListener(maybeProcess)); this._removals.push(briefcase.txns.onReplayedExternalTxns.addListener(maybeProcess)); this._removals.push(briefcase.txns.onAfterUndoRedo.addListener(maybeProcess)); this._removals.push(briefcase.txns.onChangesPulled.addListener(maybeProcess)); } async close() { for (const removal of this._removals) removal(); this._removals.length = 0; if (this._editingScope) { await this._editingScope.exit(); this._editingScope = undefined; } } get editingScope() { return this._editingScope; } async enterEditingScope() { if (this._editingScope) throw new Error("Cannot create an editing scope for an iModel that already has one"); this._editingScope = await GraphicalEditingScope.enter(this._briefcase); const removeGeomListener = this._editingScope.onGeometryChanges.addListener((changes) => { const modelIds = []; for (const change of changes) modelIds.push(change.id); this.invalidateScenes(modelIds); }); this._editingScope.onExited.addOnce((scope) => { assert(scope === this._editingScope); this._editingScope = undefined; removeGeomListener(); this.processBuffered(); }); return this._editingScope; } processBuffered() { const models = this._briefcase.models; for (const [id, guid] of this._modelIdToGuid) { const model = models.getLoaded(id)?.asGeometricModel; if (model) model.geometryGuid = guid; } const modelIds = new Set(this._modelIdToGuid.keys()); for (const deleted of this._deletedModels) { modelIds.add(deleted); models.unload(deleted); } this.invalidateScenes(modelIds); disposeTileTreesForGeometricModels(modelIds, this._briefcase); this._briefcase.onBufferedModelChanges.raiseEvent(modelIds); this._modelIdToGuid.clear(); this._deletedModels.clear(); } invalidateScenes(changedModels) { for (const user of IModelApp.tileAdmin.tileUsers) { if (user instanceof Viewport && user.iModel === this._briefcase) { for (const modelId of changedModels) { if (user.view.viewsModel(modelId)) { user.invalidateScene(); user.setFeatureOverrideProviderChanged(); break; } } } } } } /** Settings that can be used to control the behavior of [[Tool]]s that modify a [[BriefcaseConnection]]. * For example, tools that want to create new elements can consult the briefcase's editor tool settings to * determine into which model and category to insert the elements. * Specialized tools are free to ignore these settings. * @see [[BriefcaseConnection.editorToolSettings]] to query or modify the current settings for a briefcase. * @see [CreateElementTool]($editor-frontend) for an example of a tool that uses these settings. * @beta */ export class BriefcaseEditorToolSettings { _category; _model; /** An event raised just after the default [[category]] is changed. */ onCategoryChanged = new BeEvent(); /** An event raised just after the default [[model]] is changed. */ onModelChanged = new BeEvent(); /** The [Category]($backend) into which new elements should be inserted by default. * Specialized tools are free to ignore this setting and instead use their own logic to select an appropriate category. * @see [[onCategoryChanged]] to be notified when this property is modified. * @see [CreateElementTool.targetCategory]($editor-frontend) for an example of a tool that uses this setting. */ get category() { return this._category; } set category(category) { const previousCategory = this.category; if (category !== this.category) { this._category = category; this.onCategoryChanged.raiseEvent(previousCategory); } } /** The [Model]($backend) into which new elements should be inserted by default. * Specialized tools are free to ignore this setting and instead use their own logic to select an appropriate model. * @see [[onModelChanged]] to be notified when this property is modified. * @see [CreateElementTool.targetModelId]($editor-frontend) for an example of a tool that uses this setting. */ get model() { return this._model; } set model(model) { const previousModel = this.model; if (model !== this.model) { this._model = model; this.onModelChanged.raiseEvent(previousModel); } } } /** A connection to an editable briefcase on the backend. This class uses [Ipc]($docs/learning/IpcInterface.md) to communicate * to the backend and may only be used by [[IpcApp]]s. * @public */ export class BriefcaseConnection extends IModelConnection { _isClosed; _modelsMonitor; /** Default settings that can be used to control the behavior of [[Tool]]s that modify this briefcase. * @beta */ editorToolSettings = new BriefcaseEditorToolSettings(); /** Manages local changes to the briefcase via [Txns]($docs/learning/InteractiveEditing.md). */ txns; isBriefcaseConnection() { return true; } /** The Guid that identifies the iTwin that owns this iModel. */ get iTwinId() { return super.iTwinId; } // GuidString | undefined for IModelConnection, but required for BriefcaseConnection /** The Guid that identifies this iModel. */ get iModelId() { return super.iModelId; } // GuidString | undefined for IModelConnection, but required for BriefcaseConnection constructor(props, openMode) { super(props); this._openMode = openMode; this.txns = new BriefcaseTxns(this); this._modelsMonitor = new ModelChangeMonitor(this); if (OpenMode.ReadWrite === this._openMode) this.txns.onAfterUndoRedo.addListener(async () => { await IModelApp.toolAdmin.restartPrimitiveTool(); }); } /** Open a BriefcaseConnection to a [BriefcaseDb]($backend). */ static async openFile(briefcaseProps) { const iModelProps = await IpcApp.appFunctionIpc.openBriefcase(briefcaseProps); const connection = new this({ ...briefcaseProps, ...iModelProps }, briefcaseProps.readonly ? OpenMode.Readonly : OpenMode.ReadWrite); IModelConnection.onOpen.raiseEvent(connection); return connection; } /** Open a BriefcaseConnection to a [StandaloneDb]($backend) * @note StandaloneDbs, by definition, may not push or pull changes. Attempting to do so will throw exceptions. */ static async openStandalone(filePath, openMode = OpenMode.ReadWrite, opts) { const openResponse = await IpcApp.appFunctionIpc.openStandalone(filePath, openMode, opts); const connection = new this(openResponse, openMode); IModelConnection.onOpen.raiseEvent(connection); return connection; } /** Returns `true` if [[close]] has already been called. */ get isClosed() { return this._isClosed === true; } /** * Close this BriefcaseConnection. * @note make sure to call [[saveChanges]] before calling this method. Unsaved local changes are abandoned. */ async close() { if (this.isClosed) return; await this._modelsMonitor.close(); this.beforeClose(); this.txns[Symbol.dispose](); this._isClosed = true; await IpcApp.appFunctionIpc.closeIModel(this._fileKey); } requireTimeline() { if (this.iTwinId === Guid.empty) throw new IModelError(IModelStatus.WrongIModel, "iModel has no timeline"); } /** Query if there are any pending Txns in this briefcase that are waiting to be pushed. */ async hasPendingTxns() { return this.txns.hasPendingTxns(); } /** Commit pending changes to this briefcase. * @param description Optional description of the changes. */ async saveChanges(description) { await IpcApp.appFunctionIpc.saveChanges(this.key, description); } /** Abandon pending changes to this briefcase. */ async abandonChanges() { await IpcApp.appFunctionIpc.abandonChanges(this.key); } /** Pull (and potentially merge if there are local changes) up to a specified changeset from iModelHub into this briefcase * @param toIndex The changeset index to pull changes to. If `undefined`, pull all changes. * @param options Options for pulling changes. * @see [[BriefcaseTxns.onChangesPulled]] for the event dispatched after changes are pulled. */ async pullChanges(toIndex, options) { const removeListeners = []; const shouldReportProgress = !!options?.downloadProgressCallback; if (shouldReportProgress) { const handleProgress = (_evt, data) => { options?.downloadProgressCallback?.(data); }; const removeProgressListener = IpcApp.addListener(getPullChangesIpcChannel(this.iModelId), handleProgress); removeListeners.push(removeProgressListener); } if (options?.abortSignal) { const abort = () => void IpcApp.appFunctionIpc.cancelPullChangesRequest(this.key); options?.abortSignal.addEventListener("abort", abort); removeListeners.push(() => options?.abortSignal?.removeEventListener("abort", abort)); } this.requireTimeline(); const ipcAppOptions = { reportProgress: shouldReportProgress, progressInterval: options?.progressInterval, enableCancellation: !!options?.abortSignal, }; try { this.changeset = await IpcApp.appFunctionIpc.pullChanges(this.key, toIndex, ipcAppOptions); } finally { removeListeners.forEach((remove) => remove()); } } /** Create a changeset from local Txns and push to iModelHub. On success, clear Txn table. * @param description The description for the changeset * @returns the changesetId of the pushed changes * @see [[BriefcaseTxns.onChangesPushed]] for the event dispatched after changes are pushed. */ async pushChanges(description) { this.requireTimeline(); return IpcApp.appFunctionIpc.pushChanges(this.key, description); } /** The current graphical editing scope, if one is in progress. * @see [[enterEditingScope]] to begin graphical editing. */ get editingScope() { return this._modelsMonitor.editingScope; } /** Return whether graphical editing is supported for this briefcase. It is not supported if the briefcase is read-only, or the briefcase contains a version of * the BisCore ECSchema older than v0.1.11. * @see [[enterEditingScope]] to enable graphical editing. */ async supportsGraphicalEditing() { return IpcApp.appFunctionIpc.isGraphicalEditingSupported(this.key); } /** Begin a new graphical editing scope. * @throws Error if an editing scope already exists or one could not be created. * @see [[GraphicalEditingScope.exit]] to exit the scope. * @see [[supportsGraphicalEditing]] to determine whether this method should be expected to succeed. * @see [[editingScope]] to obtain the current editing scope, if one is in progress. */ async enterEditingScope() { return this._modelsMonitor.enterEditingScope(); } /** Strictly for tests - dispatched from ModelChangeMonitor.processBuffered. * @internal */ onBufferedModelChanges = new BeEvent(); } //# sourceMappingURL=BriefcaseConnection.js.map