UNPKG

@itwin/core-markup

Version:
770 lines • 35 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 MarkupTools */ Object.defineProperty(exports, "__esModule", { value: true }); exports.SelectTool = exports.MarkupSelected = exports.Handles = exports.ModifyHandle = void 0; const core_bentley_1 = require("@itwin/core-bentley"); const core_geometry_1 = require("@itwin/core-geometry"); const core_frontend_1 = require("@itwin/core-frontend"); const svg_js_1 = require("@svgdotjs/svg.js"); const Markup_1 = require("./Markup"); const MarkupTool_1 = require("./MarkupTool"); const TextEdit_1 = require("./TextEdit"); // cspell:ignore lmultiply untransform unFlash multiselect /** Classes added to HTMLElements so they can be customized in CSS by applications. * A "modify handle" is a visible position on the screen that provides UI to modify a MarkupElement. * @public */ class ModifyHandle { handles; vbToStartTrn; constructor(handles) { this.handles = handles; } async onClick(_ev) { } /** the mouse just went down on this handle, begin modification. */ startDrag(_ev, makeCopy = false) { this.vbToStartTrn = this.handles.vbToBoxTrn.clone(); // save the starting vp -> element box transform this.startModify(makeCopy); } startModify(makeCopy) { const handles = this.handles; const el = handles.el; const cloned = handles.el = el.cloneMarkup(); // make a clone of this element if (makeCopy) { el.after(cloned); } else { cloned.originalEl = el; // save original for undo el.replace(cloned); // put it into the DOM in place of the original } } setMouseHandler(target) { const node = target.node; node.addEventListener("mousedown", (event) => { const ev = event; if (0 === ev.button && undefined === this.handles.active) this.handles.active = this; }); node.addEventListener("touchstart", () => { if (undefined === this.handles.active) this.handles.active = this; }); } addTouchPadding(visible, handles) { if (core_frontend_1.InputSource.Touch !== core_frontend_1.IModelApp.toolAdmin.currentInputState.inputSource) return visible; const padding = visible.cloneMarkup().scale(3).attr("opacity", 0); const g = handles.group.group(); padding.addTo(g); visible.addTo(g); return g; } } exports.ModifyHandle = ModifyHandle; /** A ModifyHandle that changes the size of the element * @public */ class StretchHandle extends ModifyHandle { _circle; posNpc; startPos; opposite; startBox; startCtm; constructor(handles, xy, cursor) { super(handles); this.posNpc = new core_geometry_1.Point2d(xy[0], xy[1]); const props = Markup_1.MarkupApp.props.handles; this._circle = handles.group.circle(props.size).addClass(Markup_1.MarkupApp.stretchHandleClass).attr(props.stretch).attr("cursor", `${cursor}-resize`); // the visible "circle" for this handle this._circle = this.addTouchPadding(this._circle, handles); this.setMouseHandler(this._circle); } setPosition() { const pt = this.handles.npcToVb(this.posNpc); // convert to viewbox coords this._circle.center(pt.x, pt.y); } startDrag(_ev) { const handles = this.handles; this.startCtm = handles.el.screenCTM().lmultiplyO(Markup_1.MarkupApp.screenToVbMtx()); this.startBox = handles.el.bbox(); // save starting size so we can preserve aspect ratio this.startPos = handles.npcToBox(this.posNpc); this.opposite = handles.npcToBox({ x: 1 - this.posNpc.x, y: 1 - this.posNpc.y }); super.startDrag(_ev); } /** perform the stretch. Always stretch element with anchor at the opposite corner of the one being moved. */ modify(ev) { const evPt = Markup_1.MarkupApp.convertVpToVb(ev.viewPoint); // get cursor location in viewbox coords const diff = this.startPos.vectorTo(this.vbToStartTrn.multiplyPoint2d(evPt)); // movement of cursor from start, in viewbox coords const diag = this.startPos.vectorTo(this.opposite).normalize(); // vector from opposite corner to this handle let diagVec = diag.scaleToLength(diff.dotProduct(diag)); // projected distance along diagonal if (diagVec === undefined) diagVec = core_geometry_1.Vector2d.createZero(); // if the shift key is down, don't preserve aspect ratio const adjusted = ev.isShiftKey ? { x: diff.x, y: diff.y } : { x: diagVec.x, y: diagVec.y }; let { x, y, h, w } = this.startBox; if (this.posNpc.x === 0) { x += adjusted.x; // left edge w -= adjusted.x; } else if (this.posNpc.x === 1) { w += adjusted.x; // right edge } if (this.posNpc.y === 0) { y += adjusted.y; // top edge h -= adjusted.y; } else if (this.posNpc.y === 1) { h += adjusted.y; // bottom edge } const mtx = this.startCtm.inverse().scaleO(this.startBox.w / w, this.startBox.h / h, this.opposite.x, this.opposite.y).inverseO(); const minSize = 10; if (w > minSize && h > minSize) // don't let element get too small this.handles.el.markupStretch(w, h, x, y, mtx); } } /** A ModifyHandle to rotate an element * @public */ class RotateHandle extends ModifyHandle { handles; _line; _circle; location; constructor(handles) { super(handles); this.handles = handles; const props = Markup_1.MarkupApp.props.handles; this._line = handles.group.line(0, 0, 1, 1).attr(props.rotateLine).addClass(Markup_1.MarkupApp.rotateLineClass); this._circle = handles.group.circle(props.size * 1.25).attr(props.rotate).addClass(Markup_1.MarkupApp.rotateHandleClass); this._circle = this.addTouchPadding(this._circle, handles); this.setMouseHandler(this._circle); } get centerVb() { return this.handles.npcToVb({ x: .5, y: .5 }); } get anchorVb() { return this.handles.npcToVb({ x: .5, y: 0 }); } setPosition() { const anchor = this.anchorVb; const dir = this.centerVb.vectorTo(anchor).normalize(); const loc = this.location = anchor.plusScaled(dir, Markup_1.MarkupApp.props.handles.size * 3); this._line.plot(anchor.x, anchor.y, loc.x, loc.y); this._circle.center(loc.x, loc.y); } modify(ev) { const centerVp = this.centerVb; const currDir = centerVp.vectorTo(Markup_1.MarkupApp.convertVpToVb(ev.viewPoint)); const dir = centerVp.vectorTo(this.location); this.handles.el.rotate(dir.angleTo(currDir).degrees); } } /** A VertexHandle to move a point on a line * @public */ class VertexHandle extends ModifyHandle { handles; _circle; _x; _y; constructor(handles, index) { super(handles); this.handles = handles; const props = Markup_1.MarkupApp.props.handles; this._circle = handles.group.circle(props.size).attr(props.vertex).addClass(Markup_1.MarkupApp.vertexHandleClass); this._x = `x${index + 1}`; this._y = `y${index + 1}`; this._circle = this.addTouchPadding(this._circle, handles); this.setMouseHandler(this._circle); } setPosition() { let point = new svg_js_1.Point(this.handles.el.attr(this._x), this.handles.el.attr(this._y)); const matrix = this.handles.el.screenCTM().lmultiplyO(Markup_1.MarkupApp.screenToVbMtx()); point = point.transform(matrix); this._circle.center(point.x, point.y); } modify(ev) { let point = new svg_js_1.Point(ev.viewPoint.x, ev.viewPoint.y); const matrix = this.handles.el.screenCTM().inverseO().multiplyO(Markup_1.MarkupApp.getVpToScreenMtx()); point = point.transform(matrix); const el = this.handles.el; el.attr(this._x, point.x); el.attr(this._y, point.y); } } /** A handle that moves (translates) an element. * @public */ class MoveHandle extends ModifyHandle { handles; _shape; _outline; _lastPos; constructor(handles, showBBox) { super(handles); this.handles = handles; const props = Markup_1.MarkupApp.props.handles; const clone = this.handles.el.cloneMarkup(); clone.css(props.move); clone.forElementsOfGroup((child) => child.css(props.move)); if (showBBox) { this._outline = handles.group.polygon().attr(props.moveOutline); const rect = this.handles.el.getOutline().attr(props.move).attr({ fill: "none" }); const group = handles.group.group(); group.add(this._outline); group.add(rect); group.add(clone); this._shape = group; } else { clone.addTo(handles.group); this._shape = clone; } this._shape.addClass(Markup_1.MarkupApp.moveHandleClass); this.setMouseHandler(this._shape); } async onClick(_ev) { const el = this.handles.el; // eslint-disable-next-line @typescript-eslint/no-deprecated if (el instanceof svg_js_1.Text || (el instanceof svg_js_1.G && el.node.className.baseVal === Markup_1.MarkupApp.boxedTextClass)) // if they click on the move handle of a text element, start the text editor await new TextEdit_1.EditTextTool(el).run(); } /** draw the outline of the element's bbox (in viewbox coordinates) */ setPosition() { if (undefined !== this._outline) { const pts = [new core_geometry_1.Point2d(0, 0), new core_geometry_1.Point2d(0, 1), new core_geometry_1.Point2d(1, 1), new core_geometry_1.Point2d(1, 0)]; this._outline.plot(this.handles.npcToVbArray(pts).map((pt) => [pt.x, pt.y])); } } startDrag(ev) { super.startDrag(ev, ev.isShiftKey); this._lastPos = Markup_1.MarkupApp.convertVpToVb(ev.viewPoint); // save stating position in viewbox coordinates } modify(ev) { const evPt = Markup_1.MarkupApp.convertVpToVb(ev.viewPoint); (0, core_bentley_1.assert)(this._lastPos !== undefined); const dist = evPt.minus(this._lastPos); this._lastPos = evPt; this.handles.el.translate(dist.x, dist.y); // move the element } } /** The set of ModifyHandles active. Only applies if there is a single element selected. * @public */ class Handles { ss; el; handles = []; active; dragging = false; group; npcToVbTrn; vbToBoxTrn; constructor(ss, el) { this.ss = ss; this.el = el; this.group = ss.svg.group(); if (el instanceof svg_js_1.Line) { this.handles.push(new MoveHandle(this, false)); this.handles.push(new VertexHandle(this, 0)); this.handles.push(new VertexHandle(this, 1)); this.draw(); // show starting state return; } // move box is in the back this.handles.push(new MoveHandle(this, true)); // then rotate handle this.handles.push(new RotateHandle(this)); // then add all the stretch handles const pts = [[0, 0], [0, .5], [0, 1], [.5, 1], [1, 1], [1, .5], [1, 0], [.5, 0]]; const cursors = ["nw", "w", "sw", "s", "se", "e", "ne", "n"]; const order = [7, 3, 1, 5, 2, 6, 0, 4]; const angle = el.screenCTM().decompose().rotate || 0; const start = Math.round(-angle / 45); // so that we rotate the cursors for rotated elements order.forEach((index) => this.handles.push(new StretchHandle(this, pts[index], cursors[(index + start + 8) % 8]))); this.draw(); // show starting state } npcToBox(p) { const pt = this.npcToVb(p); return this.vbToBox(pt, pt); } npcToVb(p, result) { return this.npcToVbTrn.multiplyPoint2d(p, result); } vbToBox(p, result) { return this.vbToBoxTrn.multiplyPoint2d(p, result); } npcToVbArray(pts) { pts.forEach((pt) => this.npcToVb(pt, pt)); return pts; } draw() { const el = this.el; const bb = el.bbox(); const ctm = el.screenCTM().lmultiplyO(Markup_1.MarkupApp.screenToVbMtx()); this.vbToBoxTrn = ctm.inverse().toIModelTransform(); this.npcToVbTrn = new svg_js_1.Matrix().scaleO(bb.w, bb.h).translateO(bb.x, bb.y).lmultiplyO(ctm).toIModelTransform(); this.handles.forEach((h) => h.setPosition()); } remove() { if (this.dragging) this.cancelDrag(); this.group.remove(); } startDrag(ev) { if (this.active) { this.active.startDrag(ev); this.dragging = true; Markup_1.MarkupApp.markup.disablePick(); core_frontend_1.IModelApp.toolAdmin.setCursor(core_frontend_1.IModelApp.viewManager.dynamicsCursor); } return core_frontend_1.EventHandled.Yes; } drag(ev) { if (this.dragging) { this.active.modify(ev); this.draw(); } } /** complete the modification for the active handle. */ endDrag(undo) { undo.performOperation(Markup_1.MarkupApp.getActionName("modify"), () => { const el = this.el; const original = el.originalEl; // save original element if (original === undefined) { this.ss.emptyAll(); this.ss.add(el); undo.onAdded(el); } else { el.originalEl = undefined; // clear original element undo.onModified(el, original); } }); this.draw(); this.dragging = false; this.active = undefined; Markup_1.MarkupApp.markup.enablePick(); return core_frontend_1.EventHandled.Yes; } /** called when the reset button is pressed. */ cancelDrag() { if (!this.dragging) return; const el = this.el; const original = el.originalEl; if (original) { el.replace(original); this.el = original; } this.draw(); this.active = undefined; Markup_1.MarkupApp.markup.enablePick(); } } exports.Handles = Handles; /** The set of currently selected SVG elements. When elements are added to the set, they are hilited. * @public */ class MarkupSelected { svg; elements = new Set(); handles; /** Called whenever elements are added or removed from this SelectionSet */ onChanged = new core_bentley_1.BeEvent(); get size() { return this.elements.size; } get isEmpty() { return this.size === 0; } has(el) { return this.elements.has(el); } emptyAll() { this.clearEditors(); if (this.isEmpty) return; // Don't send onChanged if already empty. this.elements.forEach((el) => el.unHilite()); this.elements.clear(); this.onChanged.raiseEvent(this); } restart(el) { this.emptyAll(); if (el) this.add(el); } constructor(svg) { this.svg = svg; } clearEditors() { if (this.handles) { this.handles.remove(); this.handles = undefined; } } sizeChanged() { this.clearEditors(); if (this.elements.size === 1) this.handles = new Handles(this, this.elements.values().next().value); this.onChanged.raiseEvent(this); } /** Add a new element to the SS */ add(el) { this.elements.add(el); el.hilite(); this.sizeChanged(); } /** Remove an element from the selection set and unhilite it. * @returns true if the element was in the SS and was removed. */ drop(el) { el.unHilite(); return this.elements.delete(el) ? (this.sizeChanged(), true) : false; } /** Replace an entry in the selection set with a different element. */ replace(oldEl, newEl) { if (this.drop(oldEl)) this.add(newEl); } deleteAll(undo) { undo.performOperation(Markup_1.MarkupApp.getActionName("delete"), () => this.elements.forEach((el) => { undo.onDelete(el); el.remove(); })); this.emptyAll(); } groupAll(undo) { if (this.size < 2) return; const first = this.elements.values().next().value; const parent = first.parent(); const group = parent.group(); const ordered = []; this.elements.forEach((el) => ordered.push(el)); ordered.sort((lhs, rhs) => parent.index(lhs) - parent.index(rhs)); // Preserve relative z ordering undo.performOperation(Markup_1.MarkupApp.getActionName("group"), () => { ordered.forEach((el) => { const oldParent = el.parent(); const oldPos = el.position(); el.unHilite(); undo.onRepositioned(el.addTo(group), oldPos, oldParent); }), undo.onAdded(group); }); this.restart(group); } ungroupAll(undo) { const groups = new Set(); this.elements.forEach((el) => { if (el instanceof svg_js_1.G) groups.add(el); }); if (0 === groups.size) return; undo.performOperation(Markup_1.MarkupApp.getActionName("ungroup"), () => { groups.forEach((g) => { g.unHilite(); this.elements.delete(g); undo.onDelete(g); g.each((index, children) => { const child = children[index]; const oldPos = child.position(); child.toParent(g.parent()); undo.onRepositioned(child, oldPos, g); }, false); g.untransform(); // Don't want undo of ungroup to push the current group transform... g.remove(); }); }); this.sizeChanged(); } /** Move all of the entries to a new position in the DOM via a callback. */ reposition(cmdName, undo, fn) { undo.performOperation(cmdName, () => this.elements.forEach((el) => { const oldParent = el.parent(); const oldPos = el.position(); fn(el); undo.onRepositioned(el, oldPos, oldParent); })); this.sizeChanged(); } } exports.MarkupSelected = MarkupSelected; /** Provides UI for selection, delete, move, copy, bring-to-front, send-to-back, etc. for Markup SVG elements * @public */ class SelectTool extends MarkupTool_1.MarkupTool { static toolId = "Markup.Select"; static iconSpec = "icon-cursor"; _flashedElement; _dragging = []; _anchorPt; _isBoxSelect = false; get flashedElement() { return this._flashedElement; } set flashedElement(el) { if (el === this._flashedElement) return; if (undefined !== this._flashedElement) this._flashedElement.unFlash(); if (undefined !== el) el.flash(); this._flashedElement = el; } unflashSelected() { if (undefined !== this._flashedElement && this.markup.selected.has(this._flashedElement)) this.flashedElement = undefined; } initSelect() { this.markup.setCursor("default"); this.markup.enablePick(); this.flashedElement = undefined; this.boxSelectInit(); } clearSelect() { this.cancelDrag(); this.markup.selected.emptyAll(); } async onCleanup() { this.clearSelect(); } async onPostInstall() { this.initSelect(); return super.onPostInstall(); } async onRestartTool() { this.initSelect(); } showPrompt() { const mainInstruction = core_frontend_1.ToolAssistance.createInstruction(this.iconSpec, core_frontend_1.IModelApp.localization.getLocalizedString(`${MarkupTool_1.MarkupTool.toolKey}Select.Prompts.IdentifyMarkup`)); const mouseInstructions = []; const touchInstructions = []; const acceptMsg = core_frontend_1.IModelApp.localization.getLocalizedString(`${MarkupTool_1.MarkupTool.toolKey}Select.Prompts.AcceptMarkup`); touchInstructions.push(core_frontend_1.ToolAssistance.createInstruction(core_frontend_1.ToolAssistanceImage.OneTouchTap, acceptMsg, false, core_frontend_1.ToolAssistanceInputMethod.Touch)); mouseInstructions.push(core_frontend_1.ToolAssistance.createInstruction(core_frontend_1.ToolAssistanceImage.LeftClick, acceptMsg, false, core_frontend_1.ToolAssistanceInputMethod.Mouse)); touchInstructions.push(core_frontend_1.ToolAssistance.createInstruction(core_frontend_1.ToolAssistanceImage.OneTouchDrag, core_frontend_1.CoreTools.translate("ElementSet.Inputs.BoxCorners"), false, core_frontend_1.ToolAssistanceInputMethod.Touch)); mouseInstructions.push(core_frontend_1.ToolAssistance.createInstruction(core_frontend_1.ToolAssistanceImage.LeftClickDrag, core_frontend_1.CoreTools.translate("ElementSet.Inputs.BoxCorners"), false, core_frontend_1.ToolAssistanceInputMethod.Mouse)); mouseInstructions.push(core_frontend_1.ToolAssistance.createModifierKeyInstruction(core_frontend_1.ToolAssistance.shiftKey, core_frontend_1.ToolAssistanceImage.LeftClickDrag, core_frontend_1.CoreTools.translate("ElementSet.Inputs.OverlapSelection"), false, core_frontend_1.ToolAssistanceInputMethod.Mouse)); mouseInstructions.push(core_frontend_1.ToolAssistance.createModifierKeyInstruction(core_frontend_1.ToolAssistance.ctrlKey, core_frontend_1.ToolAssistanceImage.LeftClick, core_frontend_1.CoreTools.translate("ElementSet.Inputs.InvertSelection"), false, core_frontend_1.ToolAssistanceInputMethod.Mouse)); mouseInstructions.push(core_frontend_1.ToolAssistance.createInstruction(core_frontend_1.ToolAssistanceImage.CursorClick, core_frontend_1.CoreTools.translate("ElementSet.Inputs.ClearSelection"), false, core_frontend_1.ToolAssistanceInputMethod.Mouse)); const sections = []; sections.push(core_frontend_1.ToolAssistance.createSection(mouseInstructions, core_frontend_1.ToolAssistance.inputsLabel)); sections.push(core_frontend_1.ToolAssistance.createSection(touchInstructions, core_frontend_1.ToolAssistance.inputsLabel)); const instructions = core_frontend_1.ToolAssistance.createInstructions(mainInstruction, sections); core_frontend_1.IModelApp.notifications.setToolAssistance(instructions); } /** When we start a drag operation, we add a new set of elements to the DOM and start modifying them. * If we cancel the operation, we need remove them from the DOM. */ cancelDrag() { this._dragging.forEach((el) => el.remove()); // remove temporary elements from DOM this._dragging.length = 0; this.boxSelectInit(); } async onResetButtonUp(_ev) { const selected = this.markup.selected; const handles = selected.handles; if (handles && handles.dragging) handles.cancelDrag(); this.cancelDrag(); selected.sizeChanged(); return core_frontend_1.EventHandled.Yes; } /** Called when there is a mouse "click" (down+up without any motion) */ async onDataButtonUp(ev) { const markup = this.markup; const selected = markup.selected; const handles = selected.handles; if (handles) { if (handles.dragging) return handles.endDrag(markup.undo); if (handles.active) { // clicked on a handle if (ev.isControlKey) selected.drop(handles.el); else await handles.active.onClick(ev); handles.active = undefined; return core_frontend_1.EventHandled.Yes; } } const el = this.flashedElement = this.pickElement(ev.viewPoint); if (ev.isControlKey) { if (el && selected.drop(el)) return core_frontend_1.EventHandled.Yes; } else { selected.emptyAll(); } if (el !== undefined) selected.add(el); return core_frontend_1.EventHandled.Yes; } async onTouchTap(ev) { // Allow tap with a second touch point to multiselect (similar functionality to control being held with mouse click). if (ev.isSingleTap && 2 === ev.touchEvent.touches.length) { const el = this.flashedElement = this.pickElement(ev.viewPoint); if (el) { const selected = this.markup.selected; if (!selected.drop(el)) selected.add(el); return core_frontend_1.EventHandled.Yes; } } return super.onTouchTap(ev); } boxSelectInit() { this._isBoxSelect = false; this.markup.svgDynamics.clear(); } boxSelectStart(ev) { if (!ev.isControlKey) this.markup.selected.emptyAll(); this._anchorPt = Markup_1.MarkupApp.convertVpToVb(ev.viewPoint); this._isBoxSelect = true; return true; } boxSelect(ev, isDynamics) { if (!this._isBoxSelect) return false; const start = this._anchorPt; const end = Markup_1.MarkupApp.convertVpToVb(ev.viewPoint); const vec = start.vectorTo(end); const width = Math.abs(vec.x); const height = Math.abs(vec.y); if (width < 1 || height < 1) return true; const rightToLeft = (start.x > end.x); const overlapMode = (ev.isShiftKey ? !rightToLeft : rightToLeft); // Shift inverts inside/overlap selection... const offset = core_geometry_1.Point3d.create(vec.x < 0 ? end.x : start.x, vec.y < 0 ? end.y : start.y); // define location by corner points... this.markup.svgDynamics.clear(); this.markup.svgDynamics.rect(width, height).move(offset.x, offset.y).css({ "stroke-width": 1, "stroke": "black", "stroke-opacity": 0.5, "fill": "lightBlue", "fill-opacity": 0.2 }); const selectBox = this.markup.svgDynamics.rect(width, height).move(offset.x, offset.y).css({ "stroke-width": 1, "stroke": "white", "stroke-opacity": 1.0, "stroke-dasharray": overlapMode ? "5" : "2", "fill": "none" }); const outlinesG = isDynamics ? this.markup.svgDynamics.group() : undefined; const selectRect = selectBox.node.getBoundingClientRect(); this.markup.svgMarkup.forElementsOfGroup((child) => { const childRect = child.node.getBoundingClientRect(); const inside = (childRect.left >= selectRect.left && childRect.top >= selectRect.top && childRect.right <= selectRect.right && childRect.bottom <= selectRect.bottom); const overlap = !inside && (childRect.left < selectRect.right && childRect.right > selectRect.left && childRect.bottom > selectRect.top && childRect.top < selectRect.bottom); const accept = inside || (overlap && overlapMode); if (undefined !== outlinesG) { if (inside || overlap) { const outline = child.getOutline().attr(Markup_1.MarkupApp.props.handles.moveOutline).addTo(outlinesG); if (accept) outline.attr({ "fill": Markup_1.MarkupApp.props.hilite.flash, "fill-opacity": 0.2 }); } } else if (accept) { this.markup.selected.add(child); } }); if (!isDynamics) this.boxSelectInit(); return true; } /** called when the mouse moves while the data button is down. */ async onMouseStartDrag(ev) { if (core_frontend_1.BeButton.Data !== ev.button) return core_frontend_1.EventHandled.No; const markup = this.markup; const selected = markup.selected; const handles = selected.handles; if (handles && handles.active) { this.flashedElement = undefined; // make sure there are no elements flashed while dragging return handles.startDrag(ev); } const flashed = this.flashedElement = this.pickElement(ev.viewPoint); if (undefined === flashed) return this.boxSelectStart(ev) ? core_frontend_1.EventHandled.Yes : core_frontend_1.EventHandled.No; if (!selected.has(flashed)) selected.restart(flashed); // we clicked on an element not in the selection set, replace current selection with just this element selected.clearEditors(); this._anchorPt = Markup_1.MarkupApp.convertVpToVb(ev.viewPoint); // save the starting point. This is the point where the "down" occurred. this.cancelDrag(); selected.elements.forEach((el) => { const cloned = el.cloneMarkup(); // make a clone of this element el.after(cloned); // put it into the DOM after its original cloned.originalEl = el; // save original element so we can remove it if this is a "move" command this._dragging.push(cloned); // add to dragging set }); return core_frontend_1.EventHandled.Yes; } /** Called whenever the mouse moves while this tool is active. */ async onMouseMotion(ev) { const markup = this.markup; const handles = markup.selected.handles; if (handles && handles.dragging) { this.receivedDownEvent = true; // necessary to tell ToolAdmin to send us the button up event return handles.drag(ev); // drag the handle } if (this._dragging.length === 0) { if (this.boxSelect(ev, true)) return; if (core_frontend_1.InputSource.Touch !== ev.inputSource) this.flashedElement = this.pickElement(ev.viewPoint); // if we're not dragging, try to find an element under the cursor return; } // we have a set of elements being dragged. NOTE: coordinates are viewbox const vbPt = Markup_1.MarkupApp.convertVpToVb(ev.viewPoint); const delta = vbPt.minus(this._anchorPt); this._dragging.forEach((el) => el.translate(delta.x, delta.y)); this._anchorPt = vbPt; // translate moves from last mouse location } /** Called when the mouse goes up after dragging. */ async onMouseEndDrag(ev) { const markup = this.markup; const selected = markup.selected; const handles = selected.handles; if (handles && handles.dragging) // if we have handles up, and if they're in the "dragging" state, send the event to them return handles.endDrag(markup.undo); if (this._dragging.length === 0) return this.boxSelect(ev, false) ? core_frontend_1.EventHandled.Yes : core_frontend_1.EventHandled.No; // NOTE: all units should be in viewbox coordinates const delta = Markup_1.MarkupApp.convertVpToVb(ev.viewPoint).minus(this._anchorPt); const undo = markup.undo; if (ev.isShiftKey) // shift key means "add to existing," otherwise new selection replaces old selected.emptyAll(); // move or copy all of the elements in dragged set undo.performOperation(Markup_1.MarkupApp.getActionName("copy"), () => this._dragging.forEach((el) => { el.translate(delta.x, delta.y); // move to final location const original = el.originalEl; // save original element el.originalEl = undefined; // clear original element if (ev.isShiftKey) { selected.add(el); undo.onAdded(el); // shift key means copy element } else { original.replace(el); undo.onModified(el, original); } })); this._dragging.length = 0; // empty dragging set selected.sizeChanged(); // notify that size of selection set changed return core_frontend_1.EventHandled.Yes; } /** called when a modifier key is pressed or released. Updates stretch handles, if present */ async onModifierKeyTransition(_wentDown, modifier, _event) { if (modifier !== core_frontend_1.BeModifierKeys.Shift) // we only care about the shift key return core_frontend_1.EventHandled.No; const selected = this.markup.selected; const handles = selected.handles; if (undefined === handles || !handles.dragging) // and only if we're currently dragging return core_frontend_1.EventHandled.No; const ev = new core_frontend_1.BeButtonEvent(); // we need to simulate a mouse motion by sending a drag event at the last cursor position core_frontend_1.IModelApp.toolAdmin.fillEventFromCursorLocation(ev); return (undefined === ev.viewport) ? core_frontend_1.EventHandled.No : (handles.drag(ev), core_frontend_1.EventHandled.Yes); } /** called whenever a key is pressed while this tool is active. */ async onKeyTransition(wentDown, key) { if (!wentDown) return core_frontend_1.EventHandled.No; const markup = this.markup; switch (key.key.toLowerCase()) { case "delete": // delete key or backspace = delete current selection set case "backspace": this.unflashSelected(); markup.deleteSelected(); return core_frontend_1.EventHandled.Yes; case "escape": // esc = cancel current operation await this.exitTool(); return core_frontend_1.EventHandled.Yes; case "b": // alt-shift-b = send to back return (key.altKey && key.shiftKey) ? (markup.sendToBack(), core_frontend_1.EventHandled.Yes) : core_frontend_1.EventHandled.No; case "f": // alt-shift-f = bring to front return (key.altKey && key.shiftKey) ? (markup.bringToFront(), core_frontend_1.EventHandled.Yes) : core_frontend_1.EventHandled.No; case "g": // ctrl-g = create group return (key.ctrlKey) ? (this.unflashSelected(), markup.groupSelected(), core_frontend_1.EventHandled.Yes) : core_frontend_1.EventHandled.No; case "u": // ctrl-u = ungroup return (key.ctrlKey) ? (this.unflashSelected(), markup.ungroupSelected(), core_frontend_1.EventHandled.Yes) : core_frontend_1.EventHandled.No; } return core_frontend_1.EventHandled.No; } } exports.SelectTool = SelectTool; //# sourceMappingURL=SelectTool.js.map