@itwin/core-frontend
Version:
iTwin.js frontend components
928 lines • 39.8 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, CompressedId64Set, dispose } from "@itwin/core-bentley";
import { Angle, ClipShape, ClipVector, Constant, Matrix3d, Point2d, Point3d, PolyfaceBuilder, Range2d, Range3d, StrokeOptions, Transform } from "@itwin/core-geometry";
import { ColorDef, Feature, FeatureTable, Frustum, Gradient, GraphicParams, PackedFeatureTable, Placement2d, TextureTransparency, } from "@itwin/core-common";
import { CategorySelectorState } from "./CategorySelectorState";
import { DisplayStyle2dState } from "./DisplayStyleState";
import { GraphicBranch } from "./render/GraphicBranch";
import { Frustum2d } from "./Frustum2d";
import { MockRender } from "./internal/render/MockRender";
import { FeatureSymbology } from "./render/FeatureSymbology";
import { IModelApp } from "./IModelApp";
import { CoordSystem } from "./CoordSystem";
import { OffScreenViewport } from "./Viewport";
import { ViewState2d } from "./ViewState";
import { createDefaultViewFlagOverrides, TileGraphicType } from "./tile/internal";
import { imageBufferToPngDataUrl, openImageDataUrlInNewWindow } from "./common/ImageUtil";
import { ViewRect } from "./common/ViewRect";
import { GraphicType } from "./common/render/GraphicType";
// cSpell:ignore ovrs
/** Describes the geometry and styling of a sheet border decoration.
* The sheet border decoration mimics a sheet of paper with a drop shadow.
*/
class SheetBorder {
_rect;
_shadow;
_gradient;
constructor(rect, shadow, gradient) {
this._rect = rect;
this._shadow = shadow;
this._gradient = gradient;
}
/** Create a new sheet border. If a context is supplied, points are transformed to view coordinates. */
static create(width, height, context) {
// Rect
const rect = [
Point3d.create(0, height),
Point3d.create(0, 0),
Point3d.create(width, 0),
Point3d.create(width, height),
Point3d.create(0, height)
];
if (context) {
context.viewport.worldToViewArray(rect);
}
// Shadow
const shadowWidth = .01 * Math.sqrt(width * width + height * height);
const shadow = [
Point3d.create(shadowWidth, 0),
Point3d.create(shadowWidth, -shadowWidth),
Point3d.create(width + shadowWidth, -shadowWidth),
Point3d.create(width + shadowWidth, height - shadowWidth),
Point3d.create(width, height - shadowWidth),
Point3d.create(width, 0),
Point3d.create(shadowWidth, 0),
];
if (context) {
context.viewport.worldToViewArray(shadow);
}
// Gradient
const gradient = new Gradient.Symb();
gradient.mode = Gradient.Mode.Linear;
gradient.angle = Angle.createDegrees(-45);
gradient.keys = [{ value: 0, color: ColorDef.from(25, 25, 25) }, { value: 0.5, color: ColorDef.from(150, 150, 150) }];
// Copy over points
const rect2d = [];
for (const point of rect)
rect2d.push(Point2d.createFrom(point));
const shadow2d = [];
for (const point of shadow)
shadow2d.push(Point2d.createFrom(point));
return new SheetBorder(rect2d, shadow2d, gradient);
}
getRange() {
const range = Range2d.createArray(this._rect);
const shadowRange = Range2d.createArray(this._shadow);
range.extendRange(shadowRange);
return range;
}
/** Add this border to the given GraphicBuilder. */
addToBuilder(builder) {
const lineColor = ColorDef.black;
const fillColor = ColorDef.black;
const params = new GraphicParams();
params.fillColor = fillColor;
params.gradient = this._gradient;
builder.activateGraphicParams(params);
builder.addShape2d(this._shadow, Frustum2d.minimumZDistance);
builder.setSymbology(lineColor, fillColor, 2);
builder.addLineString2d(this._rect, 0);
}
}
/** The information required to instantiate an ViewAttachments object to draw ViewAttachments into a sheet. The list of view attachment Ids is
* supplied to SheetViewState via the constructor. The corresponding ViewAttachmentProps for each attachment are obtained asynchronously in
* SheetViewState.load(). The Attachments object is created in SheetViewState.attachToViewport and disposed of in SheetViewState.detachFromViewport.
*/
class ViewAttachmentsInfo {
_attachments;
get attachments() { return this._attachments; }
constructor(attachments) {
this._attachments = attachments;
}
get isLoaded() {
return 0 === this._attachments.length || "string" !== typeof this._attachments[0];
}
get viewAttachmentProps() {
return this.isLoaded ? this._props : [];
}
get _props() {
assert(this.isLoaded);
return this._attachments;
}
get _ids() {
assert(!this.isLoaded);
return this._attachments;
}
static fromJSON(ids = []) {
return new ViewAttachmentsInfo(ids);
}
toJSON() {
return this.isLoaded ? this._props.map((x) => x.id) : [...this._ids];
}
clone(iModel) {
let attachments = this._attachments;
if (this.isLoaded) {
// Need to clone the attached ViewStates.
attachments = attachments.map((attachment) => {
assert(typeof attachment !== "string");
return {
...attachment,
attachedView: attachment.attachedView.clone(iModel),
};
});
}
return new ViewAttachmentsInfo(attachments);
}
preload(options) {
if (this.isLoaded)
return;
options.sheetViewAttachmentIds = CompressedId64Set.sortAndCompress(this._ids);
options.viewStateLoadProps = {
displayStyle: {
omitScheduleScriptElementIds: !IModelApp.tileAdmin.enableFrontendScheduleScripts,
compressExcludedElementIds: true,
},
};
}
async postload(options, iModel) {
if (options.sheetViewViews === undefined)
return;
if (options.sheetViewAttachmentProps === undefined)
return;
const viewStateProps = options.sheetViewViews; // This is viewstateProps, need to turn this into ViewState
const promises = [];
for (const viewProps of viewStateProps) {
const loadView = async () => {
try {
if (viewProps === undefined)
return undefined;
const view = await iModel.views.convertViewStatePropsToViewState(viewProps);
return view;
}
catch {
return undefined;
}
};
promises.push(loadView());
}
const views = await Promise.all(promises);
const attachmentProps = options.sheetViewAttachmentProps;
assert(views.length === attachmentProps.length);
const attachments = [];
for (let i = 0; i < views.length; i++) {
const view = views[i];
if (view && !(view instanceof SheetViewState)) {
const props = attachmentProps[i];
props.attachedView = view;
attachments.push(props);
}
}
this._attachments = attachments;
}
async load(iModel) {
if (this.isLoaded)
return;
const attachmentProps = await iModel.elements.getProps(this._ids);
const promises = [];
for (const attachment of attachmentProps) {
const loadView = async () => {
try {
const view = await iModel.views.load(attachment.view.id);
return view;
}
catch {
return undefined;
}
};
promises.push(loadView());
}
const views = await Promise.all(promises);
assert(views.length === attachmentProps.length);
const attachments = [];
for (let i = 0; i < views.length; i++) {
const view = views[i];
if (view && !(view instanceof SheetViewState)) {
const props = attachmentProps[i];
props.attachedView = view;
attachments.push(props);
}
}
this._attachments = attachments;
}
createAttachments(sheetView) {
return this.isLoaded ? new ViewAttachments(this._props, sheetView) : undefined;
}
}
/** The set of view attachments to be displayed in a Viewport via a SheetViewState. Allocated when the view becomes attached to a Viewport;
* disposed of when it becomes detached from the viewport.
*/
class ViewAttachments {
_attachments = [];
maxDepth = Frustum2d.minimumZDistance;
constructor(infos, sheetView) {
for (const info of infos) {
const drawAsRaster = info.jsonProperties?.displayOptions?.drawAsRaster || (info.attachedView.is3d() && info.attachedView.isCameraOn);
const ctor = drawAsRaster ? RasterAttachment : OrthographicAttachment;
const attachment = new ctor(info.attachedView, info, sheetView);
this._attachments.push(attachment);
this.maxDepth = Math.max(this.maxDepth, attachment.zDepth);
}
}
[Symbol.dispose]() {
for (const attachment of this._attachments)
attachment[Symbol.dispose]();
this._attachments.length = 0;
}
[Symbol.iterator]() {
return this._attachments[Symbol.iterator]();
}
/** For tests. */
get attachments() {
return this._attachments;
}
get isEmpty() {
return 0 === this._attachments.length;
}
areAllTileTreesLoaded(displayedExtents) {
return this._attachments.every((x) => {
const placement = Placement2d.fromJSON(x.viewAttachmentProps.placement);
const attachmentRange = placement.calculateRange();
if (!attachmentRange.intersectsRangeXY(displayedExtents))
return true;
return x.areAllTileTreesLoaded;
});
}
/** Strictly for testing purposes */
areAllAttachmentsLoaded() {
return this._attachments.every((attachment) => attachment.areAllTileTreesLoaded);
}
discloseTileTrees(trees) {
for (const attachment of this._attachments)
trees.disclose(attachment);
}
collectStatistics(stats) {
for (const attachment of this._attachments)
attachment.collectStatistics(stats);
}
addToScene(context) {
for (const attachment of this._attachments)
attachment.addToScene(context);
}
findById(attachmentId) {
return this._attachments.find((attachment) => attachment.viewAttachmentProps.id === attachmentId);
}
}
/** A view of a [SheetModel]($backend).
* @public
* @extensions
*/
export class SheetViewState extends ViewState2d {
/** The width and height of the sheet in world coordinates. */
sheetSize;
_attachmentsInfo;
_attachments;
_viewedExtents;
get attachmentIds() {
return this._attachmentsInfo.toJSON();
}
static get className() { return "SheetViewDefinition"; }
static createFromProps(viewStateData, iModel) {
const cat = new CategorySelectorState(viewStateData.categorySelectorProps, iModel);
const displayStyleState = new DisplayStyle2dState(viewStateData.displayStyleProps, iModel);
// use "new this" so subclasses are correct
return new this(viewStateData.viewDefinitionProps, iModel, cat, displayStyleState, viewStateData.sheetProps, viewStateData.sheetAttachments);
}
toProps() {
const props = super.toProps();
props.sheetAttachments = this._attachmentsInfo.toJSON();
// For sheetProps all that is actually used is the size, so just null out everything else.
const codeProps = { spec: "", scope: "", value: "" };
props.sheetProps = {
model: "",
code: codeProps,
classFullName: "",
width: this.sheetSize.x,
height: this.sheetSize.y,
scale: 1,
};
return props;
}
/** Strictly for testing. @internal */
get viewAttachmentProps() {
return this._attachmentsInfo.viewAttachmentProps.map((x) => {
return {
...x,
attachedView: undefined,
};
});
}
/** Strictly for testing. @internal */
get viewAttachmentInfos() {
return this._attachmentsInfo.attachments;
}
/** Strictly for testing. @internal */
get attachments() {
return this._attachments?.attachments;
}
isDrawingView() { return false; }
isSheetView() { return true; }
constructor(props, iModel, categories, displayStyle, sheetProps, attachments) {
super(props, iModel, categories, displayStyle);
if (categories instanceof SheetViewState) {
// we are coming from clone...
this.sheetSize = categories.sheetSize.clone();
this._attachmentsInfo = categories._attachmentsInfo.clone(iModel);
this._viewedExtents = categories._viewedExtents.clone();
}
else {
this.sheetSize = Point2d.create(sheetProps.width, sheetProps.height);
this._attachmentsInfo = ViewAttachmentsInfo.fromJSON(attachments);
const extents = new Range3d(0, 0, 0, this.sheetSize.x, this.sheetSize.y, 0);
const margin = 1.1;
extents.scaleAboutCenterInPlace(margin);
this._viewedExtents = extents;
}
}
getOrigin() {
const origin = super.getOrigin();
if (this._attachments)
origin.z = -this._attachments.maxDepth;
return origin;
}
getExtents() {
const extents = super.getExtents();
if (this._attachments)
extents.z = this._attachments.maxDepth + Frustum2d.minimumZDistance;
return extents;
}
/** Overrides [[ViewState.discloseTileTrees]] to include tile trees associated with [ViewAttachment]($backend)s displayed on this sheet. */
discloseTileTrees(trees) {
super.discloseTileTrees(trees);
if (this._attachments)
trees.disclose(this._attachments);
}
/** @internal */
collectNonTileTreeStatistics(stats) {
super.collectNonTileTreeStatistics(stats);
if (this._attachments)
this._attachments.collectStatistics(stats);
}
get defaultExtentLimits() {
return { min: Constant.oneMillimeter, max: this.sheetSize.magnitude() * 10 };
}
getViewedExtents() {
return this._viewedExtents;
}
/** @internal */
preload(hydrateRequest) {
super.preload(hydrateRequest);
this._attachmentsInfo.preload(hydrateRequest);
}
/** @internal */
async postload(hydrateResponse) {
const promises = [];
promises.push(super.postload(hydrateResponse));
promises.push(this._attachmentsInfo.postload(hydrateResponse, this.iModel));
await Promise.all(promises);
}
/** @internal */
createScene(context) {
super.createScene(context);
if (this._attachments)
this._attachments.addToScene(context);
}
/** @internal */
get secondaryViewports() {
const attachments = this._attachments;
if (!attachments)
return super.secondaryViewports;
function* iterator() {
for (const attachment of attachments) {
const vp = attachment.viewport;
if (vp)
yield vp;
}
}
return {
[Symbol.iterator]: () => iterator(),
};
}
/** @internal */
async queryAttachmentIds() {
const ecsql = `SELECT ECInstanceId as attachmentId FROM bis.ViewAttachment WHERE model.Id=${this.baseModelId}`;
const ids = [];
for await (const row of this.iModel.createQueryReader(ecsql))
ids.push(row[0]);
return ids;
}
async changeViewedModel(modelId) {
await super.changeViewedModel(modelId);
const attachmentIds = await this.queryAttachmentIds();
this._attachmentsInfo = ViewAttachmentsInfo.fromJSON(attachmentIds);
assert(undefined === this._attachments);
}
/** See [[ViewState.attachToViewport]]. */
attachToViewport(args) {
super.attachToViewport(args);
assert(undefined === this._attachments);
this._attachments = this._attachmentsInfo.createAttachments(this);
}
/** See [[ViewState.detachFromViewport]]. */
detachFromViewport() {
super.detachFromViewport();
this._attachments = dispose(this._attachments);
}
get areAllTileTreesLoaded() {
let displayedExtents = this._viewedExtents;
const frustum = this.calculateFrustum();
if (frustum) {
displayedExtents = frustum.toRange();
}
return super.areAllTileTreesLoaded && (!this._attachments || this._attachments.areAllTileTreesLoaded(displayedExtents));
}
/** @internal Strictly for testing */
areAllAttachmentsLoaded() {
if (this._attachments) {
return this._attachments.areAllAttachmentsLoaded();
}
return true;
}
/** Create a sheet border decoration graphic. */
createBorder(width, height, context) {
const border = SheetBorder.create(width, height, context);
const builder = context.createGraphicBuilder(GraphicType.ViewBackground);
border.addToBuilder(builder);
return builder.finish();
}
/** @internal */
decorate(context) {
super.decorate(context);
if (this.sheetSize !== undefined) {
const border = this.createBorder(this.sheetSize.x, this.sheetSize.y, context);
context.setViewBackground(border);
}
}
computeFitRange() {
const size = this.sheetSize;
if (0 >= size.x || 0 >= size.y)
return super.computeFitRange();
return new Range3d(0, 0, -1, size.x, size.y, 1);
}
/** @internal */
getAttachmentViewport(args) {
const attachment = args.viewAttachmentId ? this._attachments?.findById(args.viewAttachmentId) : undefined;
if (!attachment) {
return undefined;
}
return args.inSectionDrawingAttachment ? attachment.viewport?.view.getAttachmentViewport({ inSectionDrawingAttachment: true }) : attachment.viewport;
}
/** @beta */
computeDisplayTransform(args) {
// ###TODO we're currently ignoring model and element Id in args, assuming irrelevant for sheets.
// Should probably call super or have super call us.
const attachment = undefined !== args.viewAttachmentId ? this._attachments?.findById(args.viewAttachmentId) : undefined;
if (!attachment || !(attachment instanceof OrthographicAttachment)) {
return undefined;
}
const sheetTransform = attachment.toSheet;
const sectionTransform = args.inSectionDrawingAttachment ? attachment.view.computeDisplayTransform(args) : undefined;
if (!sectionTransform) {
return sheetTransform.clone(args.output);
}
return sheetTransform.multiplyTransformTransform(sectionTransform, args.output);
}
}
/** A mostly no-op RenderTarget for an Attachment.
* its Scene and symbology overrides.
*/
class AttachmentTarget extends MockRender.OffScreenTarget {
_attachment;
constructor(attachment) {
// The dimensions don't matter - we're not drawing anything.
const rect = new ViewRect(1, 1);
super(IModelApp.renderSystem, rect);
this._attachment = attachment;
}
changeScene(scene) {
this._attachment.scene = scene;
}
overrideFeatureSymbology(ovrs) {
this._attachment.symbologyOverrides = ovrs;
}
}
/** Draws the contents a 2d or orthographic 3d view directly into a sheet view.
* We select tiles for the view in the context of a light-weight offscreen viewport with a no-op RenderTarget, then
* collect the resultant graphics and add them to the sheet view's scene.
*/
class OrthographicAttachment {
_viewport;
_props;
_sheetModelId;
_viewFlagOverrides;
_toSheet;
_fromSheet;
_sizeInMeters;
_range;
_viewRect = new ViewRect(0, 0, 1, 1);
_originalFrustum = new Frustum();
_clipVolume;
_hiddenLineSettings;
_scale;
_debugFeatureTable;
scene;
symbologyOverrides;
zDepth;
get view() {
return this._viewport.view;
}
get viewAttachmentProps() {
return this._props;
}
get viewport() {
return this._viewport;
}
constructor(view, props, sheetView) {
this.symbologyOverrides = new FeatureSymbology.Overrides(view);
const target = new AttachmentTarget(this);
this._viewport = OffScreenViewport.createViewport(view, target, true);
this._props = props;
this._sheetModelId = sheetView.baseModelId;
const applyClip = true; // set to false for debugging
this._viewFlagOverrides = {
...view.viewFlags,
clipVolume: applyClip,
lighting: false,
shadows: false,
};
const placement = Placement2d.fromJSON(props.placement);
const range = placement.calculateRange();
this._range = range;
this._sizeInMeters = new Point2d(range.xLength(), range.yLength());
// Compute transform from attached view's world coordinates to sheet's world coordinates.
// NB: We obtain the extents and origin from the *viewport* not the *view* - they may have been adjusted by the viewport.
const applySkew = true; // set to false for debugging
const skew = applySkew ? view.getAspectRatioSkew() : 1;
const extents = this._viewport.viewingSpace.viewDelta.clone();
const zDepth = Math.abs(extents.z);
const scaleX = this._sizeInMeters.x / Math.abs(extents.x);
const scaleY = skew * this._sizeInMeters.y / Math.abs(extents.y);
this._scale = { x: 1 / scaleX, y: 1 / scaleY };
const zBias = Frustum2d.depthFromDisplayPriority(props.jsonProperties?.displayPriority ?? 0);
this.zDepth = 1.01 * (zDepth - zBias); // give a little padding so that geometry right up against far plane doesn't get clipped.
// View origin is at the *back* of the view. Align *front* of view based on display priority.
const viewRot = view.getRotation();
const viewOrg = viewRot.multiplyVector(this._viewport.viewingSpace.viewOrigin);
viewOrg.z += zDepth;
viewRot.multiplyTransposeVectorInPlace(viewOrg);
const matrix = Matrix3d.createScale(scaleX, scaleY, 1);
matrix.multiplyMatrixMatrix(viewRot, matrix);
const origin = Matrix3d.xyzMinusMatrixTimesXYZ(viewOrg, matrix, viewOrg);
const attachmentOrigin = Point3d.createFrom(placement.origin);
attachmentOrigin.z = zBias;
const viewOrgToAttachment = attachmentOrigin.minus(viewOrg);
origin.addInPlace(viewOrgToAttachment);
this._toSheet = Transform.createRefs(origin, matrix);
this._fromSheet = this._toSheet.inverse();
// If the attached view is a section drawing, it may itself have an attached spatial view with a clip.
// The clip needs to be transformed into sheet space.
if (view.isDrawingView())
this._viewport.drawingToSheetTransform = this._toSheet;
// ###TODO? If we also apply the attachment's clip to the attached view, we may get additional culling during tile selection.
// However the attached view's frustum is already clipped by intersection with sheet view's frustum, and additional clipping planes
// introduce additional computation, so possibly not worth it.
// Transform the view's clip (if any) to sheet space
let viewClip = view.viewFlags.clipVolume ? view.getViewClip()?.clone() : undefined;
if (viewClip)
viewClip.transformInPlace(this._toSheet);
else
viewClip = ClipVector.createEmpty();
let sheetClip;
if (undefined !== props.jsonProperties?.clip)
sheetClip = ClipVector.fromJSON(props.jsonProperties?.clip);
if (sheetClip && sheetClip.isValid) {
// Clip to view attachment's clip. NB: clip is in sheet coordinate space.
for (const clip of sheetClip.clips)
viewClip.clips.push(clip);
}
else {
// Clip to view attachment's bounding box
viewClip.appendShape([
Point3d.create(this._range.low.x, this._range.low.y),
Point3d.create(this._range.high.x, this._range.low.y),
Point3d.create(this._range.high.x, this._range.high.y),
Point3d.create(this._range.low.x, this._range.high.y),
]);
}
this._clipVolume = IModelApp.renderSystem.createClipVolume(viewClip);
// Save off the original frustum (potentially adjusted by viewport).
this._viewport.setupFromView();
this._viewport.viewingSpace.getFrustum(CoordSystem.World, true, this._originalFrustum);
const applyHiddenLineSettings = true; // for debugging edge display, set to false...
const style = view.displayStyle;
if (style.is3d() && applyHiddenLineSettings)
this._hiddenLineSettings = style.settings.hiddenLineSettings;
}
[Symbol.dispose]() {
this._viewport[Symbol.dispose]();
}
discloseTileTrees(trees) {
trees.disclose(this._viewport);
}
addToScene(context) {
if (context.viewport.freezeScene)
return;
if (!context.viewport.view.viewsCategory(this._props.category))
return;
const wantBounds = context.viewport.wantViewAttachmentBoundaries;
const wantClipShapes = context.viewport.wantViewAttachmentClipShapes;
if (wantBounds || wantClipShapes) {
const builder = context.createSceneGraphicBuilder();
if (wantBounds) {
builder.setSymbology(ColorDef.red, ColorDef.red, 2);
builder.addRangeBox(this._range);
}
if (wantClipShapes && this._clipVolume) {
builder.setSymbology(ColorDef.blue, ColorDef.blue, 2);
for (const prim of this._clipVolume.clipVector.clips) {
if (!(prim instanceof ClipShape))
continue; // ###TODO handle non-shape primitives, if any such ever encountered
const pts = [];
const tf = prim.transformFromClip;
for (const pt of prim.polygon) {
const tfPt = tf ? tf.multiplyPoint3d(pt) : pt;
pts.push(new Point2d(tfPt.x, tfPt.y));
}
builder.addLineString2d(pts, 0);
}
}
// Put into a Batch so that we can see tooltip with attachment Id on mouseover.
const batch = context.target.renderSystem.createBatch(builder.finish(), this.getDebugFeatureTable(), this._range);
context.outputGraphic(batch);
}
if (!context.viewport.wantViewAttachments)
return;
// Pixel size used to compute size of ViewRect so that tiles of appropriate LOD are selected.
const pixelSize = context.viewport.getPixelSizeAtPoint();
if (0 === pixelSize)
return;
// Adjust attached view frustum based on intersection with sheet view frustum.
const attachFrustum = this._originalFrustum.transformBy(this._toSheet);
const attachFrustumRange = attachFrustum.toRange();
const sheetFrustum = context.viewport.getWorldFrustum();
const sheetFrustumRange = sheetFrustum.toRange();
const intersect = attachFrustumRange.intersect(sheetFrustumRange);
if (intersect.isNull)
return;
attachFrustum.initFromRange(intersect);
attachFrustum.transformBy(this._fromSheet, attachFrustum);
this._viewport.setupViewFromFrustum(attachFrustum);
// Adjust view rect based on size of attachment on screen so that tiles of appropriate LOD are selected.
const width = this._sizeInMeters.x * intersect.xLength() / attachFrustumRange.xLength();
const height = this._sizeInMeters.y * intersect.yLength() / attachFrustumRange.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 on-screen 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 scene graphics and insert into on-screen scene context.
const options = {
viewAttachmentId: this._props.id,
clipVolume: this._clipVolume,
hline: this._hiddenLineSettings,
frustum: {
is3d: this.view.is3d(),
scale: this._scale,
},
};
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._toSheet, options);
context.outputGraphic(branch);
};
outputGraphics(scene.foreground);
context.withGraphicType(TileGraphicType.BackgroundMap, () => outputGraphics(scene.background));
context.withGraphicType(TileGraphicType.Overlay, () => outputGraphics(scene.overlay));
// Report tile statistics to sheet view's 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,
});
}
getDebugFeatureTable() {
if (this._debugFeatureTable)
return this._debugFeatureTable;
const featureTable = new FeatureTable(1, this._sheetModelId);
featureTable.insert(new Feature(this._props.id));
this._debugFeatureTable = PackedFeatureTable.pack(featureTable);
return this._debugFeatureTable;
}
get areAllTileTreesLoaded() {
return this.view.areAllTileTreesLoaded;
}
collectStatistics(_stats) {
// Handled by discloseTileTrees()
}
get toSheet() {
return this._toSheet;
}
}
function createRasterAttachmentViewport(_view, _rect, _attachment) {
class RasterAttachmentViewport extends OffScreenViewport {
_sceneContext;
_isSceneReady = false;
_attachment;
constructor(view, rect, attachment) {
super(IModelApp.renderSystem.createOffscreenTarget(rect));
this._attachment = attachment;
this._isAspectRatioLocked = true;
this.changeView(view);
}
createSceneContext() {
assert(!this._isSceneReady);
this._sceneContext = super.createSceneContext();
return this._sceneContext;
}
renderFrame() {
assert(!this._isSceneReady);
this.clearSceneContext();
super.renderFrame();
if (undefined !== this._sceneContext) {
this._isSceneReady = !this._sceneContext.hasMissingTiles && this.view.areAllTileTreesLoaded;
if (this._isSceneReady)
this._attachment.produceGraphics(this._sceneContext);
this._sceneContext = undefined;
}
}
clearSceneContext() {
this._sceneContext = undefined;
}
addDecorations(_decorations) {
// ###TODO: skybox, ground plane, possibly grid. DecorateContext requires a ScreenViewport...
}
}
return new RasterAttachmentViewport(_view, _rect, _attachment);
}
/** Draws a 3d view with camera enabled into a sheet view by producing an image of the view's contents offscreen. */
class RasterAttachment {
_props;
_placement;
_transform;
zDepth;
_viewport;
_graphics;
constructor(view, props, sheetView) {
// Render to a 2048x2048 view rect. Scale in Y to preserve aspect ratio.
const maxSize = 2048;
const rect = new ViewRect(0, 0, maxSize, maxSize);
const height = maxSize * view.getAspectRatio() * view.getAspectRatioSkew();
const skew = maxSize / height;
view.setAspectRatioSkew(skew);
if (true !== props.jsonProperties?.displayOptions?.preserveBackground) {
// Make background color 100% transparent so that Viewport.readImageBuffer() will discard transparent pixels.
const bgColor = sheetView.displayStyle.backgroundColor.withAlpha(0);
view.displayStyle.backgroundColor = bgColor;
}
this._viewport = createRasterAttachmentViewport(view, rect, this);
this._props = props;
this._placement = Placement2d.fromJSON(props.placement);
this._transform = this._placement.transform;
this.zDepth = Frustum2d.depthFromDisplayPriority(props.jsonProperties?.displayPriority ?? 0);
}
[Symbol.dispose]() {
this._viewport?.[Symbol.dispose]();
}
get viewAttachmentProps() {
return this._props;
}
get viewport() {
return this._viewport;
}
get areAllTileTreesLoaded() {
return this._viewport?.areAllTileTreesLoaded ?? true;
}
addToScene(context) {
// ###TODO: check viewport.wantViewAttachmentClipShapes
if (!context.viewport.view.viewsCategory(this._props.category))
return;
if (context.viewport.wantViewAttachmentBoundaries) {
const builder = context.createSceneGraphicBuilder(this._transform);
builder.setSymbology(ColorDef.red, ColorDef.red, 2);
builder.addRangeBox(Range3d.createRange2d(this._placement.bbox));
context.outputGraphic(builder.finish());
}
if (!context.viewport.wantViewAttachments)
return;
if (this._graphics) {
context.outputGraphic(this._graphics);
return;
}
if (undefined === this._viewport)
return;
this._viewport.debugBoundingBoxes = context.viewport.debugBoundingBoxes;
this._viewport.setTileSizeModifier(context.viewport.tileSizeModifier);
this._viewport.renderFrame();
if (this._graphics)
context.outputGraphic(this._graphics);
}
discloseTileTrees(trees) {
if (this._viewport)
trees.disclose(this._viewport);
}
produceGraphics(context) {
assert(context.viewport === this._viewport);
this._graphics = this.createGraphics(this._viewport);
this._viewport = dispose(this._viewport);
if (undefined !== this._graphics)
context.outputGraphic(this._graphics);
}
createGraphics(vp) {
// Create a texture from the contents of the view.
const image = vp.readImageBuffer({ upsideDown: true });
if (undefined === image)
return undefined;
const debugImage = false; // set to true to open a window displaying the captured image.
if (debugImage) {
const url = imageBufferToPngDataUrl(image, false);
if (url)
openImageDataUrlInNewWindow(url, "Attachment");
}
const texture = IModelApp.renderSystem.createTexture({
image: { source: image, transparency: TextureTransparency.Opaque },
});
if (!texture)
return undefined;
// Create a material for the texture
const graphicParams = new GraphicParams();
graphicParams.material = IModelApp.renderSystem.createRenderMaterial({ textureMapping: { texture } });
// Apply the texture to a rectangular polyface.
const depth = this.zDepth;
const east = this._placement.bbox.low.x;
const west = this._placement.bbox.high.x;
const north = this._placement.bbox.low.y;
const south = this._placement.bbox.high.y;
const corners = [
Point3d.create(east, north, depth),
Point3d.create(west, north, depth),
Point3d.create(west, south, depth),
Point3d.create(east, south, depth),
];
const params = [
Point2d.create(0, 0),
Point2d.create(1, 0),
Point2d.create(1, 1),
Point2d.create(0, 1),
];
const strokeOptions = new StrokeOptions();
strokeOptions.needParams = strokeOptions.shouldTriangulate = true;
const polyfaceBuilder = PolyfaceBuilder.create(strokeOptions);
polyfaceBuilder.addQuadFacet(corners, params);
const polyface = polyfaceBuilder.claimPolyface();
const graphicBuilder = IModelApp.renderSystem.createGraphicBuilder(Transform.createIdentity(), GraphicType.Scene, vp, this._props.id);
graphicBuilder.activateGraphicParams(graphicParams);
graphicBuilder.addPolyface(polyface, false);
const graphic = graphicBuilder.finish();
// Wrap the polyface in a GraphicBranch.
const branch = new GraphicBranch(true);
const vfOvrs = createDefaultViewFlagOverrides({ clipVolume: true, shadows: false, lighting: false, thematic: false });
// Disable transparency - background pixels are 100% transparent so they will be discarded anyway. Other pixels are 100% opaque.
vfOvrs.transparency = false;
branch.setViewFlagOverrides(vfOvrs);
branch.symbologyOverrides = new FeatureSymbology.Overrides();
branch.entries.push(graphic);
// Apply the attachment's clip, if any.
let clipVolume;
if (this._props.jsonProperties?.clip) {
const clipVector = ClipVector.fromJSON(this._props.jsonProperties?.clip);
if (clipVector.isValid)
clipVolume = IModelApp.renderSystem.createClipVolume(clipVector);
}
return IModelApp.renderSystem.createGraphicBranch(branch, this._transform, { clipVolume });
}
collectStatistics(stats) {
if (this._graphics)
this._graphics.collectStatistics(stats);
}
}
//# sourceMappingURL=SheetViewState.js.map