@itwin/core-frontend
Version:
iTwin.js frontend components
419 lines • 18.4 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Views
*/
import { assert, dispose, Id64 } from "@itwin/core-bentley";
import { Frustum, QueryRowFormat, } from "@itwin/core-common";
import { Constant, Range3d, Transform } from "@itwin/core-geometry";
import { CategorySelectorState } from "./CategorySelectorState";
import { CoordSystem } from "./CoordSystem";
import { DisplayStyle2dState } from "./DisplayStyleState";
import { Frustum2d } from "./Frustum2d";
import { IModelApp } from "./IModelApp";
import { FeatureSymbology } from "./render/FeatureSymbology";
import { GraphicBranch } from "./render/GraphicBranch";
import { MockRender } from "./internal/render/MockRender";
import { TileGraphicType } from "./tile/internal";
import { OffScreenViewport } from "./Viewport";
import { ViewRect } from "./common/ViewRect";
import { ViewState2d, ViewState3d } from "./ViewState";
/** The information required to instantiate a [[SectionAttachment]]. This information is supplied to DrawingViewState constructor via ViewStateProps.
* The spatial view is obtained asynchronously in DrawingViewState.load(). The SectionAttachment is created in DrawingViewState.attachToViewport and
* disposed of in DrawingViewState.detachFromViewport.
*/
class SectionAttachmentInfo {
_spatialView;
_drawingToSpatialTransform;
_displaySpatialView;
get spatialView() { return this._spatialView; }
get wantDisplayed() {
return this._displaySpatialView || DrawingViewState.alwaysDisplaySpatialView;
}
constructor(spatialView, drawingToSpatialTransform, displaySpatialView) {
this._spatialView = spatialView;
this._drawingToSpatialTransform = drawingToSpatialTransform;
this._displaySpatialView = displaySpatialView;
}
static fromJSON(props) {
if (!props)
return new SectionAttachmentInfo(Id64.invalid, Transform.createIdentity(), false);
return new SectionAttachmentInfo(props.spatialView, Transform.fromJSON(props.drawingToSpatialTransform), true === props.displaySpatialView);
}
toJSON() {
if ("string" === typeof this._spatialView && !Id64.isValidId64(this._spatialView))
return undefined;
return {
spatialView: (this._spatialView instanceof ViewState3d) ? this._spatialView.id : this._spatialView,
drawingToSpatialTransform: this._drawingToSpatialTransform.isIdentity ? undefined : this._drawingToSpatialTransform.toJSON(),
displaySpatialView: this._displaySpatialView,
};
}
clone(iModel) {
let spatialView = this._spatialView;
if (spatialView instanceof ViewState3d)
spatialView = spatialView.clone(iModel);
return new SectionAttachmentInfo(spatialView, this._drawingToSpatialTransform, this._displaySpatialView);
}
preload(options) {
if (!this.wantDisplayed)
return;
if (this._spatialView instanceof ViewState3d)
return;
if (!Id64.isValidId64(this._spatialView))
return;
options.spatialViewId = this._spatialView;
options.viewStateLoadProps = {
displayStyle: {
omitScheduleScriptElementIds: !IModelApp.tileAdmin.enableFrontendScheduleScripts,
compressExcludedElementIds: true,
},
};
}
async load(iModel) {
if (!this.wantDisplayed)
return;
if (this._spatialView instanceof ViewState3d)
return;
if (!Id64.isValidId64(this._spatialView))
return;
const spatialView = await iModel.views.load(this._spatialView);
if (spatialView instanceof ViewState3d)
this._spatialView = spatialView;
}
async postload(options, iModel) {
let spatialView;
if (options.spatialViewProps) {
spatialView = await iModel.views.convertViewStatePropsToViewState(options.spatialViewProps);
}
if (spatialView instanceof ViewState3d)
this._spatialView = spatialView;
}
createAttachment(toSheet) {
if (!this.wantDisplayed || !(this._spatialView instanceof ViewState3d))
return undefined;
const spatialToDrawing = this._drawingToSpatialTransform.inverse();
return spatialToDrawing ? new SectionAttachment(this._spatialView, spatialToDrawing, this._drawingToSpatialTransform, toSheet) : undefined;
}
get sectionDrawingInfo() {
return {
drawingToSpatialTransform: this._drawingToSpatialTransform,
spatialView: this._spatialView instanceof ViewState3d ? this._spatialView.id : this._spatialView,
};
}
}
/** A mostly no-op [[RenderTarget]] for a [[SectionAttachment]]. It allocates no webgl resources. */
class SectionTarget extends MockRender.OffScreenTarget {
_attachment;
constructor(attachment) {
super(IModelApp.renderSystem, new ViewRect(0, 0, 1, 1));
this._attachment = attachment;
}
changeScene(scene) {
this._attachment.scene = scene;
}
overrideFeatureSymbology(ovrs) {
this._attachment.symbologyOverrides = ovrs;
}
}
/** Draws the contents of an orthographic [[ViewState3d]] directly into a [[DrawingViewState]], if the associated [SectionDrawing]($backend)
* specifies it should be. We select tiles for the view in the context of a lightweight offscreen viewport with a no-op [[RenderTarget]], then
* add the resultant graphics to the drawing view's scene. The attachment is created in DrawingViewState.attachToViewport and disposed of in
* DrawingViewState.detachFromViewport.
*/
class SectionAttachment {
_viewFlagOverrides;
toDrawing;
_fromDrawing;
_viewRect = new ViewRect(0, 0, 1, 1);
_originalFrustum = new Frustum();
_drawingExtents;
viewport;
_branchOptions;
scene;
symbologyOverrides;
get view() {
assert(this.viewport.view instanceof ViewState3d);
return this.viewport.view;
}
get zDepth() {
return this._drawingExtents.z;
}
get drawingRange() {
const frustum3d = this._originalFrustum.transformBy(this.toDrawing);
return frustum3d.toRange();
}
constructor(view, toDrawing, fromDrawing, toSheet) {
// Save the input for clone(). Attach a copy to the viewport.
this.toDrawing = toDrawing;
this._fromDrawing = fromDrawing;
this.viewport = OffScreenViewport.createViewport(view, new SectionTarget(this), true);
this.symbologyOverrides = new FeatureSymbology.Overrides(view);
let clipVolume;
let clip = this.view.getViewClip();
if (clip) {
clip = clip.clone();
const clipTransform = toSheet ? toSheet.multiplyTransformTransform(this.toDrawing) : this.toDrawing;
clip.transformInPlace(clipTransform);
clipVolume = IModelApp.renderSystem.createClipVolume(clip);
}
this._branchOptions = {
clipVolume,
hline: view.getDisplayStyle3d().settings.hiddenLineSettings,
inSectionDrawingAttachment: true,
frustum: {
is3d: true,
scale: { x: 1, y: 1 },
},
contours: view.getDisplayStyle3d().settings.contours
};
this._viewFlagOverrides = { ...view.viewFlags, lighting: false, shadows: false };
// Save off the original frustum (potentially adjusted by viewport).
this.viewport.setupFromView();
this.viewport.viewingSpace.getFrustum(CoordSystem.World, true, this._originalFrustum);
const drawingFrustum = this._originalFrustum.transformBy(this.toDrawing);
const drawingRange = drawingFrustum.toRange();
this._drawingExtents = drawingRange.diagonal();
this._drawingExtents.z = Math.abs(this._drawingExtents.z);
}
[Symbol.dispose]() {
this.viewport[Symbol.dispose]();
}
addToScene(context) {
if (context.viewport.freezeScene)
return;
const pixelSize = context.viewport.getPixelSizeAtPoint();
if (0 === pixelSize)
return;
// Adjust offscreen viewport's frustum based on intersection with drawing view frustum.
const frustum3d = this._originalFrustum.transformBy(this.toDrawing);
const frustumRange3d = frustum3d.toRange();
const frustum2d = context.viewport.getWorldFrustum();
const frustumRange2d = frustum2d.toRange();
const intersect = frustumRange3d.intersect(frustumRange2d);
if (intersect.isNull)
return;
frustum3d.initFromRange(intersect);
frustum3d.transformBy(this._fromDrawing, frustum3d);
this.viewport.setupViewFromFrustum(frustum3d);
// Adjust view rect based on size of attachment on screen so tiles of appropriate LOD are selected.
const width = this._drawingExtents.x * intersect.xLength() / frustumRange3d.xLength();
const height = this._drawingExtents.y * intersect.yLength() / frustumRange3d.yLength();
this._viewRect.width = Math.max(1, Math.round(width / pixelSize));
this._viewRect.height = Math.max(1, Math.round(height / pixelSize));
this.viewport.setRect(this._viewRect);
// Propagate settings from drawing viewport.
this.viewport.debugBoundingBoxes = context.viewport.debugBoundingBoxes;
this.viewport.setTileSizeModifier(context.viewport.tileSizeModifier);
// Create the scene.
this.viewport.renderFrame();
const scene = this.scene;
if (!scene)
return;
// Extract graphics and insert into drawing's scene context.
const outputGraphics = (source) => {
if (0 === source.length)
return;
const graphics = new GraphicBranch();
graphics.setViewFlagOverrides(this._viewFlagOverrides);
graphics.symbologyOverrides = this.symbologyOverrides;
for (const graphic of source)
graphics.entries.push(graphic);
const branch = context.createGraphicBranch(graphics, this.toDrawing, this._branchOptions);
context.outputGraphic(branch);
};
outputGraphics(scene.foreground);
context.withGraphicType(TileGraphicType.BackgroundMap, () => outputGraphics(scene.background));
context.withGraphicType(TileGraphicType.Overlay, () => outputGraphics(scene.overlay));
// Report tile statistics to drawing viewport.
const tileAdmin = IModelApp.tileAdmin;
const selectedAndReady = tileAdmin.getTilesForUser(this.viewport);
const requested = tileAdmin.getRequestsForUser(this.viewport);
tileAdmin.addExternalTilesForUser(context.viewport, {
requested: requested?.size ?? 0,
selected: selectedAndReady?.selected.size ?? 0,
ready: selectedAndReady?.ready.size ?? 0,
});
}
}
/** A view of a [DrawingModel]($backend)
* @public
* @extensions
*/
export class DrawingViewState extends ViewState2d {
static get className() { return "DrawingViewDefinition"; }
/** Exposed strictly for testing and debugging. Indicates that when loading the view, the spatial view should be displayed even
* if `SectionDrawing.displaySpatialView` is not `true`.
* @internal
*/
static alwaysDisplaySpatialView = false;
/** Exposed strictly for testing and debugging. Indicates that the 2d graphics should not be displayed.
* @internal
*/
static hideDrawingGraphics = false;
_viewedExtents;
_attachmentInfo;
_attachment;
/** Strictly for testing. @internal */
get sectionDrawingProps() {
return this._attachmentInfo.toJSON();
}
/** Strictly for testing. @internal */
get sectionDrawingInfo() {
return this._attachmentInfo.sectionDrawingInfo;
}
/** Strictly for testing. @internal */
get attachment() {
return this._attachment;
}
/** Strictly for testing. @internal */
get attachmentInfo() {
return this._attachmentInfo;
}
constructor(props, iModel, categories, displayStyle, extents, sectionDrawing) {
super(props, iModel, categories, displayStyle);
if (categories instanceof DrawingViewState) {
this._viewedExtents = categories._viewedExtents.clone();
this._attachmentInfo = categories._attachmentInfo.clone(iModel);
}
else {
this._viewedExtents = extents;
this._attachmentInfo = SectionAttachmentInfo.fromJSON(sectionDrawing);
}
}
/** See [[ViewState.attachToViewport]]. */
attachToViewport(args) {
super.attachToViewport(args);
assert(undefined === this._attachment);
this._attachment = this._attachmentInfo.createAttachment(args.drawingToSheetTransform);
}
/** See [[ViewState.detachFromViewport]]. */
detachFromViewport() {
super.detachFromViewport();
this._attachment = dispose(this._attachment);
}
async changeViewedModel(modelId) {
await super.changeViewedModel(modelId);
const props = await this.querySectionDrawingProps();
this._attachmentInfo = SectionAttachmentInfo.fromJSON(props);
// super.changeViewedModel() throws if attached to viewport, and attachment only allocated while attached to viewport
assert(undefined === this._attachment);
}
async querySectionDrawingProps() {
let spatialView = Id64.invalid;
let drawingToSpatialTransform;
let displaySpatialView = false;
try {
const ecsql = `
SELECT spatialView,
json_extract(jsonProperties, '$.drawingToSpatialTransform') as drawingToSpatialTransform,
CAST(json_extract(jsonProperties, '$.displaySpatialView') as BOOLEAN) as displaySpatialView
FROM bis.SectionDrawing
WHERE ECInstanceId=${this.baseModelId}`;
for await (const row of this.iModel.createQueryReader(ecsql, undefined, { rowFormat: QueryRowFormat.UseJsPropertyNames })) {
spatialView = Id64.fromJSON(row.spatialView?.id);
displaySpatialView = !!row.displaySpatialView;
try {
drawingToSpatialTransform = JSON.parse(row.drawingToSpatialTransform);
}
catch {
// We'll use identity transform.
}
break;
}
}
catch {
// The version of BisCore ECSchema in the iModel is probably too old to contain the SectionDrawing ECClass.
}
return { spatialView, displaySpatialView, drawingToSpatialTransform };
}
/** @internal */
preload(hydrateRequest) {
assert(!this.isAttachedToViewport);
super.preload(hydrateRequest);
this._attachmentInfo.preload(hydrateRequest);
}
/** @internal */
async postload(hydrateResponse) {
const promises = [];
promises.push(super.postload(hydrateResponse));
promises.push(this._attachmentInfo.postload(hydrateResponse, this.iModel));
await Promise.all(promises);
}
static createFromProps(props, iModel) {
const cat = new CategorySelectorState(props.categorySelectorProps, iModel);
const displayStyleState = new DisplayStyle2dState(props.displayStyleProps, iModel);
const extents = props.modelExtents ? Range3d.fromJSON(props.modelExtents) : new Range3d();
// use "new this" so subclasses are correct
return new this(props.viewDefinitionProps, iModel, cat, displayStyleState, extents, props.sectionDrawing);
}
toProps() {
const props = super.toProps();
props.modelExtents = this._viewedExtents.toJSON();
props.sectionDrawing = this._attachmentInfo.toJSON();
return props;
}
getViewedExtents() {
const extents = this._viewedExtents.clone();
if (this._attachment) {
extents.extendRange(this._attachment.drawingRange);
}
return extents;
}
get defaultExtentLimits() {
return {
min: Constant.oneMillimeter,
max: 3 * Constant.diameterOfEarth,
};
}
isDrawingView() { return true; }
/** See [[ViewState.getOrigin]]. */
getOrigin() {
const origin = super.getOrigin();
if (this._attachment)
origin.z = -this._attachment.zDepth;
return origin;
}
/** See [[ViewState.getExtents]]. */
getExtents() {
const extents = super.getExtents();
if (this._attachment)
extents.z = this._attachment.zDepth + Frustum2d.minimumZDistance;
return extents;
}
/** @internal */
discloseTileTrees(trees) {
super.discloseTileTrees(trees);
if (this._attachment)
trees.disclose(this._attachment.viewport);
}
/** @internal */
createScene(context) {
if (!DrawingViewState.hideDrawingGraphics)
super.createScene(context);
if (this._attachment)
this._attachment.addToScene(context);
}
get areAllTileTreesLoaded() {
return super.areAllTileTreesLoaded && (!this._attachment || this._attachment.view.areAllTileTreesLoaded);
}
/** @internal */
get secondaryViewports() {
return this._attachment ? [this._attachment.viewport] : super.secondaryViewports;
}
/** @internal */
getAttachmentViewport(args) {
const attach = args.inSectionDrawingAttachment ? this._attachment : undefined;
return attach?.viewport;
}
/** @beta */
computeDisplayTransform(args) {
// ###TODO we're currently ignoring model and element Id in args, assuming irrelevant for drawings.
// Should probably call super or have super call us.
const attach = args.inSectionDrawingAttachment ? this._attachment : undefined;
return attach?.toDrawing.clone(args.output);
}
}
//# sourceMappingURL=DrawingViewState.js.map