@itwin/core-frontend
Version:
iTwin.js frontend components
1,045 lines • 199 kB
JavaScript
"use strict";
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module 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