UNPKG

@itwin/core-frontend

Version:
462 lines • 22.6 kB
"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