@itwin/core-frontend
Version:
iTwin.js frontend components
462 lines • 22.6 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* 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
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.createViewAttachmentRenderer = createViewAttachmentRenderer;
const core_common_1 = require("@itwin/core-common");
const CoordSystem_1 = require("../CoordSystem");
const Frustum2d_1 = require("../Frustum2d");
const IModelApp_1 = require("../IModelApp");
const Viewport_1 = require("../Viewport");
const ImageUtil_1 = require("../common/ImageUtil");
const ViewRect_1 = require("../common/ViewRect");
const GraphicType_1 = require("../common/render/GraphicType");
const FeatureSymbology_1 = require("../render/FeatureSymbology");
const GraphicBranch_1 = require("../render/GraphicBranch");
const internal_1 = require("../tile/internal");
const MockRender_1 = require("./render/MockRender");
const core_bentley_1 = require("@itwin/core-bentley");
const core_geometry_1 = require("@itwin/core-geometry");
const ViewFlagOverrides_1 = require("./tile/ViewFlagOverrides");
function createViewAttachmentRenderer(args) {
const { props, view } = args;
if (props.jsonProperties?.displayOptions?.drawAsRaster || (view.is3d() && view.isCameraOn)) {
return new RasterAttachment(view, props, args.backgroundColor);
}
else {
return new OrthographicAttachment(view, props, args.sheetModelId);
}
}
/** A mostly no-op RenderTarget for an OrthographicAttachment. */
class AttachmentTarget extends MockRender_1.MockRender.OffScreenTarget {
_attachment;
constructor(attachment) {
// The dimensions don't matter - we're not drawing anything.
const rect = new ViewRect_1.ViewRect(1, 1);
super(IModelApp_1.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_1.ViewRect(0, 0, 1, 1);
_originalFrustum = new core_common_1.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, sheetModelId) {
this.symbologyOverrides = new FeatureSymbology_1.FeatureSymbology.Overrides(view);
const target = new AttachmentTarget(this);
this._viewport = Viewport_1.OffScreenViewport.createViewport(view, target, true);
this._props = props;
this._sheetModelId = sheetModelId;
const applyClip = true; // set to false for debugging
this._viewFlagOverrides = {
...view.viewFlags,
clipVolume: applyClip,
lighting: false,
shadows: false,
};
const placement = core_common_1.Placement2d.fromJSON(props.placement);
const range = placement.calculateRange();
this._range = range;
this._sizeInMeters = new core_geometry_1.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_1.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 = core_geometry_1.Matrix3d.createScale(scaleX, scaleY, 1);
matrix.multiplyMatrixMatrix(viewRot, matrix);
const origin = core_geometry_1.Matrix3d.xyzMinusMatrixTimesXYZ(viewOrg, matrix, viewOrg);
const attachmentOrigin = core_geometry_1.Point3d.createFrom(placement.origin);
attachmentOrigin.z = zBias;
const viewOrgToAttachment = attachmentOrigin.minus(viewOrg);
origin.addInPlace(viewOrgToAttachment);
this._toSheet = core_geometry_1.Transform.createRefs(origin, matrix);
this._fromSheet = (0, core_bentley_1.expectDefined)(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 = core_geometry_1.ClipVector.createEmpty();
let sheetClip;
if (undefined !== props.jsonProperties?.clip)
sheetClip = core_geometry_1.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([
core_geometry_1.Point3d.create(this._range.low.x, this._range.low.y),
core_geometry_1.Point3d.create(this._range.high.x, this._range.low.y),
core_geometry_1.Point3d.create(this._range.high.x, this._range.high.y),
core_geometry_1.Point3d.create(this._range.low.x, this._range.high.y),
]);
}
this._clipVolume = IModelApp_1.IModelApp.renderSystem.createClipVolume(viewClip);
// Save off the original frustum (potentially adjusted by viewport).
this._viewport.setupFromView();
this._viewport.viewingSpace.getFrustum(CoordSystem_1.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(core_common_1.ColorDef.red, core_common_1.ColorDef.red, 2);
builder.addRangeBox(this._range);
}
if (wantClipShapes && this._clipVolume) {
builder.setSymbology(core_common_1.ColorDef.blue, core_common_1.ColorDef.blue, 2);
for (const prim of this._clipVolume.clipVector.clips) {
if (!(prim instanceof core_geometry_1.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 core_geometry_1.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_1.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(internal_1.TileGraphicType.BackgroundMap, () => outputGraphics(scene.background));
context.withGraphicType(internal_1.TileGraphicType.Overlay, () => outputGraphics(scene.overlay));
// Report tile statistics to sheet view's viewport.
const tileAdmin = IModelApp_1.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 core_common_1.FeatureTable(1, this._sheetModelId);
featureTable.insert(new core_common_1.Feature(this._props.id));
this._debugFeatureTable = core_common_1.PackedFeatureTable.pack(featureTable);
return this._debugFeatureTable;
}
get areAllTileTreesLoaded() {
return this.view.areAllTileTreesLoaded;
}
collectStatistics(_stats) {
// Handled by discloseTileTrees()
}
get ortho() {
return {
toSheet: this._toSheet,
view: this.view,
};
}
}
function createRasterAttachmentViewport(_view, _rect, _attachment) {
class RasterAttachmentViewport extends Viewport_1.OffScreenViewport {
_sceneContext;
_isSceneReady = false;
_attachment;
constructor(view, rect, attachment) {
super(IModelApp_1.IModelApp.renderSystem.createOffscreenTarget(rect));
this._attachment = attachment;
this._isAspectRatioLocked = true;
this.changeView(view);
}
createSceneContext() {
(0, core_bentley_1.assert)(!this._isSceneReady);
this._sceneContext = super.createSceneContext();
return this._sceneContext;
}
renderFrame() {
(0, core_bentley_1.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() {
// ###TODO: skybox, ground plane, possibly grid. DecorateContext requires a ScreenViewport...
}
}
return new RasterAttachmentViewport(_view, _rect, _attachment);
}
/** Draws a 3d view (usually 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, bgColor) {
// Render to a 2048x2048 view rect. Scale in Y to preserve aspect ratio.
const maxSize = 2048;
const rect = new ViewRect_1.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.
view.displayStyle.backgroundColor = bgColor.withAlpha(0);
}
this._viewport = createRasterAttachmentViewport(view, rect, this);
this._props = props;
this._placement = core_common_1.Placement2d.fromJSON(props.placement);
this._transform = this._placement.transform;
this.zDepth = Frustum2d_1.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(core_common_1.ColorDef.red, core_common_1.ColorDef.red, 2);
builder.addRangeBox(core_geometry_1.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) {
(0, core_bentley_1.assert)(context.viewport === this._viewport);
this._graphics = this.createGraphics(this._viewport);
this._viewport = (0, core_bentley_1.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 = (0, ImageUtil_1.imageBufferToPngDataUrl)(image, false);
if (url)
(0, ImageUtil_1.openImageDataUrlInNewWindow)(url, "Attachment");
}
const texture = IModelApp_1.IModelApp.renderSystem.createTexture({
image: { source: image, transparency: core_common_1.TextureTransparency.Opaque },
});
if (!texture)
return undefined;
// Create a material for the texture
const graphicParams = new core_common_1.GraphicParams();
graphicParams.material = IModelApp_1.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 = [
core_geometry_1.Point3d.create(east, north, depth),
core_geometry_1.Point3d.create(west, north, depth),
core_geometry_1.Point3d.create(west, south, depth),
core_geometry_1.Point3d.create(east, south, depth),
];
const params = [
core_geometry_1.Point2d.create(0, 0),
core_geometry_1.Point2d.create(1, 0),
core_geometry_1.Point2d.create(1, 1),
core_geometry_1.Point2d.create(0, 1),
];
const strokeOptions = new core_geometry_1.StrokeOptions();
strokeOptions.needParams = strokeOptions.shouldTriangulate = true;
const polyfaceBuilder = core_geometry_1.PolyfaceBuilder.create(strokeOptions);
polyfaceBuilder.addQuadFacet(corners, params);
const polyface = polyfaceBuilder.claimPolyface();
const graphicBuilder = IModelApp_1.IModelApp.renderSystem.createGraphicBuilder(core_geometry_1.Transform.createIdentity(), GraphicType_1.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_1.GraphicBranch(true);
const vfOvrs = (0, ViewFlagOverrides_1.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_1.FeatureSymbology.Overrides();
branch.entries.push(graphic);
// Apply the attachment's clip, if any.
let clipVolume;
if (this._props.jsonProperties?.clip) {
const clipVector = core_geometry_1.ClipVector.fromJSON(this._props.jsonProperties?.clip);
if (clipVector.isValid)
clipVolume = IModelApp_1.IModelApp.renderSystem.createClipVolume(clipVector);
}
return IModelApp_1.IModelApp.renderSystem.createGraphicBranch(branch, this._transform, { clipVolume });
}
collectStatistics(stats) {
if (this._graphics)
this._graphics.collectStatistics(stats);
}
}
//# sourceMappingURL=ViewAttachmentRenderer.js.map