UNPKG

@itwin/core-frontend

Version:
645 lines • 28.8 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 Tiles */ import { assert, comparePossiblyUndefined, compareStrings, } from "@itwin/core-bentley"; import { BatchType, compareIModelTileTreeIds, FeatureAppearanceProvider, iModelTileTreeIdToString, ModelMapLayerDrapeTarget, ModelMapLayerSettings, RenderMode, SpatialClassifier, } from "@itwin/core-common"; import { Range3d, StringifiedClipVector, Transform } from "@itwin/core-geometry"; import { IModelApp } from "../../IModelApp"; import { formatAnimationBranchId } from "../../internal/render/AnimationBranchState"; import { AnimationNodeId } from "../../common/internal/render/AnimationNodeId"; import { IModelTileTree, iModelTileTreeParamsFromJSON, LayerTileTreeReferenceHandler, MapLayerTileTreeReference, TileGraphicType, TileTreeReference, } from "../../tile/internal"; import { _scheduleScriptReference } from "../../common/internal/Symbols"; class PlanProjectionTileTree extends IModelTileTree { baseElevation; constructor(params, treeId, baseElevation) { super(params, treeId); this.baseElevation = baseElevation; } } class PrimaryTreeSupplier { constructor() { } compareTileTreeIds(lhs, rhs) { // NB: we don't compare isPlanProjection or is3d - they should always have the same value for a given modelId. return compareStrings(lhs.modelId, rhs.modelId) || compareIModelTileTreeIds(lhs.treeId, rhs.treeId) || comparePossiblyUndefined((x, y) => x.compareTo(y), lhs.timeline, rhs.timeline); } async createTileTree(id, iModel) { const treeId = id.treeId; const idStr = iModelTileTreeIdToString(id.modelId, treeId, IModelApp.tileAdmin); const props = await IModelApp.tileAdmin.requestTileTreeProps(iModel, idStr); // ###TODO remove restriction that animated tile trees can't contained instanced geometry. const isAnimated = undefined !== treeId.animationId || undefined !== id.timeline; const allowInstancing = !isAnimated && !treeId.enforceDisplayPriority && !treeId.sectionCut; const options = { edges: treeId.edges, allowInstancing, is3d: id.is3d, batchType: BatchType.Primary, timeline: id.timeline, }; const params = iModelTileTreeParamsFromJSON(props, iModel, id.modelId, options); if (!id.isPlanProjection) return new IModelTileTree(params, id.treeId); let elevation = 0; try { const ranges = await iModel.models.queryExtents(id.modelId); if (1 === ranges.length) { const range = Range3d.fromJSON(ranges[0].extents); const lo = range.low.z; const hi = range.high.z; if (lo <= hi) elevation = (lo + hi) / 2; } } catch { // } return new PlanProjectionTileTree(params, id.treeId, elevation); } getOwner(id, iModel) { return iModel.tiles.getTileTreeOwner(id, this); } addModelsAnimatedByScript(modelIds, scriptSourceId, trees) { // Note: This is invoked when an element hosting a schedule script is updated - it doesn't care about frontend schedule scripts. for (const tree of trees) if (scriptSourceId === tree.id.treeId.animationId) modelIds.add(tree.id.modelId); } addSpatialModels(modelIds, trees) { for (const tree of trees) if (tree.id.is3d) modelIds.add(tree.id.modelId); } } const primaryTreeSupplier = new PrimaryTreeSupplier(); /** Find all extant tile trees associated with the specified model Ids and dispose of them. * This is used by BriefcaseConnection when a GraphicalEditingScope is exited or after a change to the models' geometry guids * is committed, undone, redone, or merged. */ export function disposeTileTreesForGeometricModels(modelIds, iModel) { const trees = iModel.tiles.getTreeOwnersForSupplier(primaryTreeSupplier); for (const kvp of trees) { const id = kvp.id; assert(undefined !== id.modelId); if (modelIds.has(id.modelId)) kvp.owner[Symbol.dispose](); } } class PrimaryTreeReference extends TileTreeReference { view; model; /** Chiefly for debugging - disables iteration of this reference in SpatialModelRefs to e.g. omit the reference from the scene. */ deactivated = false; _viewFlagOverrides; _id; _owner; _sectionClip; _sectionCutAppearanceProvider; _animationTransformNodeId; _layerRefHandler; iModel; shouldDrapeLayer(layerTreeRef) { const mapLayerSettings = layerTreeRef?.layerSettings; if (mapLayerSettings && mapLayerSettings instanceof ModelMapLayerSettings) return ModelMapLayerDrapeTarget.IModel === mapLayerSettings.drapeTarget; return false; } constructor(view, model, planProjection, transformNodeId, sectionClip, backgroundBase, backgroundLayers) { super(); this.iModel = model.iModel; this._layerRefHandler = new LayerTileTreeReferenceHandler(this, false, backgroundBase, backgroundLayers, false); this.view = view; this.model = model; this._animationTransformNodeId = transformNodeId; this._sectionClip = sectionClip; this._viewFlagOverrides = { ...model.jsonProperties.viewFlagOverrides }; if (sectionClip) { // Clipping will be applied on backend; don't clip out cut geometry. this._viewFlagOverrides.clipVolume = false; this._sectionCutAppearanceProvider = FeatureAppearanceProvider.supplement((app) => { const cutApp = this.view.displayStyle.settings.clipStyle.cutStyle.appearance; return cutApp ? app.extendAppearance(cutApp) : app; }); } const scriptInfo = IModelApp.tileAdmin.getScriptInfoForTreeId(model.id, view.displayStyle[_scheduleScriptReference]); this._id = { modelId: model.id, is3d: model.is3d, treeId: this.createTreeId(view, model.id), isPlanProjection: planProjection, timeline: scriptInfo?.timeline, }; this._owner = primaryTreeSupplier.getOwner(this._id, model.iModel); } getAnimationTransformNodeId() { return this._animationTransformNodeId ?? AnimationNodeId.Untransformed; } getViewFlagOverrides(_tree) { return this._viewFlagOverrides; } getAppearanceProvider(_tree) { if (this._sectionCutAppearanceProvider && this.view.displayStyle.settings.clipStyle.cutStyle.appearance) return this._sectionCutAppearanceProvider; return undefined; } getHiddenLineSettings(_tree) { return this._sectionClip ? this.view.displayStyle.settings.clipStyle.cutStyle.hiddenLine : undefined; } get castsShadows() { return true; } get isPlanProjection() { return false; } discloseTileTrees(trees) { super.discloseTileTrees(trees); this._layerRefHandler.discloseTileTrees(trees); } getClipVolume(_tree) { // ###TODO: reduce frequency with which getModelClip() is called return this.view.is3d() && !this._sectionClip ? this.view.getModelClip(this.model.id) : undefined; } canSupplyToolTip() { return false; } createDrawArgs(context) { const args = super.createDrawArgs(context); if (args) args.intersectionClip = this._sectionClip; return args; } get treeOwner() { const newId = this.createTreeId(this.view, this._id.modelId); const timeline = IModelApp.tileAdmin.getScriptInfoForTreeId(this._id.modelId, this.view.displayStyle[_scheduleScriptReference])?.timeline; if (0 !== compareIModelTileTreeIds(newId, this._id.treeId) || timeline?.isEditingCommitted) { this._id = { modelId: this._id.modelId, is3d: this._id.is3d, treeId: newId, isPlanProjection: this._id.isPlanProjection, timeline, }; this._owner = primaryTreeSupplier.getOwner(this._id, this.model.iModel); } return this._owner; } createTreeId(view, modelId) { if (this._sectionClip) { // We do this each time in case the ClipStyle's overrides are modified. // ###TODO: can we avoid that? Event listeners maybe? this._viewFlagOverrides = { ...this.view.displayStyle.settings.clipStyle.cutStyle.viewflags, // Do not clip out the cut geometry intersecting the clip planes. clipVolume: false, // The cut geometry is planar - it should win a z-fight. // Also we need to preserve this flag if this is a plan projection tile tree reference. forceSurfaceDiscard: true, }; } const animationId = IModelApp.tileAdmin.getScriptInfoForTreeId(modelId, view.displayStyle[_scheduleScriptReference])?.animationId; const renderMode = this._viewFlagOverrides.renderMode ?? view.viewFlags.renderMode; const visibleEdges = this._viewFlagOverrides.visibleEdges ?? view.viewFlags.visibleEdges; const edgesRequired = visibleEdges || RenderMode.SmoothShade !== renderMode || IModelApp.tileAdmin.alwaysRequestEdges; const edges = edgesRequired ? IModelApp.tileAdmin.edgeOptions : false; const sectionCut = this._sectionClip?.clipString; const disablePolyfaceDecimation = IModelApp.tileAdmin.disablePolyfaceDecimation; return { type: BatchType.Primary, edges, animationId, sectionCut, disablePolyfaceDecimation }; } computeBaseTransform(tree) { return super.computeTransform(tree); } computeTransform(tree) { const baseTf = this.computeBaseTransform(tree); const displayTf = this.view.modelDisplayTransformProvider?.getModelDisplayTransform(this.model.id); if (!displayTf) return baseTf; return displayTf.premultiply ? displayTf.transform.multiplyTransformTransform(baseTf) : baseTf.multiplyTransformTransform(displayTf.transform); } addToScene(context) { const tree = this.treeOwner.load(); if (undefined === tree || !this._layerRefHandler.initializeLayers(context)) return; // Not loaded yet. super.addToScene(context); } } export class AnimatedTreeReference extends PrimaryTreeReference { _branchId; constructor(view, model, transformNodeId) { super(view, model, false, transformNodeId); this._branchId = formatAnimationBranchId(model.id, transformNodeId); } computeBaseTransform(tree) { const tf = super.computeBaseTransform(tree); const style = this.view.displayStyle; const script = style.scheduleScript; if (undefined === script || undefined === this._animationTransformNodeId) return tf; const timePoint = style.settings.timePoint ?? script.duration.low; const animTf = script.getTransform(this._id.modelId, this._animationTransformNodeId, timePoint); if (animTf) animTf.multiplyTransformTransform(tf, tf); return tf; } createDrawArgs(context) { const animBranch = context.viewport.target.animationBranches?.branchStates.get(this._branchId); if (animBranch && animBranch.omit) return undefined; const args = super.createDrawArgs(context); if (args?.tree && undefined !== this._animationTransformNodeId) { assert(args.tree instanceof IModelTileTree); args.boundingRange = args.tree.getTransformNodeRange(this._animationTransformNodeId); } return args; } } class PlanProjectionTreeReference extends PrimaryTreeReference { get _view3d() { return this.view; } _baseTransform = Transform.createIdentity(); constructor(view, model, sectionCut, backgroundBase, backgroundLayers) { super(view, model, true, undefined, sectionCut, backgroundBase, backgroundLayers); this._viewFlagOverrides.forceSurfaceDiscard = true; } get castsShadows() { return false; } get isPlanProjection() { return true; } createDrawArgs(context) { const args = super.createDrawArgs(context); if (undefined !== args && this._id.treeId.enforceDisplayPriority) { args.drawGraphics = () => { const graphics = args.produceGraphics(); if (undefined !== graphics) { const settings = this.getSettings(); const asOverlay = undefined !== settings && settings.overlay; const transparency = settings?.transparency || 0; let elevation = settings?.elevation; if (undefined === elevation) { const tree = this.treeOwner.tileTree; if (tree) { assert(tree instanceof PlanProjectionTileTree); elevation = tree.baseElevation; } else { elevation = 0; } } context.outputGraphic(context.target.renderSystem.createGraphicLayerContainer(graphics, asOverlay, transparency, elevation)); } }; } return args; } computeBaseTransform(tree) { assert(tree instanceof PlanProjectionTileTree); const transform = tree.iModelTransform.clone(this._baseTransform); const elevation = this.getSettings()?.elevation; if (undefined !== elevation) transform.origin.z = elevation; return transform; } draw(args) { const settings = this.getSettings(); if (undefined === settings || settings.enforceDisplayPriority || !settings.overlay) super.draw(args); else args.context.withGraphicType(TileGraphicType.Overlay, () => args.tree.draw(args)); } getSettings() { return this._view3d.getDisplayStyle3d().settings.getPlanProjectionSettings(this.model.id); } createTreeId(view, modelId) { const id = super.createTreeId(view, modelId); const settings = this.getSettings(); if (undefined !== settings && settings.enforceDisplayPriority) id.enforceDisplayPriority = true; return id; } } function isPlanProjection(view, model) { const model3d = view.is3d() ? model.asGeometricModel3d : undefined; return undefined !== model3d && model3d.isPlanProjection; } function createTreeRef(view, model, sectionCut, backgroundBase, backgroundLayers) { if (false !== IModelApp.renderSystem.options.planProjections && isPlanProjection(view, model)) return new PlanProjectionTreeReference(view, model, sectionCut, backgroundBase, backgroundLayers); return new PrimaryTreeReference(view, model, false, undefined, sectionCut, backgroundBase, backgroundLayers); } export function createPrimaryTileTreeReference(view, model, getBackgroundBase, getBackgroundLayers) { const backgroundBase = getBackgroundBase?.(); const backgroundLayers = getBackgroundLayers?.(); return createTreeRef(view, model, undefined, backgroundBase, backgroundLayers); } class MaskTreeReference extends TileTreeReference { _id; _owner; model; get castsShadows() { return false; } constructor(view, model) { super(); this.model = model; this._id = { modelId: model.id, is3d: model.is3d, treeId: this.createTreeId(), isPlanProjection: isPlanProjection(view, model), }; this._owner = primaryTreeSupplier.getOwner(this._id, model.iModel); } get treeOwner() { const newId = this.createTreeId(); if (0 !== compareIModelTileTreeIds(newId, this._id.treeId)) { this._id = { modelId: this._id.modelId, is3d: this._id.is3d, treeId: newId, isPlanProjection: false }; this._owner = primaryTreeSupplier.getOwner(this._id, this.model.iModel); } return this._owner; } createTreeId() { return { type: BatchType.Primary, edges: false, disablePolyfaceDecimation: IModelApp.tileAdmin.disablePolyfaceDecimation }; } } export function createMaskTreeReference(view, model) { return new MaskTreeReference(view, model); } export class ModelMapLayerTileTreeReference extends MapLayerTileTreeReference { _classifier; _source; _id; _owner; get isPlanar() { return true; } get activeClassifier() { return this._classifier; } constructor(layerSettings, _classifier, layerIndex, iModel, _source) { super(layerSettings, layerIndex, iModel); this._classifier = _classifier; this._source = _source; this._id = { modelId: _classifier.modelId, is3d: true, // model.is3d, treeId: this.createTreeId(), isPlanProjection: false, // isPlanProjection(view, model), }; this._owner = primaryTreeSupplier.getOwner(this._id, this.iModel); } createTreeId() { return { type: BatchType.Primary, edges: false, disablePolyfaceDecimation: IModelApp.tileAdmin.disablePolyfaceDecimation }; } get treeOwner() { const newId = this.createTreeId(); if (0 !== compareIModelTileTreeIds(newId, this._id.treeId)) { this._id = { modelId: this._id.modelId, is3d: this._id.is3d, treeId: newId, isPlanProjection: false }; this._owner = primaryTreeSupplier.getOwner(this._id, this.iModel); } return this._owner; } get viewFlags() { return { renderMode: RenderMode.SmoothShade, transparency: true, // Igored for point clouds as they don't support transparency. textures: true, lighting: false, shadows: false, monochrome: false, materials: false, ambientOcclusion: false, visibleEdges: true, hiddenEdges: false, fill: true, }; } } export function createModelMapLayerTileTreeReference(layerSettings, layerIndex, iModel) { const classifier = SpatialClassifier.fromModelMapLayer(layerSettings); return classifier ? new ModelMapLayerTileTreeReference(layerSettings, classifier, layerIndex, iModel) : undefined; } /** @internal */ export function collectMaskRefs(view, modelIds, excludedModelIds, maskTreeRefs, maskRange) { for (const modelId of modelIds) { if (!excludedModelIds?.has(modelId)) { const model = view.iModel.models.getLoaded(modelId); assert(model !== undefined); // Models should be loaded by RealityModelTileTree if (model?.asGeometricModel) { const treeRef = createMaskTreeReference(view, model.asGeometricModel); maskTreeRefs.push(treeRef); const range = treeRef.computeWorldContentRange(); maskRange.extendRange(range); } } } } /** Provides [[TileTreeReference]]s for the loaded models present in a [[SpatialViewState]]'s [[ModelSelectorState]] and * not present in the optionally-supplied exclusion list. * @internal */ export function createSpatialTileTreeReferences(view, excludedModels) { return new SpatialRefs(view, excludedModels); } /** Provides [[TileTreeReference]]s for the loaded models present in a [[SpatialViewState]]'s [[ModelSelectorState]]. * @internal */ export var SpatialTileTreeReferences; (function (SpatialTileTreeReferences) { /** Create a SpatialTileTreeReferences object reflecting the contents of the specified view. */ function create(view) { return createSpatialTileTreeReferences(view); } SpatialTileTreeReferences.create = create; })(SpatialTileTreeReferences || (SpatialTileTreeReferences = {})); /** Represents the [[TileTreeReference]]s associated with one model in a [[SpatialTileTreeReferences]]. */ class SpatialModelRefs { /** The TileTreeReference representing the model's primary content. */ _modelRef; /** TileTreeReferences representing nodes transformed by the view's schedule script. */ _animatedRefs = []; /** TileTreeReference providing cut geometry intersecting the view's clip volume. */ _sectionCutRef; /** Whether `this._modelRef` is a [[PrimaryTreeReference]] (as opposed to, e.g., a reality model tree reference). */ _isPrimaryRef; /** Used to mark refs as excluded so that only their _sectionCutRef is returned by the iterator. */ _isExcluded; constructor(model, view, excluded) { this._modelRef = model.createTileTreeReference(view); this._isPrimaryRef = this._modelRef instanceof PrimaryTreeReference; this._isExcluded = excluded; } *[Symbol.iterator]() { if ((!this._primaryRef || !this._primaryRef.deactivated) && !this._isExcluded) yield this._modelRef; for (const animated of this._animatedRefs) if (!animated.deactivated) yield animated; if (this._sectionCutRef && !this._sectionCutRef.deactivated) yield this._sectionCutRef; } updateAnimated(script) { const ref = this._primaryRef; if (!ref || this._isExcluded) return; this._animatedRefs.length = 0; const nodeIds = script?.script.getTransformBatchIds(ref.model.id); if (nodeIds) for (const nodeId of nodeIds) this._animatedRefs.push(new AnimatedTreeReference(ref.view, ref.model, nodeId)); } updateSectionCut(clip) { const ref = this._primaryRef; if (!ref) { assert(undefined === this._sectionCutRef); return; } // If the clip isn't supposed to apply to this model, don't produce cut geometry. const vfJson = clip ? ref.model.jsonProperties.viewFlagOverrides : undefined; const vfOvrs = vfJson ? { ...vfJson } : undefined; if (vfOvrs && !vfOvrs.clipVolume) clip = undefined; this._sectionCutRef = clip ? createTreeRef(ref.view, ref.model, clip) : undefined; } setDeactivated(deactivated, which) { if (typeof which !== "string") { for (const index of which) if (this._animatedRefs[index]) this._animatedRefs[index].deactivated = deactivated ?? !this._animatedRefs[index].deactivated; return; } if (("all" === which || "primary" === which) && this._primaryRef) this._primaryRef.deactivated = deactivated ?? !this._primaryRef.deactivated; if (("all" === which || "section" === which) && this._sectionCutRef) this._sectionCutRef.deactivated = deactivated ?? !this._sectionCutRef.deactivated; if (("all" === which || "animated" === which)) for (const ref of this._animatedRefs) ref.deactivated = deactivated ?? !ref.deactivated; } get _primaryRef() { if (!this._isPrimaryRef) return undefined; assert(this._modelRef instanceof PrimaryTreeReference); return this._modelRef; } } /** Provides [[TileTreeReference]]s for the loaded models present in a [[SpatialViewState]]'s [[ModelSelectorState]]. */ class SpatialRefs { _allLoaded = false; _view; _excludedModels; _refs = new Map(); _swapRefs = new Map(); _sectionCutOnlyRefs = new Map(); _swapSectionCutOnlyRefs = new Map(); _scheduleScript; _sectionCut; constructor(view, excludedModels) { this._view = view; this._scheduleScript = view.displayStyle[_scheduleScriptReference]; this._sectionCut = this.getSectionCutFromView(); if (excludedModels) this._excludedModels = new Set(excludedModels); } update() { this._allLoaded = false; } attachToViewport() { } detachFromViewport() { } *[Symbol.iterator]() { this.load(); for (const modelRef of this._refs.values()) for (const ref of modelRef) yield ref; if (this._sectionCut) { for (const modelRef of this._sectionCutOnlyRefs.values()) for (const ref of modelRef) yield ref; } } setDeactivated(modelIds, deactivated, refs) { if (undefined === modelIds) { for (const model of this._refs.values()) model.setDeactivated(deactivated, refs); return; } if (typeof modelIds === "string") modelIds = [modelIds]; for (const modelId of modelIds) this._refs.get(modelId)?.setDeactivated(deactivated, refs); } /** For getting the [TileTreeReference]s that are in the modelIds, for planar classification. * @param modelIds modelIds for which to get the TileTreeReferences * @param maskTreeRefs where to store the TileTreeReferences * @param maskRange range to extend for the maskRefs */ collectMaskRefs(modelIds, maskTreeRefs, maskRange) { collectMaskRefs(this._view, modelIds, this._excludedModels, maskTreeRefs, maskRange); } /** For getting a list of modelIds which do not participate in masking, for planar classification. * For non-batched tile trees this is not needed, so just return undefined. */ getModelsNotInMask(_maskModels, _useVisible) { return undefined; } load() { if (!this._allLoaded) { this._allLoaded = true; this.updateModels(); } const curScript = this._view.displayStyle[_scheduleScriptReference]; const prevScript = this._scheduleScript; if (curScript !== prevScript) { this._scheduleScript = curScript; if (!curScript || !prevScript || !curScript.script.equals(prevScript.script)) for (const ref of this._refs.values()) ref.updateAnimated(curScript); } const sectionCut = this.getSectionCutFromView(); if (sectionCut?.clipString !== this._sectionCut?.clipString) { this._sectionCut = sectionCut; for (const ref of this._refs.values()) ref.updateSectionCut(sectionCut); for (const ref of this._sectionCutOnlyRefs.values()) ref.updateSectionCut(sectionCut); } } getSectionCutFromView() { const wantCut = this._view.viewFlags.clipVolume && this._view.displayStyle.settings.clipStyle.produceCutGeometry; const clip = wantCut ? this._view.getViewClip() : undefined; return StringifiedClipVector.fromClipVector(clip); } /** Ensure this._refs contains a SpatialModelRefs for all loaded models in the model selector. */ updateModels() { let prev = this._refs; let cur = this._swapRefs; this._refs = cur; this._swapRefs = prev; cur.clear(); prev = this._sectionCutOnlyRefs; cur = this._swapSectionCutOnlyRefs; this._sectionCutOnlyRefs = cur; this._swapSectionCutOnlyRefs = prev; cur.clear(); for (const modelId of this._view.modelSelector.models) { let excluded = false; if (undefined !== this._excludedModels && this._excludedModels.has(modelId)) { excluded = true; cur = this._sectionCutOnlyRefs; prev = this._swapSectionCutOnlyRefs; } else { cur = this._refs; prev = this._swapRefs; } let modelRefs = prev.get(modelId); if (!modelRefs) { const model = this._view.iModel.models.getLoaded(modelId)?.asGeometricModel3d; if (model) { modelRefs = new SpatialModelRefs(model, this._view, excluded); modelRefs.updateAnimated(this._scheduleScript); modelRefs.updateSectionCut(this._sectionCut); } } if (modelRefs) cur.set(modelId, modelRefs); } } } //# sourceMappingURL=PrimaryTileTree.js.map