UNPKG

@itwin/core-frontend

Version:
1,045 lines • 199 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 Tools */ Object.defineProperty(exports, "__esModule", { value: true }); exports.SetupWalkCameraTool = exports.SetupCameraTool = exports.ViewToggleCameraTool = exports.ViewRedoTool = exports.ViewUndoTool = exports.DefaultViewTouchTool = exports.WindowAreaTool = exports.StandardViewTool = exports.ViewGlobeIModelTool = exports.ViewGlobeLocationTool = exports.ViewGlobeBirdTool = exports.ViewGlobeSatelliteTool = exports.FitViewTool = exports.FlyViewTool = exports.WalkViewTool = exports.LookAndMoveTool = exports.ZoomViewTool = exports.ScrollViewTool = exports.LookViewTool = exports.RotateViewTool = exports.PanViewTool = exports.ViewManip = exports.ViewHandleArray = exports.ViewingToolHandle = exports.ViewTool = exports.ViewHandleType = void 0; const core_bentley_1 = require("@itwin/core-bentley"); const core_geometry_1 = require("@itwin/core-geometry"); const core_common_1 = require("@itwin/core-common"); const appui_abstract_1 = require("@itwin/appui-abstract"); const AccuDraw_1 = require("../AccuDraw"); const BingLocation_1 = require("../BingLocation"); const CoordSystem_1 = require("../CoordSystem"); const IModelApp_1 = require("../IModelApp"); const LengthDescription_1 = require("../properties/LengthDescription"); const Pixel_1 = require("../render/Pixel"); const StandardView_1 = require("../StandardView"); const ViewGlobalLocation_1 = require("../ViewGlobalLocation"); const Viewport_1 = require("../Viewport"); const ViewRect_1 = require("../common/ViewRect"); const ViewState_1 = require("../ViewState"); const ViewStatus_1 = require("../ViewStatus"); const EditManipulator_1 = require("./EditManipulator"); const PrimitiveTool_1 = require("./PrimitiveTool"); const Tool_1 = require("./Tool"); const ToolAssistance_1 = require("./ToolAssistance"); const ToolSettings_1 = require("./ToolSettings"); const GraphicType_1 = require("../common/render/GraphicType"); /** @internal */ 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 || (exports.ViewHandleType = ViewHandleType = {})); /* eslint-enable no-restricted-syntax */ // dampen an inertia vector according to tool settings const inertialDampen = (pt) => { pt.scaleInPlace(core_geometry_1.Geometry.clamp(ToolSettings_1.ToolSettings.viewingInertia.damping, .75, .999)); }; /** An InteractiveTool that manipulates a view. * @public * @extensions */ class ViewTool extends Tool_1.InteractiveTool { viewport; static translate(val) { return Tool_1.CoreTools.translate(`View.${val}`); } inDynamicUpdate = false; beginDynamicUpdate() { this.inDynamicUpdate = true; } endDynamicUpdate() { this.inDynamicUpdate = false; } async run(..._args) { const toolAdmin = IModelApp_1.IModelApp.toolAdmin; if (undefined !== this.viewport && this.viewport === toolAdmin.markupView) { IModelApp_1.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 Tool_1.EventHandled.Yes; } /** Do not override. */ async exitTool() { return IModelApp_1.IModelApp.toolAdmin.exitViewTool(); } static showPrompt(prompt) { IModelApp_1.IModelApp.notifications.outputPrompt(ViewTool.translate(prompt)); } } exports.ViewTool = ViewTool; /** @internal */ class ViewingToolHandle { viewTool; _lastPtNpc = new core_geometry_1.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_1.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 Viewport_1.DepthPointSource.Geometry: case Viewport_1.DepthPointSource.Model: case Viewport_1.DepthPointSource.BackgroundMap: case Viewport_1.DepthPointSource.GroundPlane: case Viewport_1.DepthPointSource.Grid: case Viewport_1.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 } } exports.ViewingToolHandle = ViewingToolHandle; /** @internal */ 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); } } exports.ViewHandleArray = ViewHandleArray; /** Base class for tools that manipulate the frustum of a Viewport. * @public * @extensions */ class ViewManip extends ViewTool { handleMask; oneShot; isDraggingRequired; /** @internal */ viewHandles; frustumValid = false; // unused targetCenterWorld = new core_geometry_1.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_1.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 = core_geometry_1.Matrix3d.createRigidHeadsUp(normal); const ellipse = core_geometry_1.Arc3d.createScaledXYColumns(origin, rMatrix, radius, radius / skew, core_geometry_1.AngleSweep.create360()); const colorBase = (this._depthPreview.isDefaultDepth ? core_common_1.ColorDef.red : (Viewport_1.DepthPointSource.Geometry === this._depthPreview.source ? core_common_1.ColorDef.green : context.viewport.hilite.color)); const colorLine = EditManipulator_1.EditManipulator.HandleUtils.adjustForBackgroundColor(colorBase, cursorVp).withTransparency(50); const colorFill = colorLine.withTransparency(200); const builder = context.createGraphicBuilder(GraphicType_1.GraphicType.WorldOverlay); builder.setSymbology(colorLine, colorFill, 1, this._depthPreview.isDefaultDepth ? core_common_1.LinePixels.Code2 : core_common_1.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 (Viewport_1.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_1.ToolSettings.viewToolPickRadiusInches); const result = vp.pickDepthPoint(ev.rawPoint, pickRadiusPixels); let isValidDepth = false; switch (result.source) { case Viewport_1.DepthPointSource.Geometry: case Viewport_1.DepthPointSource.Model: case Viewport_1.DepthPointSource.Map: isValidDepth = true; break; case Viewport_1.DepthPointSource.BackgroundMap: case Viewport_1.DepthPointSource.GroundPlane: case Viewport_1.DepthPointSource.Grid: case Viewport_1.DepthPointSource.ACS: case Viewport_1.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 Tool_1.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 Tool_1.EventHandled.Yes; } async onDataButtonUp(_ev) { if (this.nPts <= 1 && this.isDraggingRequired && !this.isDragging && this.oneShot) await this.exitTool(); return Tool_1.EventHandled.No; } async onMouseWheel(inputEv) { const ev = inputEv.clone(); if (this.viewHandles.onWheel(ev)) // notify handles that wheel has rolled. return Tool_1.EventHandled.Yes; await IModelApp_1.IModelApp.toolAdmin.processWheelEvent(ev, false); return Tool_1.EventHandled.Yes; } /** @internal */ async startHandleDrag(ev, forcedHandle) { if (this.inHandleModify) return Tool_1.EventHandled.No; // If already changing the view reject the request... if (undefined !== forcedHandle) { if (!this.viewHandles.hasHandle(forcedHandle)) return Tool_1.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 Tool_1.EventHandled.Yes; } async onMouseStartDrag(ev) { if (Tool_1.BeButton.Data !== ev.button) return Tool_1.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 Tool_1.EventHandled.No; this.isDragging = false; return (0 === this.nPts) ? Tool_1.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_1.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_1.IModelApp.toolAdmin.convertTouchEndToButtonUp(ev, Tool_1.BeButton.Reset); } async onTouchMove(ev) { const focusHandle = this.viewHandles.focusHandle; if (undefined !== focusHandle && focusHandle.onTouchMove(ev)) return; if (this.inHandleModify) return IModelApp_1.IModelApp.toolAdmin.convertTouchMoveToMotion(ev); } async onTouchMoveStart(ev, startEv) { const focusHandle = this.viewHandles.focusHandle; if (undefined !== focusHandle && focusHandle.onTouchMoveStart(ev, startEv)) return Tool_1.EventHandled.Yes; if (!this.inHandleModify && startEv.isSingleTouch) await IModelApp_1.IModelApp.toolAdmin.convertTouchMoveStartToButtonDownAndMotion(startEv, ev); return this.inHandleModify ? Tool_1.EventHandled.Yes : Tool_1.EventHandled.No; } async onTouchTap(ev) { const focusHandle = this.viewHandles.focusHandle; if (undefined !== focusHandle && focusHandle.onTouchTap(ev)) return Tool_1.EventHandled.Yes; return ev.isSingleTap ? Tool_1.EventHandled.Yes : Tool_1.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) ? Tool_1.EventHandled.Yes : Tool_1.EventHandled.No); } async onModifierKeyTransition(wentDown, modifier, event) { const focusHandle = this.viewHandles.focusHandle; return (undefined !== focusHandle && focusHandle.onModifierKeyTransition(wentDown, modifier, event) ? Tool_1.EventHandled.Yes : Tool_1.EventHandled.No); } async onPostInstall() { await super.onPostInstall(); await this.onReinitialize(); // Call onReinitialize now that tool is installed. } provideToolAssistance(mainInstrKey, additionalInstr) { const mainInstruction = ToolAssistance_1.ToolAssistance.createInstruction(this.iconSpec, ViewTool.translate(mainInstrKey)); const mouseInstructions = []; const touchInstructions = []; const acceptMsg = Tool_1.CoreTools.translate("ElementSet.Inputs.AcceptPoint"); const rejectMsg = Tool_1.CoreTools.translate("ElementSet.Inputs.Exit"); touchInstructions.push(ToolAssistance_1.ToolAssistance.createInstruction(ToolAssistance_1.ToolAssistanceImage.OneTouchDrag, acceptMsg, false, ToolAssistance_1.ToolAssistanceInputMethod.Touch)); mouseInstructions.push(ToolAssistance_1.ToolAssistance.createInstruction(ToolAssistance_1.ToolAssistanceImage.LeftClick, acceptMsg, false, ToolAssistance_1.ToolAssistanceInputMethod.Mouse)); touchInstructions.push(ToolAssistance_1.ToolAssistance.createInstruction(ToolAssistance_1.ToolAssistanceImage.TwoTouchTap, rejectMsg, false, ToolAssistance_1.ToolAssistanceInputMethod.Touch)); mouseInstructions.push(ToolAssistance_1.ToolAssistance.createInstruction(ToolAssistance_1.ToolAssistanceImage.RightClick, rejectMsg, false, ToolAssistance_1.ToolAssistanceInputMethod.Mouse)); if (undefined !== additionalInstr) { for (const instr of additionalInstr) { if (ToolAssistance_1.ToolAssistanceInputMethod.Touch === instr.inputMethod) touchInstructions.push(instr); else mouseInstructions.push(instr); } } const sections = []; sections.push(ToolAssistance_1.ToolAssistance.createSection(mouseInstructions, ToolAssistance_1.ToolAssistance.inputsLabel)); sections.push(ToolAssistance_1.ToolAssistance.createSection(touchInstructions, ToolAssistance_1.ToolAssistance.inputsLabel)); const instructions = ToolAssistance_1.ToolAssistance.createInstructions(mainInstruction, sections); IModelApp_1.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_1.IModelApp.tentativePoint.isActive) { let tentPt = IModelApp_1.IModelApp.tentativePoint.getPoint(); if (!IModelApp_1.IModelApp.tentativePoint.isSnapped) { if (undefined === this._depthPreview && this.targetCenterLocked) { const ev = new Tool_1.BeButtonEvent(); IModelApp_1.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_1.IModelApp.tentativePoint.clear(true); // Clear tentative, there won't be a datapoint to accept... } return; } if (IModelApp_1.IModelApp.tentativePoint.isActive) return this.setTargetCenterWorld(IModelApp_1.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 = core_geometry_1.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(core_common_1.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 = core_geometry_1.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 = (0, ViewGlobalLocation_1.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_1.ViewStatus.InvalidViewport; const view = vp.view; if (!view.is3d() || !view.allow3dManipulations()) return ViewStatus_1.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_1.ViewStatus.Success) return result; vp.setupFromView(); return ViewStatus_1.ViewStatus.Success; } enforceZUp(pivotPoint) { const vp = this.viewport; if (!vp || this.isZUp) return false; const viewY = vp.view.getYVector(); const rotMatrix = core_geometry_1.Matrix3d.createRotationVectorToVector(viewY, core_geometry_1.Vector3d.unitZ()); if (!rotMatrix) return false; const transform = core_geometry_1.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)); } } exports.ViewManip = ViewManip; /** 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_1.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_1.ToolSettings.viewingInertia.duration; if (this._duration.isTowardsFuture) { // ensure duration is towards future. Otherwise, don't start animation this._end = core_bentley_1.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 - core_bentley_1.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_1.IModelApp.viewManager.grabbingCursor : IModelApp_1.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_1.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 && Tool_1.CoordSource.User === ev.coordsFrom; } } /** ViewingToolHandle for performing the "rotate view" operation */ class ViewRotate extends HandleWithInertia { _frustum = new core_common_1.Frustum(); _activeFrustum = new core_common_1.Frustum(); _anchorPtNpc = new core_geometry_1.Point3d(); get handleType() { return ViewHandleType.Rotate; } getHandleCursor() { return IModelApp_1.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_1.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_1.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 = core_geometry_1.Vector2d.createStartEnd(centerPt, firstPt); const vector1 = core_geometry_1.Vector2d.createStartEnd(centerPt, currPt); angle = vector0.angleTo(vector1); worldAxis = core_geometry_1.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_1.ToolSettings.preserveWorldUp && !vp.viewingGlobe ? (undefined !== this._depthPoint ? vp.view.getUpVector(this._depthPoint) : core_geometry_1.Vector3d.unitZ()) : vp.rotation.getRow(1); // Movement in screen y == rotation about screen X... const yAxis = vp.rotation.getRow(0); const xRMatrix = (xDelta ? core_geometry_1.Matrix3d.createRotationAroundVector(xAxis, core_geometry_1.Angle.createRadians(Math.PI / (viewRect.width / xDelta))) : undefined) ?? core_geometry_1.Matrix3d.identity; const yRMatrix = (yDelta ? core_geometry_1.Matrix3d.createRotationAroundVector(yAxis, core_geometry_1.Angle.createRadians(Math.PI / (viewRect.height / yDelta))) : undefined) ?? core_geometry_1.Matrix3d.identity; const worldRMatrix = yRMatrix.multiplyMatrixMatrix(xRMatrix); const result = worldRMatrix.getAxisAndAngleOfRotation(); angle = core_geometry_1.Angle.createRadians(-result.angle.radians); worldAxis = result.axis; } const worldMatrix = core_geometry_1.Matrix3d.createRotationAr