diff --git a/common/api/imodeljs-frontend.api.md b/common/api/imodeljs-frontend.api.md index a53ac3c19f6f..5cc08dd61f89 100644 --- a/common/api/imodeljs-frontend.api.md +++ b/common/api/imodeljs-frontend.api.md @@ -941,7 +941,9 @@ export class AccuSnap implements Decorator { readonly toolState: AccuSnap.ToolState; // @internal (undocumented) touchCursor?: TouchCursor; - } + // @internal (undocumented) + get wantVirtualCursor(): boolean; +} // @public (undocumented) export namespace AccuSnap { @@ -10852,6 +10854,8 @@ export class ToolSettings { static doubleClickTimeout: BeDuration; static doubleClickToleranceInches: number; static doubleTapTimeout: BeDuration; + // @beta + static enableVirtualCursorForLocate: boolean; static preserveWorldUp: boolean; static scrollSpeed: number; static startDragDelay: BeDuration; diff --git a/common/changes/@bentley/imodeljs-frontend/virtual-cursor-for-locate_2021-03-25-15-39.json b/common/changes/@bentley/imodeljs-frontend/virtual-cursor-for-locate_2021-03-25-15-39.json new file mode 100644 index 000000000000..9c0b09f7dd78 --- /dev/null +++ b/common/changes/@bentley/imodeljs-frontend/virtual-cursor-for-locate_2021-03-25-15-39.json @@ -0,0 +1,11 @@ +{ + "changes": [ + { + "packageName": "@bentley/imodeljs-frontend", + "comment": "Add an option to use the virtual cursor to help with element locate w/touch input.", + "type": "none" + } + ], + "packageName": "@bentley/imodeljs-frontend", + "email": "65233531+bbastings@users.noreply.github.com" +} \ No newline at end of file diff --git a/core/frontend/src/AccuSnap.ts b/core/frontend/src/AccuSnap.ts index 9d0fc5608811..914edf2d4654 100644 --- a/core/frontend/src/AccuSnap.ts +++ b/core/frontend/src/AccuSnap.ts @@ -15,6 +15,7 @@ import { IModelApp } from "./IModelApp"; import { CanvasDecoration } from "./render/CanvasDecoration"; import { IconSprites, Sprite, SpriteLocation } from "./Sprites"; import { BeButton, BeButtonEvent, BeTouchEvent, InputSource } from "./tools/Tool"; +import { ToolSettings } from "./tools/ToolSettings"; import { DecorateContext } from "./ViewContext"; import { Decorator } from "./ViewManager"; import { ScreenViewport, Viewport } from "./Viewport"; @@ -125,6 +126,8 @@ export class TouchCursor implements CanvasDecoration { } public doTouchEnd(ev: BeTouchEvent): void { + if (this._isDragging && undefined !== ev.viewport) + IModelApp.toolAdmin.currentInputState.fromPoint(ev.viewport, this._offsetPosition, InputSource.Touch); // Current location should reflect virtual cursor offset... this._isSelected = this._isDragging = false; if (undefined !== ev.viewport) ev.viewport.invalidateDecorations(); @@ -566,7 +569,7 @@ export class AccuSnap implements Decorator { this.toolState.enabled = yesNo; if (!yesNo) { this.clear(); - if (undefined !== this.touchCursor) { + if (undefined !== this.touchCursor && !this.wantVirtualCursor) { this.touchCursor = undefined; IModelApp.viewManager.invalidateDecorationsAllViews(); } @@ -954,11 +957,16 @@ export class AccuSnap implements Decorator { /** @internal */ public onTouchMoveStart(ev: BeTouchEvent, startEv: BeTouchEvent): boolean { return (undefined !== this.touchCursor) ? this.touchCursor.doTouchMoveStart(ev, startEv) : false; } + /** @internal */ + public get wantVirtualCursor(): boolean { + return this._doSnapping || (this.isLocateEnabled && ToolSettings.enableVirtualCursorForLocate); + } + /** @internal */ public async onTouchTap(ev: BeTouchEvent): Promise { if (undefined !== this.touchCursor) return this.touchCursor.doTouchTap(ev); - if (!this._doSnapping) + if (!this.wantVirtualCursor) return false; this.touchCursor = TouchCursor.createFromTouchTap(ev); if (undefined === this.touchCursor) @@ -1018,7 +1026,13 @@ export class AccuSnap implements Decorator { /** Enable locating elements. * @public */ - public enableLocate(yesNo: boolean) { this.toolState.locate = yesNo; } + public enableLocate(yesNo: boolean) { + this.toolState.locate = yesNo; + if (!yesNo && undefined !== this.touchCursor && !this.wantVirtualCursor) { + this.touchCursor = undefined; + IModelApp.viewManager.invalidateDecorationsAllViews(); + } + } /** Called whenever a new [[Tool]] is started. * @internal diff --git a/core/frontend/src/tools/SelectTool.ts b/core/frontend/src/tools/SelectTool.ts index 865378952a37..5cccb5d2183f 100644 --- a/core/frontend/src/tools/SelectTool.ts +++ b/core/frontend/src/tools/SelectTool.ts @@ -190,7 +190,8 @@ export class SelectionTool extends PrimitiveTool { sections.push(ToolAssistance.createSection(mousePickInstructions, ToolAssistance.inputsLabel)); const touchPickInstructions: ToolAssistanceInstruction[] = []; - touchPickInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.OneTouchTap, CoreTools.translate("ElementSet.Inputs.AcceptElement"), false, ToolAssistanceInputMethod.Touch)); + if (!ToolAssistance.createTouchCursorInstructions(touchPickInstructions)) + touchPickInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.OneTouchTap, CoreTools.translate("ElementSet.Inputs.AcceptElement"), false, ToolAssistanceInputMethod.Touch)); sections.push(ToolAssistance.createSection(touchPickInstructions, ToolAssistance.inputsLabel)); break; case SelectionMethod.Line: diff --git a/core/frontend/src/tools/ToolAdmin.ts b/core/frontend/src/tools/ToolAdmin.ts index 6e6a2c411cdb..924b2da3ea27 100644 --- a/core/frontend/src/tools/ToolAdmin.ts +++ b/core/frontend/src/tools/ToolAdmin.ts @@ -1573,7 +1573,15 @@ export class ToolAdmin { viewport.drawLocateCursor(context, ev.viewPoint, viewport.pixelsFromInches(IModelApp.locateManager.apertureInches), this.isLocateCircleOn, hit); } - public get isLocateCircleOn(): boolean { return this.toolState.locateCircleOn && this.currentInputState.inputSource === InputSource.Mouse && this._canvasDecoration === undefined; } + public get isLocateCircleOn(): boolean { + if (!this.toolState.locateCircleOn || undefined !== this._canvasDecoration) + return false; + + if (InputSource.Mouse === this.currentInputState.inputSource) + return true; + + return (InputSource.Touch === this.currentInputState.inputSource && undefined !== IModelApp.accuSnap.touchCursor); + } /** @internal */ public beginDynamics(): void { diff --git a/core/frontend/src/tools/ToolAssistance.ts b/core/frontend/src/tools/ToolAssistance.ts index acaab32a77a0..c8516476833e 100644 --- a/core/frontend/src/tools/ToolAssistance.ts +++ b/core/frontend/src/tools/ToolAssistance.ts @@ -262,7 +262,7 @@ export class ToolAssistance { */ public static createTouchCursorInstructions(instructions: ToolAssistanceInstruction[]): boolean { const accuSnap = IModelApp.accuSnap; - if (accuSnap.isSnapEnabled && accuSnap.isSnapEnabledByUser && undefined === accuSnap.touchCursor) { + if (undefined === accuSnap.touchCursor && accuSnap.wantVirtualCursor) { instructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.OneTouchTap, this.translateTouch("Activate"), false, ToolAssistanceInputMethod.Touch)); return true; } else if (undefined !== accuSnap.touchCursor) { diff --git a/core/frontend/src/tools/ToolSettings.ts b/core/frontend/src/tools/ToolSettings.ts index 8fd42bedbf3f..8aa6512c258b 100644 --- a/core/frontend/src/tools/ToolSettings.ts +++ b/core/frontend/src/tools/ToolSettings.ts @@ -19,6 +19,8 @@ export class ToolSettings { public static doubleClickTimeout = BeDuration.fromMilliseconds(500); /** Number of screen inches of movement allowed between clicks to still qualify as a double-click. */ public static doubleClickToleranceInches = 0.05; + /** @beta Use virtual cursor to help with locating elements using touch input. By default it's only enabled for snapping. */ + public static enableVirtualCursorForLocate = false; /** If true, view rotation tool keeps the up vector (worldZ) aligned with screenY. */ public static preserveWorldUp = true; /** Delay with a touch on the surface before a move operation begins. */ diff --git a/test-apps/display-test-app/README.md b/test-apps/display-test-app/README.md index aeec97319f75..fb497775e635 100644 --- a/test-apps/display-test-app/README.md +++ b/test-apps/display-test-app/README.md @@ -201,6 +201,11 @@ display-test-app has access to all key-ins defined in the imodeljs-frontend and * `background=`: Preserve background color when drawing as a raster image. * **dta aspect skew decorator** *apply=0|1* - Toggle a decorator that draws a simple bspline curve based on the project extents, for testing the effect of aspect ratio skew on the curve stroke tolerance. Use in conjunction with `fdt aspect skew` to adjust the skew. If `apply` is 0, then the skew will have no effect on the curve's level of detail; otherwise a higher aspect ratio skew should produce higher-resolution curve graphics. * **dta classifyclip selected** *inside* - Color code elements from the current selection set based on their containment with the current view clip. Inside - Green, Outside - Red, Overlap - Blue. Specify optional inside arg to only determine inside or outside, not overlap. Disable clip in the view settings to select elements outside clip, use clip tool panel EDIT button to redisplay clip decoration after processing selection. Use key-in again without a clip or selection set to clear the color override. +* **dta grid settings** - Change the grid settings for the selected viewport. + * `spacing=number` Specify x and y grid reference line spacing in meters. + * `ratio=number` Specify y spacing as current x * ratio. + * `gridsPerRef=number` Specify number of grid lines to display per reference line. + * `orientation=0|1|2|3|4` Value for GridOrientationType. * **dta model transform** - Apply a display transform to all models currently displayed in the selected viewport. Origin is specified like `x=1 y=2 z=3`; pitch and roll as `p=45 r=90` in degrees. Any argument can be omitted. Omitting all arguments clears the display transform. Snapping intentionally does not take the display transform into account. ## Editing @@ -217,9 +222,7 @@ Using an editing session is optional, but outside of a session, the viewport's g ### Editing key-ins +display-test-app has access to all key-ins defined in the imodeljs-editor-frontend package. It also provides the following additional key-ins. + * `dta edit` - begin a new editing session, or end the current editing session. The title of the window or browser tab will update to reflect the current state: "[R/W]" indicating no current editing session, or "[EDIT]" indicating an active editing session. -* `dta delete elements` - delete all elements currently in the selection set. -* `dta move elements` - start moving elements. If no elements are currently in the selection set, you will be prompted to select one. First data point defines the start point; second defines the end point and moves the element(s) by the delta between the two points. * `dta place line string` - start placing a line string. Each data point defines another point in the string; a reset (right mouse button) finishes. The element is placed into the first spatial model and spatial category in the viewport's model and category selectors. -* `dta undo` - undo the most recent change. -* `dta redo` - redo the most recently-undone change. diff --git a/test-apps/display-test-app/public/locales/en/SVTTools.json b/test-apps/display-test-app/public/locales/en/SVTTools.json index 1c9147662507..0a57c0e67e0e 100644 --- a/test-apps/display-test-app/public/locales/en/SVTTools.json +++ b/test-apps/display-test-app/public/locales/en/SVTTools.json @@ -10,6 +10,9 @@ "keyin": "dta classifyclip selected" } }, + "GridSettings": { + "keyin": "dta grid settings" + }, "Markup": { "keyin": "dta markup", "TestSelect": { diff --git a/test-apps/display-test-app/src/frontend/App.ts b/test-apps/display-test-app/src/frontend/App.ts index 1567258aeb25..62e603e0dd70 100644 --- a/test-apps/display-test-app/src/frontend/App.ts +++ b/test-apps/display-test-app/src/frontend/App.ts @@ -25,6 +25,7 @@ import { DrawingAidTestTool } from "./DrawingAidTestTool"; import { EditingSessionTool, PlaceLineStringTool } from "./EditingTools"; import { FenceClassifySelectedTool } from "./Fence"; import { RecordFpsTool } from "./FpsMonitor"; +import { ChangeGridSettingsTool } from "./Grid"; import { IncidentMarkerDemoTool } from "./IncidentMarkerDemo"; import { MarkupSelectTestTool } from "./MarkupSelectTestTool"; import { Notifications } from "./Notifications"; @@ -213,6 +214,7 @@ export class DisplayTestApp { [ ApplyModelDisplayScaleTool, ApplyModelTransformTool, + ChangeGridSettingsTool, CloneViewportTool, CloseIModelTool, CloseWindowTool, diff --git a/test-apps/display-test-app/src/frontend/Grid.ts b/test-apps/display-test-app/src/frontend/Grid.ts new file mode 100644 index 000000000000..f99365bd5d97 --- /dev/null +++ b/test-apps/display-test-app/src/frontend/Grid.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- +* Copyright (c) Bentley Systems, Incorporated. All rights reserved. +* See LICENSE.md in the project root for license terms and full copyright notice. +*--------------------------------------------------------------------------------------------*/ + +import { parseArgs } from "@bentley/frontend-devtools"; +import { GridOrientationType } from "@bentley/imodeljs-common"; +import { IModelApp, Tool } from "@bentley/imodeljs-frontend"; + +/** Change grid settings for testing. */ +export class ChangeGridSettingsTool extends Tool { + public static toolId = "GridSettings"; + public static get minArgs() { return 0; } + public static get maxArgs() { return 4; } + + public run(spacing?: number, ratio?: number, gridsPerRef?: number, orientation?: GridOrientationType): boolean { + const vp = IModelApp.viewManager.selectedView; + if (undefined === vp) + return false; + + if (undefined !== spacing) + vp.view.details.gridSpacing = { x: spacing, y: spacing }; + + if (undefined !== ratio) + vp.view.details.gridSpacing = { x: vp.view.details.gridSpacing.x, y: vp.view.details.gridSpacing.x * ratio }; + + if (undefined !== gridsPerRef) + vp.view.details.gridsPerRef = gridsPerRef; + + if (undefined !== orientation) + vp.view.details.gridOrientation = orientation; + + vp.invalidateScene(); // Needed to clear cached grid decoration... + return true; + } + + /** The keyin accepts the following arguments: + * - `spacing=number` Specify x and y grid reference line spacing in meters. + * - `ratio=number` Specify y spacing as current x * ratio. + * - `gridsPerRef=number` Specify number of grid lines to display per reference line. + * - `orientation=0|1|2|3|4` Value for GridOrientationType. + */ + public parseAndRun(...inputArgs: string[]): boolean { + let spacing; + let ratio; + let gridsPerRef; + let orientation; + const args = parseArgs(inputArgs); + + const spacingArg = args.getFloat("s"); + if (undefined !== spacingArg) + spacing = spacingArg; + + const ratioArg = args.getFloat("r"); + if (undefined !== ratioArg) + ratio = ratioArg; + + const gridsPerRefArg = args.getInteger("g"); + if (undefined !== gridsPerRefArg) + gridsPerRef = gridsPerRefArg; + + const orientationArg = args.getInteger("o"); + if (undefined !== orientationArg) { + switch (orientationArg) { + case 0: + orientation = GridOrientationType.View; + break; + case 1: + orientation = GridOrientationType.WorldXY; + break; + case 2: + orientation = GridOrientationType.WorldYZ; + break; + case 3: + orientation = GridOrientationType.WorldXZ; + break; + case 4: + orientation = GridOrientationType.AuxCoord; + break; + } + } + + this.run(spacing, ratio, gridsPerRef, orientation); + return true; + } +}