UNPKG

@itwin/core-frontend

Version:
1,085 lines • 188 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 Tools */ import { BeTimePoint } from "@itwin/core-bentley"; import { Angle, AngleSweep, Arc3d, AxisOrder, ClipUtilities, Constant, Geometry, LineString3d, Matrix3d, Plane3dByOriginAndUnitNormal, Point2d, Point3d, Range2d, Range3d, Ray3d, Transform, Vector2d, Vector3d, YawPitchRollAngles, } from "@itwin/core-geometry"; import { Cartographic, ColorDef, Frustum, LinePixels, NpcCenter } from "@itwin/core-common"; import { DialogProperty, PropertyDescriptionHelper, } from "@itwin/appui-abstract"; import { AccuDraw, AccuDrawHintBuilder } from "../AccuDraw"; import { BingLocationProvider } from "../BingLocation"; import { CoordSystem } from "../CoordSystem"; import { IModelApp } from "../IModelApp"; import { LengthDescription } from "../properties/LengthDescription"; import { Pixel } from "../render/Pixel"; import { StandardViewId } from "../StandardView"; import { eyeToCartographicOnGlobeFromGcs, queryTerrainElevationOffset, rangeToCartographicArea, viewGlobalLocation, ViewGlobalLocationConstants, } from "../ViewGlobalLocation"; import { DepthPointSource, ScreenViewport } from "../Viewport"; import { ViewRect } from "../common/ViewRect"; import { ViewState3d } from "../ViewState"; import { ViewStatus } from "../ViewStatus"; import { EditManipulator } from "./EditManipulator"; import { PrimitiveTool } from "./PrimitiveTool"; import { BeButton, BeButtonEvent, BeTouchEvent, CoordSource, CoreTools, EventHandled, InputSource, InteractiveTool, } from "./Tool"; import { ToolAssistance, ToolAssistanceImage, ToolAssistanceInputMethod } from "./ToolAssistance"; import { ToolSettings } from "./ToolSettings"; import { GraphicType } from "../common/render/GraphicType"; /** @internal */ export var ViewHandleType; (function (ViewHandleType) { ViewHandleType[ViewHandleType["None"] = 0] = "None"; ViewHandleType[ViewHandleType["Rotate"] = 1] = "Rotate"; ViewHandleType[ViewHandleType["TargetCenter"] = 2] = "TargetCenter"; ViewHandleType[ViewHandleType["Pan"] = 4] = "Pan"; ViewHandleType[ViewHandleType["Scroll"] = 8] = "Scroll"; ViewHandleType[ViewHandleType["Zoom"] = 16] = "Zoom"; ViewHandleType[ViewHandleType["Walk"] = 32] = "Walk"; ViewHandleType[ViewHandleType["Fly"] = 64] = "Fly"; ViewHandleType[ViewHandleType["Look"] = 128] = "Look"; ViewHandleType[ViewHandleType["LookAndMove"] = 256] = "LookAndMove"; })(ViewHandleType || (ViewHandleType = {})); /* eslint-enable no-restricted-syntax */ // dampen an inertia vector according to tool settings const inertialDampen = (pt) => { pt.scaleInPlace(Geometry.clamp(ToolSettings.viewingInertia.damping, .75, .999)); }; /** An InteractiveTool that manipulates a view. * @public * @extensions */ export class ViewTool extends InteractiveTool { viewport; static translate(val) { return CoreTools.translate(`View.${val}`); } inDynamicUpdate = false; beginDynamicUpdate() { this.inDynamicUpdate = true; } endDynamicUpdate() { this.inDynamicUpdate = false; } async run(..._args) { const toolAdmin = IModelApp.toolAdmin; if (undefined !== this.viewport && this.viewport === toolAdmin.markupView) { IModelApp.notifications.outputPromptByKey("iModelJs:Viewing.NotDuringMarkup"); return false; } if (!await toolAdmin.onInstallTool(this)) return false; await toolAdmin.startViewTool(this); await toolAdmin.onPostInstallTool(this); return true; } constructor(viewport) { super(); this.viewport = viewport; } async onResetButtonUp(_ev) { await this.exitTool(); return EventHandled.Yes; } /** Do not override. */ async exitTool() { return IModelApp.toolAdmin.exitViewTool(); } static showPrompt(prompt) { IModelApp.notifications.outputPrompt(ViewTool.translate(prompt)); } } /** @internal */ export class ViewingToolHandle { viewTool; _lastPtNpc = new Point3d(); _depthPoint; constructor(viewTool) { this.viewTool = viewTool; this._depthPoint = undefined; } onReinitialize() { } onCleanup() { } focusOut() { } motion(_ev) { return false; } checkOneShot() { return true; } getHandleCursor() { return "default"; } focusIn() { IModelApp.toolAdmin.setCursor(this.getHandleCursor()); } drawHandle(_context, _hasFocus) { } onWheel(_ev) { return false; } onTouchStart(_ev) { return false; } onTouchEnd(_ev) { return false; } async onTouchComplete(_ev) { return false; } async onTouchCancel(_ev) { return false; } onTouchMove(_ev) { return false; } onTouchMoveStart(_ev, _startEv) { return false; } onTouchTap(_ev) { return false; } onKeyTransition(_wentDown, _keyEvent) { return false; } onModifierKeyTransition(_wentDown, _modifier, _event) { return false; } needDepthPoint(_ev, _isPreview) { return false; } adjustDepthPoint(isValid, _vp, _plane, source) { switch (source) { case DepthPointSource.Geometry: case DepthPointSource.Model: case DepthPointSource.BackgroundMap: case DepthPointSource.GroundPlane: case DepthPointSource.Grid: case DepthPointSource.Map: return isValid; // Sources with visible geometry/graphics are considered valid by default... default: return false; // Sources without visible geometry/graphics are NOT considered valid by default... } } pickDepthPoint(ev) { this._depthPoint = this.viewTool.pickDepthPoint(ev); } // if we have a valid depth point, set the focus distance to changeFocusFromDepthPoint() { if (undefined === this._depthPoint) return; const view = this.viewTool.viewport?.view; if (undefined === view) return; if (view.is3d() && view.isCameraOn) view.changeFocusFromPoint(this._depthPoint); // set the focus distance to the depth point } } /** @internal */ export class ViewHandleArray { viewTool; handles = []; focus = -1; focusDrag = false; hitHandleIndex = 0; constructor(viewTool) { this.viewTool = viewTool; } empty() { this.focus = -1; this.focusDrag = false; this.hitHandleIndex = -1; // setting to -1 will result in onReinitialize getting called before testHit which sets the hit index this.handles.length = 0; } get count() { return this.handles.length; } get hitHandle() { return this.getByIndex(this.hitHandleIndex); } get focusHandle() { return this.getByIndex(this.focus); } add(handle) { this.handles.push(handle); } getByIndex(index) { return (index >= 0 && index < this.count) ? this.handles[index] : undefined; } focusHitHandle() { this.setFocus(this.hitHandleIndex); } testHit(ptScreen, forced = ViewHandleType.None) { this.hitHandleIndex = -1; const data = { distance: 0.0, priority: 10 /* ViewManipPriority.Normal */ }; let minDistance = 0.0; let minDistValid = false; let highestPriority = 1 /* ViewManipPriority.Low */; let nearestHitHandle; for (let i = 0; i < this.count; i++) { data.priority = 10 /* ViewManipPriority.Normal */; const handle = this.handles[i]; if (forced) { if (handle.handleType === forced) { this.hitHandleIndex = i; return true; } } else if (handle.testHandleForHit(ptScreen, data)) { if (data.priority >= highestPriority) { if (data.priority > highestPriority) minDistValid = false; highestPriority = data.priority; if (!minDistValid || (data.distance < minDistance)) { minDistValid = true; minDistance = data.distance; nearestHitHandle = handle; this.hitHandleIndex = i; } } } } return undefined !== nearestHitHandle; } drawHandles(context) { if (0 === this.count) return; // all handle objects must draw themselves for (let i = 0; i < this.count; ++i) { if (i !== this.hitHandleIndex) { const handle = this.handles[i]; handle.drawHandle(context, this.focus === i); } } // draw the hit handle last if (-1 !== this.hitHandleIndex) { const handle = this.handles[this.hitHandleIndex]; handle.drawHandle(context, this.focus === this.hitHandleIndex); } } setFocus(index) { if (this.focus === index && (this.focusDrag === this.viewTool.inHandleModify)) return; let focusHandle; if (this.focus >= 0) { focusHandle = this.getByIndex(this.focus); if (focusHandle) focusHandle.focusOut(); } if (index >= 0) { focusHandle = this.getByIndex(index); if (focusHandle) focusHandle.focusIn(); } this.focus = index; this.focusDrag = this.viewTool.inHandleModify; const vp = this.viewTool.viewport; if (undefined !== vp) vp.invalidateDecorations(); } onReinitialize() { this.handles.forEach((handle) => handle.onReinitialize()); } onCleanup() { this.handles.forEach((handle) => handle.onCleanup()); } motion(ev) { this.handles.forEach((handle) => handle.motion(ev)); } onWheel(ev) { let preventDefault = false; this.handles.forEach((handle) => { if (handle.onWheel(ev)) preventDefault = true; }); return preventDefault; } /** determine whether a handle of a specific type exists */ hasHandle(handleType) { return this.handles.some((handle) => handle.handleType === handleType); } } /** Base class for tools that manipulate the frustum of a Viewport. * @public * @extensions */ export class ViewManip extends ViewTool { handleMask; oneShot; isDraggingRequired; /** @internal */ viewHandles; frustumValid = false; // unused targetCenterWorld = new Point3d(); inHandleModify = false; isDragging = false; targetCenterValid = false; targetCenterLocked = false; nPts = 0; /** @internal */ forcedHandle = ViewHandleType.None; /** @internal */ _depthPreview; /** @internal */ _startPose; constructor(viewport, handleMask, oneShot, isDraggingRequired = false) { super(viewport); this.handleMask = handleMask; this.oneShot = oneShot; this.isDraggingRequired = isDraggingRequired; this.viewHandles = new ViewHandleArray(this); this.changeViewport(viewport); } decorate(context) { this.viewHandles.drawHandles(context); this.previewDepthPoint(context); } /** @internal */ previewDepthPoint(context) { if (undefined === this._depthPreview) return; const cursorVp = IModelApp.toolAdmin.cursorView; if (cursorVp !== context.viewport) return; let origin = this._depthPreview.plane.getOriginRef(); let normal = this._depthPreview.plane.getNormalRef(); if (this._depthPreview.isDefaultDepth) { origin = cursorVp.worldToView(origin); origin.z = 0.0; cursorVp.viewToWorld(origin, origin); // Avoid getting clipped out in z... normal = context.viewport.view.getZVector(); // Always draw circle for invalid depth point oriented to view... } const pixelSize = context.viewport.getPixelSizeAtPoint(origin); const skew = context.viewport.view.getAspectRatioSkew(); const radius = this._depthPreview.pickRadius * pixelSize; const rMatrix = Matrix3d.createRigidHeadsUp(normal); const ellipse = Arc3d.createScaledXYColumns(origin, rMatrix, radius, radius / skew, AngleSweep.create360()); const colorBase = (this._depthPreview.isDefaultDepth ? ColorDef.red : (DepthPointSource.Geometry === this._depthPreview.source ? ColorDef.green : context.viewport.hilite.color)); const colorLine = EditManipulator.HandleUtils.adjustForBackgroundColor(colorBase, cursorVp).withTransparency(50); const colorFill = colorLine.withTransparency(200); const builder = context.createGraphicBuilder(GraphicType.WorldOverlay); builder.setSymbology(colorLine, colorFill, 1, this._depthPreview.isDefaultDepth ? LinePixels.Code2 : LinePixels.Solid); builder.addArc(ellipse, true, true); builder.addArc(ellipse, false, false); context.addDecorationFromBuilder(builder); ViewTargetCenter.drawCross(context, origin, this._depthPreview.pickRadius * 0.5, false); } /** @internal */ getDepthPointGeometryId() { if (undefined === this._depthPreview) return undefined; return (DepthPointSource.Geometry === this._depthPreview.source ? this._depthPreview.sourceId : undefined); } /** @internal */ clearDepthPoint() { if (undefined === this._depthPreview) return false; this._depthPreview = undefined; return true; } /** @internal */ pickDepthPoint(ev, isPreview = false) { if (!isPreview && ev.viewport && undefined !== this.getDepthPointGeometryId()) ev.viewport.flashedId = undefined; this.clearDepthPoint(); if (isPreview && this.inDynamicUpdate) return undefined; const vp = ev.viewport; if (undefined === vp || undefined === this.viewHandles.hitHandle || !this.viewHandles.hitHandle.needDepthPoint(ev, isPreview)) return undefined; const pickRadiusPixels = vp.pixelsFromInches(ToolSettings.viewToolPickRadiusInches); const result = vp.pickDepthPoint(ev.rawPoint, pickRadiusPixels); let isValidDepth = false; switch (result.source) { case DepthPointSource.Geometry: case DepthPointSource.Model: case DepthPointSource.Map: isValidDepth = true; break; case DepthPointSource.BackgroundMap: case DepthPointSource.GroundPlane: case DepthPointSource.Grid: case DepthPointSource.ACS: case DepthPointSource.TargetPoint: const npcPt = vp.worldToNpc(result.plane.getOriginRef()); isValidDepth = !(npcPt.z < 0.0 || npcPt.z > 1.0); break; } // Allow handle to reject depth depending on source and to set a default depth point when invalid... isValidDepth = this.viewHandles.hitHandle.adjustDepthPoint(isValidDepth, vp, result.plane, result.source); if (isPreview) this._depthPreview = { testPoint: ev.rawPoint, pickRadius: pickRadiusPixels, plane: result.plane, source: result.source, isDefaultDepth: !isValidDepth, sourceId: result.sourceId }; return (isValidDepth || isPreview ? result.plane.getOriginRef() : undefined); } /** In addition to the onReinitialize calls after a tool installs or restarts, it is also * called from the mouseover event to cancel a drag operation if the up event occurred outside the view. * When operating in one shot mode and also requiring dragging, the tool should exit and not restart in ths situation. * A tool must opt in to allowing [[ViewTool.exitTool]] to be called from [[ViewManip.onReinitialize]] by * overriding this method to return true. */ get isExitAllowedOnReinitialize() { return false; } async onReinitialize() { const shouldExit = (this.oneShot && this.isDraggingRequired && this.isDragging && 0 !== this.nPts); if (undefined !== this.viewport) { this.viewport.synchWithView(); // make sure we store any changes in view undo buffer. this.viewHandles.setFocus(-1); } this.nPts = 0; this.inHandleModify = false; this.inDynamicUpdate = false; this._startPose = undefined; this.viewHandles.onReinitialize(); if (shouldExit && this.isExitAllowedOnReinitialize) return this.exitTool(); this.provideInitialToolAssistance(); } async onDataButtonDown(ev) { // Tool was started in "drag required" mode, don't advance tool state and wait to see if we get the start drag event. if ((0 === this.nPts && this.isDraggingRequired && !this.isDragging) || undefined === ev.viewport) return EventHandled.No; switch (this.nPts) { case 0: this.changeViewport(ev.viewport); if (this.processFirstPoint(ev)) this.nPts = 1; break; case 1: this.nPts = 2; break; } if (this.nPts > 1) { this.inDynamicUpdate = false; if (this.processPoint(ev, false) && this.oneShot) await this.exitTool(); else await this.onReinitialize(); } return EventHandled.Yes; } async onDataButtonUp(_ev) { if (this.nPts <= 1 && this.isDraggingRequired && !this.isDragging && this.oneShot) await this.exitTool(); return EventHandled.No; } async onMouseWheel(inputEv) { const ev = inputEv.clone(); if (this.viewHandles.onWheel(ev)) // notify handles that wheel has rolled. return EventHandled.Yes; await IModelApp.toolAdmin.processWheelEvent(ev, false); return EventHandled.Yes; } /** @internal */ async startHandleDrag(ev, forcedHandle) { if (this.inHandleModify) return EventHandled.No; // If already changing the view reject the request... if (undefined !== forcedHandle) { if (!this.viewHandles.hasHandle(forcedHandle)) return EventHandled.No; // If requested handle isn't present reject the request... this.forcedHandle = forcedHandle; } this.receivedDownEvent = true; // Request up events even though we may not have gotten the down event... this.isDragging = true; if (0 === this.nPts) await this.onDataButtonDown(ev); return EventHandled.Yes; } async onMouseStartDrag(ev) { if (BeButton.Data !== ev.button) return EventHandled.No; return this.startHandleDrag(ev); } async onMouseEndDrag(ev) { // NOTE: To support startHandleDrag being called by IdleTool for middle button drag, check inHandleModify and not the button type... if (!this.inHandleModify) return EventHandled.No; this.isDragging = false; return (0 === this.nPts) ? EventHandled.Yes : this.onDataButtonDown(ev); } async onMouseMotion(ev) { if (0 === this.nPts && this.viewHandles.testHit(ev.viewPoint)) this.viewHandles.focusHitHandle(); if (0 !== this.nPts) this.processPoint(ev, true); this.viewHandles.motion(ev); const prevSourceId = this.getDepthPointGeometryId(); const showDepthChanged = (undefined !== this.pickDepthPoint(ev, true) || this.clearDepthPoint()); if (ev.viewport && (showDepthChanged || prevSourceId)) { const currSourceId = this.getDepthPointGeometryId(); if (currSourceId !== prevSourceId) ev.viewport.flashedId = currSourceId; ev.viewport.invalidateDecorations(); } } async onTouchStart(ev) { if (0 === this.nPts && this.viewHandles.testHit(ev.viewPoint)) this.viewHandles.focusHitHandle(); const focusHandle = this.viewHandles.focusHandle; if (undefined !== focusHandle) focusHandle.onTouchStart(ev); } async onTouchEnd(ev) { const focusHandle = this.viewHandles.focusHandle; if (undefined !== focusHandle) focusHandle.onTouchEnd(ev); } async onTouchComplete(ev) { const focusHandle = this.viewHandles.focusHandle; if (undefined !== focusHandle && await focusHandle.onTouchComplete(ev)) return; if (this.inHandleModify) return IModelApp.toolAdmin.convertTouchEndToButtonUp(ev); } async onTouchCancel(ev) { const focusHandle = this.viewHandles.focusHandle; if (undefined !== focusHandle && await focusHandle.onTouchCancel(ev)) return; if (this.inHandleModify) return IModelApp.toolAdmin.convertTouchEndToButtonUp(ev, BeButton.Reset); } async onTouchMove(ev) { const focusHandle = this.viewHandles.focusHandle; if (undefined !== focusHandle && focusHandle.onTouchMove(ev)) return; if (this.inHandleModify) return IModelApp.toolAdmin.convertTouchMoveToMotion(ev); } async onTouchMoveStart(ev, startEv) { const focusHandle = this.viewHandles.focusHandle; if (undefined !== focusHandle && focusHandle.onTouchMoveStart(ev, startEv)) return EventHandled.Yes; if (!this.inHandleModify && startEv.isSingleTouch) await IModelApp.toolAdmin.convertTouchMoveStartToButtonDownAndMotion(startEv, ev); return this.inHandleModify ? EventHandled.Yes : EventHandled.No; } async onTouchTap(ev) { const focusHandle = this.viewHandles.focusHandle; if (undefined !== focusHandle && focusHandle.onTouchTap(ev)) return EventHandled.Yes; return ev.isSingleTap ? EventHandled.Yes : EventHandled.No; // Prevent IdleTool from converting single tap into data button down/up... } async onKeyTransition(wentDown, keyEvent) { const focusHandle = this.viewHandles.focusHandle; return (undefined !== focusHandle && focusHandle.onKeyTransition(wentDown, keyEvent) ? EventHandled.Yes : EventHandled.No); } async onModifierKeyTransition(wentDown, modifier, event) { const focusHandle = this.viewHandles.focusHandle; return (undefined !== focusHandle && focusHandle.onModifierKeyTransition(wentDown, modifier, event) ? EventHandled.Yes : EventHandled.No); } async onPostInstall() { await super.onPostInstall(); await this.onReinitialize(); // Call onReinitialize now that tool is installed. } provideToolAssistance(mainInstrKey, additionalInstr) { const mainInstruction = ToolAssistance.createInstruction(this.iconSpec, ViewTool.translate(mainInstrKey)); const mouseInstructions = []; const touchInstructions = []; const acceptMsg = CoreTools.translate("ElementSet.Inputs.AcceptPoint"); const rejectMsg = CoreTools.translate("ElementSet.Inputs.Exit"); touchInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.OneTouchDrag, acceptMsg, false, ToolAssistanceInputMethod.Touch)); mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.LeftClick, acceptMsg, false, ToolAssistanceInputMethod.Mouse)); touchInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.TwoTouchTap, rejectMsg, false, ToolAssistanceInputMethod.Touch)); mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.RightClick, rejectMsg, false, ToolAssistanceInputMethod.Mouse)); if (undefined !== additionalInstr) { for (const instr of additionalInstr) { if (ToolAssistanceInputMethod.Touch === instr.inputMethod) touchInstructions.push(instr); else mouseInstructions.push(instr); } } const sections = []; sections.push(ToolAssistance.createSection(mouseInstructions, ToolAssistance.inputsLabel)); sections.push(ToolAssistance.createSection(touchInstructions, ToolAssistance.inputsLabel)); const instructions = ToolAssistance.createInstructions(mainInstruction, sections); IModelApp.notifications.setToolAssistance(instructions); } /** Called from [[ViewManip.onReinitialize]] to allow tools to establish the tool assistance for the first point. */ provideInitialToolAssistance() { } async onCleanup() { let restorePrevious = false; if (this.inDynamicUpdate) { this.endDynamicUpdate(); restorePrevious = true; } const vp = this.viewport; if (undefined !== vp) { if (restorePrevious && this._startPose) { vp.view.applyPose(this._startPose); vp.animateFrustumChange(); } else { vp.synchWithView(); } vp.invalidateDecorations(); } this.viewHandles.onCleanup(); this.viewHandles.empty(); } /** * Set the center of rotation for rotate handle. * @param pt the new target point in world coordinates * @param lockTarget consider the target point locked for this tool instance * @param saveTarget save this target point for use between tool instances */ setTargetCenterWorld(pt, lockTarget, saveTarget) { this.targetCenterWorld.setFrom(pt); this.targetCenterValid = true; this.targetCenterLocked = lockTarget; if (!this.viewport) return; if (!this.viewport.view.allow3dManipulations()) this.targetCenterWorld.z = 0.0; this.viewport.viewCmdTargetCenter = (saveTarget ? pt : undefined); } updateTargetCenter() { const vp = this.viewport; if (!vp) return; if (this.targetCenterValid) { if (this.inHandleModify) return; if (IModelApp.tentativePoint.isActive) { let tentPt = IModelApp.tentativePoint.getPoint(); if (!IModelApp.tentativePoint.isSnapped) { if (undefined === this._depthPreview && this.targetCenterLocked) { const ev = new BeButtonEvent(); IModelApp.toolAdmin.fillEventFromCursorLocation(ev); this.targetCenterLocked = false; // Depth preview won't be active (or requested) if target is currently locked... this.pickDepthPoint(ev, true); } if (undefined !== this._depthPreview && !this._depthPreview.isDefaultDepth) tentPt = this._depthPreview.plane.getOriginRef(); // Prefer valid depth preview point to unsnapped tentative location... } this.setTargetCenterWorld(tentPt, true, false); IModelApp.tentativePoint.clear(true); // Clear tentative, there won't be a datapoint to accept... } return; } if (IModelApp.tentativePoint.isActive) return this.setTargetCenterWorld(IModelApp.tentativePoint.getPoint(), true, false); if (vp.viewCmdTargetCenter && this.isPointVisible(vp.viewCmdTargetCenter)) return this.setTargetCenterWorld(vp.viewCmdTargetCenter, true, true); return this.setTargetCenterWorld(ViewManip.getDefaultTargetPointWorld(vp), false, false); } processFirstPoint(ev) { const forcedHandle = this.forcedHandle; this.forcedHandle = ViewHandleType.None; if (this.viewHandles.testHit(ev.viewPoint, forcedHandle)) { this.inHandleModify = true; this.viewHandles.focusHitHandle(); const handle = this.viewHandles.hitHandle; if (undefined !== handle && !handle.firstPoint(ev)) return false; } this._startPose = this.viewport ? this.viewport.view.savePose() : undefined; return true; } processPoint(ev, inDynamics) { const hitHandle = this.viewHandles.hitHandle; if (undefined === hitHandle) return true; const doUpdate = hitHandle.doManipulation(ev, inDynamics); return inDynamics || (doUpdate && hitHandle.checkOneShot()); } lensAngleMatches(angle, tolerance) { const cameraView = this.viewport?.view; if (undefined === cameraView) return false; return !cameraView.is3d() ? false : Math.abs(cameraView.calcLensAngle().radians - angle.radians) < tolerance; } get isZUp() { const view = this.viewport?.view; if (undefined === view) return true; const viewX = view.getXVector(); const viewY = view.getXVector(); const zVec = Vector3d.unitZ(); return (Math.abs(zVec.dotProduct(viewY)) > 0.99 && Math.abs(zVec.dotProduct(viewX)) < 0.01); } static getFocusPlaneNpc(vp) { const pt = vp.worldToNpc(vp.view.getTargetPoint()); return (pt.z < 0.0 || pt.z > 1.0) ? 0.5 : pt.z; } static getDefaultTargetPointWorld(vp) { if (!vp.view.allow3dManipulations()) return vp.npcToWorld(NpcCenter); const targetPoint = vp.view.getTargetPoint(); const targetPointNpc = vp.worldToNpc(targetPoint); if (targetPointNpc.z < 0.0 || targetPointNpc.z > 1.0) { targetPointNpc.z = 0.5; vp.npcToWorld(targetPointNpc, targetPoint); } return targetPoint; } /** Determine whether the supplied point is visible in this Viewport. */ isPointVisible(testPt) { const vp = this.viewport; if (!vp) return false; return vp.isPointVisibleXY(testPt); } /** @internal */ static computeFitRange(viewport) { const range = viewport.computeViewRange(); const clip = (viewport.viewFlags.clipVolume ? viewport.view.getViewClip() : undefined); if (undefined !== clip) { const clipRange = ClipUtilities.rangeOfClipperIntersectionWithRange(clip, range); if (!clipRange.isNull) range.setFrom(clipRange); } return range; } static fitView(viewport, animateFrustumChange, options) { const range = this.computeFitRange(viewport); const aspect = viewport.viewRect.aspect; viewport.view.lookAtVolume(range, aspect, options); viewport.synchWithView({ animateFrustumChange }); viewport.viewCmdTargetCenter = undefined; } /** @internal */ static fitViewWithGlobeAnimation(viewport, animateFrustumChange, options) { const range = this.computeFitRange(viewport); if (viewport.view.isSpatialView() && animateFrustumChange && (viewport.viewingGlobe || !viewport.view.getIsViewingProject())) { const cartographicCenter = viewport.view.rootToCartographic(range.center); if (undefined !== cartographicCenter) { const cartographicArea = rangeToCartographicArea(viewport.view, range); (async () => { await viewport.animateFlyoverToGlobalLocation({ center: cartographicCenter, area: cartographicArea }); // NOTE: Turns on camera...which is why we checked that it was already on... viewport.viewCmdTargetCenter = undefined; })().catch(() => { }); return; } } const aspect = viewport.viewRect.aspect; viewport.view.lookAtVolume(range, aspect, options); viewport.synchWithView({ animateFrustumChange }); viewport.viewCmdTargetCenter = undefined; } static async zoomToAlwaysDrawnExclusive(viewport, options) { if (!viewport.isAlwaysDrawnExclusive || undefined === viewport.alwaysDrawn || 0 === viewport.alwaysDrawn.size) return false; await viewport.zoomToElements(viewport.alwaysDrawn, options); return true; } setCameraLensAngle(lensAngle, retainEyePoint) { const vp = this.viewport; if (!vp) return ViewStatus.InvalidViewport; const view = vp.view; if (!view.is3d() || !view.allow3dManipulations()) return ViewStatus.InvalidViewport; const result = (retainEyePoint && view.isCameraOn) ? view.lookAt({ eyePoint: view.getEyePoint(), targetPoint: view.getTargetPoint(), upVector: view.getYVector(), lensAngle }) : vp.turnCameraOn(lensAngle); if (result !== ViewStatus.Success) return result; vp.setupFromView(); return ViewStatus.Success; } enforceZUp(pivotPoint) { const vp = this.viewport; if (!vp || this.isZUp) return false; const viewY = vp.view.getYVector(); const rotMatrix = Matrix3d.createRotationVectorToVector(viewY, Vector3d.unitZ()); if (!rotMatrix) return false; const transform = Transform.createFixedPointAndMatrix(pivotPoint, rotMatrix); const frust = vp.getWorldFrustum(); frust.multiply(transform); vp.setupViewFromFrustum(frust); return true; } changeViewport(vp) { if (vp === this.viewport && 0 !== this.viewHandles.count) // If viewport isn't really changing do nothing... return; if (this.viewport) this.viewport.invalidateDecorations(); // Remove decorations from current viewport... this.viewport = vp; this.targetCenterValid = false; if (this.handleMask & (ViewHandleType.Rotate | ViewHandleType.TargetCenter)) this.updateTargetCenter(); this.viewHandles.empty(); if (this.handleMask & ViewHandleType.Rotate) this.viewHandles.add(new ViewRotate(this)); if (this.handleMask & ViewHandleType.TargetCenter) this.viewHandles.add(new ViewTargetCenter(this)); if (this.handleMask & ViewHandleType.Pan) this.viewHandles.add(new ViewPan(this)); if (this.handleMask & ViewHandleType.Scroll) this.viewHandles.add(new ViewScroll(this)); if (this.handleMask & ViewHandleType.Zoom) this.viewHandles.add(new ViewZoom(this)); if (this.handleMask & ViewHandleType.Walk) this.viewHandles.add(new ViewWalk(this)); if (this.handleMask & ViewHandleType.Fly) this.viewHandles.add(new ViewFly(this)); if (this.handleMask & ViewHandleType.Look) this.viewHandles.add(new ViewLook(this)); if (this.handleMask & ViewHandleType.LookAndMove) this.viewHandles.add(new ViewLookAndMove(this)); } } /** ViewingToolHandle for modifying the view's target point for operations like rotate */ class ViewTargetCenter extends ViewingToolHandle { get handleType() { return ViewHandleType.TargetCenter; } checkOneShot() { return false; } // Don't exit tool after moving target in single-shot mode... firstPoint(ev) { if (undefined === ev.viewport) return false; ev.viewport.viewCmdTargetCenter = undefined; // Clear current saved target, must accept a new location with ctrl... return true; } testHandleForHit(ptScreen, out) { if (this.viewTool.isDraggingRequired) return false; // Target center handle is not movable in this mode, but it's still nice to display the point we're rotating about... const vp = this.viewTool.viewport; if (undefined === vp) return false; const targetPt = vp.worldToView(this.viewTool.targetCenterWorld); const distance = targetPt.distanceXY(ptScreen); const locateThreshold = vp.pixelsFromInches(0.15); if (distance > locateThreshold) return false; out.distance = distance; out.priority = 1000 /* ViewManipPriority.High */; return true; } /** @internal */ static drawCross(context, worldPoint, sizePixels, hasFocus) { const crossSize = Math.floor(sizePixels) + 0.5; const outlineSize = crossSize + 1; const position = context.viewport.worldToView(worldPoint); position.x = Math.floor(position.x) + 0.5; position.y = Math.floor(position.y) + 0.5; const drawDecoration = (ctx) => { ctx.beginPath(); ctx.strokeStyle = "rgba(0,0,0,.5)"; ctx.lineWidth = hasFocus ? 5 : 3; ctx.moveTo(-outlineSize, 0); ctx.lineTo(outlineSize, 0); ctx.moveTo(0, -outlineSize); ctx.lineTo(0, outlineSize); ctx.stroke(); ctx.beginPath(); ctx.strokeStyle = "white"; ctx.lineWidth = hasFocus ? 3 : 1; ctx.shadowColor = "black"; ctx.shadowBlur = hasFocus ? 7 : 5; ctx.moveTo(-crossSize, 0); ctx.lineTo(crossSize, 0); ctx.moveTo(0, -crossSize); ctx.lineTo(0, crossSize); ctx.stroke(); }; context.addCanvasDecoration({ position, drawDecoration }); } drawHandle(context, hasFocus) { if (context.viewport !== this.viewTool.viewport) return; if (!this.viewTool.targetCenterLocked && !this.viewTool.inHandleModify) return; // Don't display default target center, will be updated to use pick point on element... if (hasFocus && this.viewTool.inHandleModify) return; // Cross display handled by preview depth point... let sizeInches = 0.2; if (!hasFocus && this.viewTool.inHandleModify) { const hitHandle = this.viewTool.viewHandles.hitHandle; if (undefined !== hitHandle && ViewHandleType.Rotate !== hitHandle.handleType) return; // Only display when modifying another handle if that handle is rotate (not pan)... sizeInches = 0.1; // Display small target when dragging... } const crossSize = context.viewport.pixelsFromInches(sizeInches); ViewTargetCenter.drawCross(context, this.viewTool.targetCenterWorld, crossSize, hasFocus); } doManipulation(ev, inDynamics) { if (inDynamics || ev.viewport !== this.viewTool.viewport) return false; this.pickDepthPoint(ev); this.viewTool.setTargetCenterWorld(undefined !== this._depthPoint ? this._depthPoint : ev.point, true, ev.isControlKey); // Lock target for just this tool instance, only save if control is down... return false; // false means don't do screen update } /** @internal */ needDepthPoint(_ev, _isPreview) { const focusHandle = this.viewTool.inHandleModify ? this.viewTool.viewHandles.focusHandle : undefined; return (undefined !== focusHandle && ViewHandleType.TargetCenter === focusHandle.handleType); } } /** A ViewingToolHandle with inertia. * If the handle is used with *throwing action* (mouse is moving when button goes up or via a touch with movement). * it continues to move briefly causing the operation to continue. */ class HandleWithInertia extends ViewingToolHandle { _duration; _end; _inertiaVec; doManipulation(ev, inDynamics) { if (ToolSettings.viewingInertia.enabled && !inDynamics && undefined !== this._inertiaVec) return this.beginAnimation(); const vp = ev.viewport; if (undefined === vp) return false; const thisPtNpc = vp.worldToNpc(ev.point); thisPtNpc.z = this._lastPtNpc.z; this._inertiaVec = undefined; if (this._lastPtNpc.isAlmostEqual(thisPtNpc, 1.0e-10)) return true; this._inertiaVec = this._lastPtNpc.vectorTo(thisPtNpc); return this.perform(thisPtNpc); } /** Set this handle to become the Viewport's animator */ beginAnimation() { this._duration = ToolSettings.viewingInertia.duration; if (this._duration.isTowardsFuture) { // ensure duration is towards future. Otherwise, don't start animation this._end = BeTimePoint.fromNow(this._duration); const vp = this.viewTool.viewport; if (undefined !== vp) vp.setAnimator(this); } return true; } /** Move this handle during the inertia duration */ animate() { if (undefined === this._inertiaVec) return true; // handle was removed // get the fraction of the inertia duration that remains. The decay is a combination of the number of iterations (see damping below) // and time. That way the handle slows down even if the framerate is lower. const remaining = ((this._end.milliseconds - BeTimePoint.now().milliseconds) / this._duration.milliseconds); const pt = this._lastPtNpc.plusScaled(this._inertiaVec, remaining); // if we're not moving any more, or if the duration has elapsed, we're done if (remaining <= 0 || (this._lastPtNpc.minus(pt).magnitudeSquared() < .000001)) { const vp = this.viewTool.viewport; if (undefined === vp) return false; vp.saveViewUndo(); return true; // remove this as the animator } this.perform(pt); // perform the viewing operation inertialDampen(this._inertiaVec); return false; } interrupt() { } } /** ViewingToolHandle for performing the "pan view" operation */ class ViewPan extends HandleWithInertia { get handleType() { return ViewHandleType.Pan; } getHandleCursor() { return this.viewTool.inHandleModify ? IModelApp.viewManager.grabbingCursor : IModelApp.viewManager.grabCursor; } firstPoint(ev) { this._inertiaVec = undefined; const tool = this.viewTool; const vp = tool.viewport; if (undefined === vp) return false; vp.worldToNpc(ev.point, this._lastPtNpc); // if the camera is on, we need to find the element under the starting point to get the z if (this.needDepthPoint(ev, false)) { this.pickDepthPoint(ev); if (undefined !== this._depthPoint) vp.worldToNpc(this._depthPoint, this._lastPtNpc); else this._lastPtNpc.z = ViewManip.getFocusPlaneNpc(vp); } tool.beginDynamicUpdate(); tool.provideToolAssistance("Pan.Prompts.NextPoint"); return true; } testHandleForHit(_ptScreen, out) { out.distance = 0.0; out.priority = 1 /* ViewManipPriority.Low */; return true; } /** perform the view pan operation */ perform(thisPtNpc) { const tool = this.viewTool; const vp = tool.viewport; if (undefined === vp) return false; const view = vp.view; const lastWorld = vp.npcToWorld(this._lastPtNpc); const thisWorld = vp.npcToWorld(thisPtNpc); const dist = thisWorld.vectorTo(lastWorld); if (view.is3d()) { if (ViewStatus.Success !== (vp.viewingGlobe ? view.moveCameraGlobal(lastWorld, thisWorld) : view.moveCameraWorld(dist))) return false; this.changeFocusFromDepthPoint(); // if we have a valid depth point, set it focus distance from it } else { view.setOrigin(view.getOrigin().plus(dist)); } vp.setupFromView(); this._lastPtNpc.setFrom(thisPtNpc); return true; } /** @internal */ needDepthPoint(ev, _isPreview) { const vp = ev.viewport; if (undefined === vp) return false; return vp.isCameraOn && CoordSource.User === ev.coordsFrom; } } /** ViewingToolHandle for performing the "rotate view" operation */ class ViewRotate extends HandleWithInertia { _frustum = new Frustum(); _activeFrustum = new Frustum(); _anchorPtNpc = new Point3d(); get handleType() { return ViewHandleType.Rotate; } getHandleCursor() { return IModelApp.viewManager.rotateCursor; } testHandleForHit(_ptScreen, out) { out.distance = 0.0; out.priority = 100 /* ViewManipPriority.Medium */; // Always prefer over pan handle which is only force enabled by IdleTool middle button action... return true; } firstPoint(ev) { this._inertiaVec = undefined; const tool = this.viewTool; const vp = ev.viewport; if (undefined === vp) return false; this.pickDepthPoint(ev); if (undefined !== this._depthPoint) tool.setTargetCenterWorld(this._depthPoint, false, false); vp.worldToNpc(ev.rawPoint, this._anchorPtNpc); this._lastPtNpc.setFrom(this._anchorPtNpc); vp.getFrustum(CoordSystem.World, false, this._activeFrustum); this._frustum.setFrom(this._activeFrustum); tool.beginDynamicUpdate(); this.viewTool.provideToolAssistance("Rotate.Prompts.NextPoint"); return true; } perform(ptNpc) { const tool = this.viewTool; const vp = tool.viewport; if (undefined === vp) return false; if (this._anchorPtNpc.isAlmostEqual(ptNpc, 1.0e-2)) // too close to anchor pt ptNpc.setFrom(this._anchorPtNpc); const currentFrustum = vp.getFrustum(CoordSystem.World, false); const frustumChange = !currentFrustum.equals(this._activeFrustum); if (frustumChange) this._frustum.setFrom(currentFrustum); else { if (!vp.setupViewFromFrustum(this._frustum)) return false; } const currPt = vp.npcToView(ptNpc); if (frustumChange) this._anchorPtNpc.setFrom(ptNpc); const view = vp.view; let angle; let worldAxis; const worldPt = tool.targetCenterWorld; if (!view.allow3dManipulations()) { const centerPt = vp.worldToView(worldPt); const firstPt = vp.npcToView(this._anchorPtNpc); const vector0 = Vector2d.createStartEnd(centerPt, firstPt); const vector1 = Vector2d.createStartEnd(centerPt, currPt); angle = vector0.angleTo(vector1); worldAxis = Vector3d.unitZ(); } else { const viewRect = vp.viewRect; vp.npcToView(ptNpc, currPt); const firstPt = vp.npcToView(this._anchorPtNpc); const xDelta = (currPt.x - firstPt.x); const yDelta = (currPt.y - firstPt.y); // Movement in screen x == rotation about drawing Z (preserve up) or rotation about screen Y... const xAxis = ToolSettings.preserveWorldUp && !vp.viewingGlobe ? (undefined !== this._depthPoint ? vp.view.getUpVector(this._depthPoint) : Vector3d.unitZ()) : vp.rotation.getRow(1); // Movement in screen y == rotation about screen X... const yAxis = vp.rotation.getRow(0); const xRMatrix = (xDelta ? Matrix3d.createRotationAroundVector(xAxis, Angle.createRadians(Math.PI / (viewRect.width / xDelta))) : undefined) ?? Matrix3d.identity; const yRMatrix = (yDelta ? Matrix3d.createRotationAroundVector(yAxis, Angle.createRadians(Math.PI / (viewRect.height / yDelta))) : undefined) ?? Matrix3d.identity; const worldRMatrix = yRMatrix.multiplyMatrixMatrix(xRMatrix); const result = worldRMatrix.getAxisAndAngleOfRotation(); angle = Angle.createRadians(-result.angle.radians); worldAxis = result.axis; } const worldMatrix = Matrix3d.createRotationAroundVector(worldAxis, angle); if (undefined !== worldMatrix) { const worldTransform = Transform.createFixedPointAndMatrix(worldPt, worldMatrix); const frustum = this._frustum.transformBy(worldTransform); view.setupFromFrustum(frustum); if (view.is3d()) view.alignToGlobe(view.getCenter()); this.changeFocusFromDepthPoint(); // if we have a valid depth point, set it focus distance from it vp.setupFromView(); } vp.getWorldFrustum(this._activeFrustum); this._lastPtNpc.setFrom(ptNpc); return true; } onWheel(ev) { // When rotate is active, the mouse wheel should zoom about the target center when it's displayed... const tool = this.viewTool; if (tool.targetCenterLocked || tool.inHandleModify) { ev.point = tool.targetCenterWorld; ev.coordsFrom = CoordSource.Precision; // WheelEventProcessor.doZoom checks this to decide whether to use raw or adjusted point... } return false; } /** @internal */ needDepthPoint(ev, _isPreview) { const vp = ev.viewport; if (undefined === vp) return false; return (!this.viewTool.targetCenterLocked && vp.view.allow3dManipulations()); } /** @internal */ adjustDepthPoint(isValid, vp, plane, source) { if (vp.viewingGlobe && vp.iModel.ecefLocation && this.viewTool.isPointVisible(vp.iModel.ecefLocation.earthCenter)) { plane.getOriginRef().setFrom(vp.iModel.ecefLocation.earthCenter); plane.getNormalRef().setFrom(vp.view.getZVector()); return true; } if (super.adjustDepthPoint(isValid, vp, plane, source)) return true; plane.getOriginRef().setFrom(this.viewTool.targetCenterWorld); return false; } } /** ViewingToolHandle for performing the "look view" operation */ class ViewLook extends ViewingToolHandle { _eyePoint = new Point3d(); _firstPtView = new Point3d(); _rotat