@itwin/core-markup 4.0.0-dev.8 → 4.0.0-dev.81

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +41 -1
  2. package/lib/cjs/Markup.d.ts +323 -310
  3. package/lib/cjs/Markup.d.ts.map +1 -1
  4. package/lib/cjs/Markup.js +451 -420
  5. package/lib/cjs/Markup.js.map +1 -1
  6. package/lib/cjs/MarkupTool.d.ts +38 -38
  7. package/lib/cjs/MarkupTool.js +88 -88
  8. package/lib/cjs/MarkupTool.js.map +1 -1
  9. package/lib/cjs/RedlineTool.d.ts +145 -145
  10. package/lib/cjs/RedlineTool.d.ts.map +1 -1
  11. package/lib/cjs/RedlineTool.js +498 -512
  12. package/lib/cjs/RedlineTool.js.map +1 -1
  13. package/lib/cjs/SelectTool.d.ts +126 -126
  14. package/lib/cjs/SelectTool.js +741 -741
  15. package/lib/cjs/SelectTool.js.map +1 -1
  16. package/lib/cjs/SvgJsExt.d.ts +85 -85
  17. package/lib/cjs/SvgJsExt.js +185 -185
  18. package/lib/cjs/TextEdit.d.ts +43 -43
  19. package/lib/cjs/TextEdit.js +196 -196
  20. package/lib/cjs/TextEdit.js.map +1 -1
  21. package/lib/cjs/Undo.d.ts +46 -46
  22. package/lib/cjs/Undo.js +168 -168
  23. package/lib/cjs/core-markup.d.ts +18 -18
  24. package/lib/cjs/core-markup.js +38 -34
  25. package/lib/cjs/core-markup.js.map +1 -1
  26. package/lib/esm/Markup.d.ts +323 -310
  27. package/lib/esm/Markup.d.ts.map +1 -1
  28. package/lib/esm/Markup.js +447 -415
  29. package/lib/esm/Markup.js.map +1 -1
  30. package/lib/esm/MarkupTool.d.ts +38 -38
  31. package/lib/esm/MarkupTool.js +85 -84
  32. package/lib/esm/MarkupTool.js.map +1 -1
  33. package/lib/esm/RedlineTool.d.ts +145 -145
  34. package/lib/esm/RedlineTool.d.ts.map +1 -1
  35. package/lib/esm/RedlineTool.js +494 -498
  36. package/lib/esm/RedlineTool.js.map +1 -1
  37. package/lib/esm/SelectTool.d.ts +126 -126
  38. package/lib/esm/SelectTool.js +735 -734
  39. package/lib/esm/SelectTool.js.map +1 -1
  40. package/lib/esm/SvgJsExt.d.ts +85 -85
  41. package/lib/esm/SvgJsExt.js +180 -180
  42. package/lib/esm/TextEdit.d.ts +43 -43
  43. package/lib/esm/TextEdit.js +193 -191
  44. package/lib/esm/TextEdit.js.map +1 -1
  45. package/lib/esm/Undo.d.ts +46 -46
  46. package/lib/esm/Undo.js +164 -164
  47. package/lib/esm/core-markup.d.ts +18 -18
  48. package/lib/esm/core-markup.js +22 -22
  49. package/package.json +19 -19
@@ -1,735 +1,736 @@
1
- /*---------------------------------------------------------------------------------------------
2
- * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
3
- * See LICENSE.md in the project root for license terms and full copyright notice.
4
- *--------------------------------------------------------------------------------------------*/
5
- /** @packageDocumentation
6
- * @module MarkupTools
7
- */
8
- import { BeEvent } from "@itwin/core-bentley";
9
- import { Point2d, Point3d, Vector2d } from "@itwin/core-geometry";
10
- import { BeButton, BeButtonEvent, BeModifierKeys, CoreTools, EventHandled, IModelApp, InputSource, ToolAssistance, ToolAssistanceImage, ToolAssistanceInputMethod, } from "@itwin/core-frontend";
11
- import { G, Line, Text as MarkupText, Matrix, Point } from "@svgdotjs/svg.js";
12
- import { MarkupApp } from "./Markup";
13
- import { MarkupTool } from "./MarkupTool";
14
- import { EditTextTool } from "./TextEdit";
15
- // cspell:ignore lmultiply untransform unFlash multiselect
16
- /** Classes added to HTMLElements so they can be customized in CSS by applications.
17
- * A "modify handle" is a visible position on the screen that provides UI to modify a MarkupElement.
18
- * @public
19
- */
20
- export class ModifyHandle {
21
- constructor(handles) {
22
- this.handles = handles;
23
- }
24
- async onClick(_ev) { }
25
- /** the mouse just went down on this handle, begin modification. */
26
- startDrag(_ev, makeCopy = false) {
27
- this.vbToStartTrn = this.handles.vbToBoxTrn.clone(); // save the starting vp -> element box transform
28
- this.startModify(makeCopy);
29
- }
30
- startModify(makeCopy) {
31
- const handles = this.handles;
32
- const el = handles.el;
33
- const cloned = handles.el = el.cloneMarkup(); // make a clone of this element
34
- if (makeCopy) {
35
- el.after(cloned);
36
- }
37
- else {
38
- cloned.originalEl = el; // save original for undo
39
- el.replace(cloned); // put it into the DOM in place of the original
40
- }
41
- }
42
- setMouseHandler(target) {
43
- const node = target.node;
44
- node.addEventListener("mousedown", (event) => {
45
- const ev = event;
46
- if (0 === ev.button && undefined === this.handles.active)
47
- this.handles.active = this;
48
- });
49
- node.addEventListener("touchstart", () => {
50
- if (undefined === this.handles.active)
51
- this.handles.active = this;
52
- });
53
- }
54
- addTouchPadding(visible, handles) {
55
- if (InputSource.Touch !== IModelApp.toolAdmin.currentInputState.inputSource)
56
- return visible;
57
- const padding = visible.cloneMarkup().scale(3).attr("opacity", 0);
58
- const g = handles.group.group();
59
- padding.addTo(g);
60
- visible.addTo(g);
61
- return g;
62
- }
63
- }
64
- /** A ModifyHandle that changes the size of the element
65
- * @public
66
- */
67
- class StretchHandle extends ModifyHandle {
68
- constructor(handles, xy, cursor) {
69
- super(handles);
70
- this.posNpc = new Point2d(xy[0], xy[1]);
71
- const props = MarkupApp.props.handles;
72
- this._circle = handles.group.circle(props.size).addClass(MarkupApp.stretchHandleClass).attr(props.stretch).attr("cursor", `${cursor}-resize`); // the visible "circle" for this handle
73
- this._circle = this.addTouchPadding(this._circle, handles);
74
- this.setMouseHandler(this._circle);
75
- }
76
- setPosition() {
77
- const pt = this.handles.npcToVb(this.posNpc); // convert to viewbox coords
78
- this._circle.center(pt.x, pt.y);
79
- }
80
- startDrag(_ev) {
81
- const handles = this.handles;
82
- this.startCtm = handles.el.screenCTM().lmultiplyO(MarkupApp.screenToVbMtx());
83
- this.startBox = handles.el.bbox(); // save starting size so we can preserve aspect ratio
84
- this.startPos = handles.npcToBox(this.posNpc);
85
- this.opposite = handles.npcToBox({ x: 1 - this.posNpc.x, y: 1 - this.posNpc.y });
86
- super.startDrag(_ev);
87
- }
88
- /** perform the stretch. Always stretch element with anchor at the opposite corner of the one being moved. */
89
- modify(ev) {
90
- const evPt = MarkupApp.convertVpToVb(ev.viewPoint); // get cursor location in viewbox coords
91
- const diff = this.startPos.vectorTo(this.vbToStartTrn.multiplyPoint2d(evPt)); // movement of cursor from start, in viewbox coords
92
- const diag = this.startPos.vectorTo(this.opposite).normalize(); // vector from opposite corner to this handle
93
- let diagVec = diag.scaleToLength(diff.dotProduct(diag)); // projected distance along diagonal
94
- if (diagVec === undefined)
95
- diagVec = Vector2d.createZero();
96
- // if the shift key is down, don't preserve aspect ratio
97
- const adjusted = ev.isShiftKey ? { x: diff.x, y: diff.y } : { x: diagVec.x, y: diagVec.y };
98
- let { x, y, h, w } = this.startBox;
99
- if (this.posNpc.x === 0) {
100
- x += adjusted.x; // left edge
101
- w -= adjusted.x;
102
- }
103
- else if (this.posNpc.x === 1) {
104
- w += adjusted.x; // right edge
105
- }
106
- if (this.posNpc.y === 0) {
107
- y += adjusted.y; // top edge
108
- h -= adjusted.y;
109
- }
110
- else if (this.posNpc.y === 1) {
111
- h += adjusted.y; // bottom edge
112
- }
113
- const mtx = this.startCtm.inverse().scaleO(this.startBox.w / w, this.startBox.h / h, this.opposite.x, this.opposite.y).inverseO();
114
- const minSize = 10;
115
- if (w > minSize && h > minSize) // don't let element get too small
116
- this.handles.el.markupStretch(w, h, x, y, mtx);
117
- }
118
- }
119
- /** A ModifyHandle to rotate an element
120
- * @public
121
- */
122
- class RotateHandle extends ModifyHandle {
123
- constructor(handles) {
124
- super(handles);
125
- this.handles = handles;
126
- const props = MarkupApp.props.handles;
127
- this._line = handles.group.line(0, 0, 1, 1).attr(props.rotateLine).addClass(MarkupApp.rotateLineClass);
128
- this._circle = handles.group.circle(props.size * 1.25).attr(props.rotate).addClass(MarkupApp.rotateHandleClass);
129
- this._circle = this.addTouchPadding(this._circle, handles);
130
- this.setMouseHandler(this._circle);
131
- }
132
- get centerVb() { return this.handles.npcToVb({ x: .5, y: .5 }); }
133
- get anchorVb() { return this.handles.npcToVb({ x: .5, y: 0 }); }
134
- setPosition() {
135
- const anchor = this.anchorVb;
136
- const dir = this.centerVb.vectorTo(anchor).normalize();
137
- const loc = this.location = anchor.plusScaled(dir, MarkupApp.props.handles.size * 3);
138
- this._line.plot(anchor.x, anchor.y, loc.x, loc.y);
139
- this._circle.center(loc.x, loc.y);
140
- }
141
- modify(ev) {
142
- const centerVp = this.centerVb;
143
- const currDir = centerVp.vectorTo(MarkupApp.convertVpToVb(ev.viewPoint));
144
- const dir = centerVp.vectorTo(this.location);
145
- this.handles.el.rotate(dir.angleTo(currDir).degrees);
146
- }
147
- }
148
- /** A VertexHandle to move a point on a line
149
- * @public
150
- */
151
- class VertexHandle extends ModifyHandle {
152
- constructor(handles, index) {
153
- super(handles);
154
- this.handles = handles;
155
- const props = MarkupApp.props.handles;
156
- this._circle = handles.group.circle(props.size).attr(props.vertex).addClass(MarkupApp.vertexHandleClass);
157
- this._x = `x${index + 1}`;
158
- this._y = `y${index + 1}`;
159
- this._circle = this.addTouchPadding(this._circle, handles);
160
- this.setMouseHandler(this._circle);
161
- }
162
- setPosition() {
163
- let point = new Point(this.handles.el.attr(this._x), this.handles.el.attr(this._y));
164
- const matrix = this.handles.el.screenCTM().lmultiplyO(MarkupApp.screenToVbMtx());
165
- point = point.transform(matrix);
166
- this._circle.center(point.x, point.y);
167
- }
168
- modify(ev) {
169
- let point = new Point(ev.viewPoint.x, ev.viewPoint.y);
170
- const matrix = this.handles.el.screenCTM().inverseO().multiplyO(MarkupApp.getVpToScreenMtx());
171
- point = point.transform(matrix);
172
- const el = this.handles.el;
173
- el.attr(this._x, point.x);
174
- el.attr(this._y, point.y);
175
- }
176
- }
177
- /** A handle that moves (translates) an element.
178
- * @public
179
- */
180
- class MoveHandle extends ModifyHandle {
181
- constructor(handles, showBBox) {
182
- super(handles);
183
- this.handles = handles;
184
- const props = MarkupApp.props.handles;
185
- const clone = this.handles.el.cloneMarkup();
186
- clone.css(props.move);
187
- clone.forElementsOfGroup((child) => child.css(props.move));
188
- if (showBBox) {
189
- this._outline = handles.group.polygon().attr(props.moveOutline);
190
- const rect = this.handles.el.getOutline().attr(props.move).attr({ fill: "none" });
191
- const group = handles.group.group();
192
- group.add(this._outline);
193
- group.add(rect);
194
- group.add(clone);
195
- this._shape = group;
196
- }
197
- else {
198
- clone.addTo(handles.group);
199
- this._shape = clone;
200
- }
201
- this._shape.addClass(MarkupApp.moveHandleClass);
202
- this.setMouseHandler(this._shape);
203
- }
204
- async onClick(_ev) {
205
- const el = this.handles.el;
206
- // eslint-disable-next-line deprecation/deprecation
207
- if (el instanceof MarkupText || (el instanceof G && el.node.className.baseVal === MarkupApp.boxedTextClass)) // if they click on the move handle of a text element, start the text editor
208
- await new EditTextTool(el).run();
209
- }
210
- /** draw the outline of the element's bbox (in viewbox coordinates) */
211
- setPosition() {
212
- if (undefined !== this._outline) {
213
- const pts = [new Point2d(0, 0), new Point2d(0, 1), new Point2d(1, 1), new Point2d(1, 0)];
214
- this._outline.plot(this.handles.npcToVbArray(pts).map((pt) => [pt.x, pt.y]));
215
- }
216
- }
217
- startDrag(ev) {
218
- super.startDrag(ev, ev.isShiftKey);
219
- this._lastPos = MarkupApp.convertVpToVb(ev.viewPoint); // save stating position in viewbox coordinates
220
- }
221
- modify(ev) {
222
- const evPt = MarkupApp.convertVpToVb(ev.viewPoint);
223
- const dist = evPt.minus(this._lastPos);
224
- this._lastPos = evPt;
225
- this.handles.el.translate(dist.x, dist.y); // move the element
226
- }
227
- }
228
- /** The set of ModifyHandles active. Only applies if there is a single element selected.
229
- * @public
230
- */
231
- export class Handles {
232
- constructor(ss, el) {
233
- this.ss = ss;
234
- this.el = el;
235
- this.handles = [];
236
- this.dragging = false;
237
- this.group = ss.svg.group();
238
- if (el instanceof Line) {
239
- this.handles.push(new MoveHandle(this, false));
240
- this.handles.push(new VertexHandle(this, 0));
241
- this.handles.push(new VertexHandle(this, 1));
242
- this.draw(); // show starting state
243
- return;
244
- }
245
- // move box is in the back
246
- this.handles.push(new MoveHandle(this, true));
247
- // then rotate handle
248
- this.handles.push(new RotateHandle(this));
249
- // then add all the stretch handles
250
- const pts = [[0, 0], [0, .5], [0, 1], [.5, 1], [1, 1], [1, .5], [1, 0], [.5, 0]];
251
- const cursors = ["nw", "w", "sw", "s", "se", "e", "ne", "n"];
252
- const order = [7, 3, 1, 5, 2, 6, 0, 4];
253
- const angle = el.screenCTM().decompose().rotate || 0;
254
- const start = Math.round(-angle / 45); // so that we rotate the cursors for rotated elements
255
- order.forEach((index) => this.handles.push(new StretchHandle(this, pts[index], cursors[(index + start + 8) % 8])));
256
- this.draw(); // show starting state
257
- }
258
- npcToBox(p) {
259
- const pt = this.npcToVb(p);
260
- return this.vbToBox(pt, pt);
261
- }
262
- npcToVb(p, result) { return this.npcToVbTrn.multiplyPoint2d(p, result); }
263
- vbToBox(p, result) { return this.vbToBoxTrn.multiplyPoint2d(p, result); }
264
- npcToVbArray(pts) {
265
- pts.forEach((pt) => this.npcToVb(pt, pt));
266
- return pts;
267
- }
268
- draw() {
269
- const el = this.el;
270
- const bb = el.bbox();
271
- const ctm = el.screenCTM().lmultiplyO(MarkupApp.screenToVbMtx());
272
- this.vbToBoxTrn = ctm.inverse().toIModelTransform();
273
- this.npcToVbTrn = new Matrix().scaleO(bb.w, bb.h).translateO(bb.x, bb.y).lmultiplyO(ctm).toIModelTransform();
274
- this.handles.forEach((h) => h.setPosition());
275
- }
276
- remove() {
277
- if (this.dragging)
278
- this.cancelDrag();
279
- this.group.remove();
280
- }
281
- startDrag(ev) {
282
- if (this.active) {
283
- this.active.startDrag(ev);
284
- this.dragging = true;
285
- MarkupApp.markup.disablePick();
286
- IModelApp.toolAdmin.setCursor(IModelApp.viewManager.dynamicsCursor);
287
- }
288
- return EventHandled.Yes;
289
- }
290
- drag(ev) {
291
- if (this.dragging) {
292
- this.active.modify(ev);
293
- this.draw();
294
- }
295
- }
296
- /** complete the modification for the active handle. */
297
- endDrag(undo) {
298
- undo.performOperation(MarkupApp.getActionName("modify"), () => {
299
- const el = this.el;
300
- const original = el.originalEl; // save original element
301
- if (original === undefined) {
302
- this.ss.emptyAll();
303
- this.ss.add(el);
304
- undo.onAdded(el);
305
- }
306
- else {
307
- el.originalEl = undefined; // clear original element
308
- undo.onModified(el, original);
309
- }
310
- });
311
- this.draw();
312
- this.dragging = false;
313
- this.active = undefined;
314
- MarkupApp.markup.enablePick();
315
- return EventHandled.Yes;
316
- }
317
- /** called when the reset button is pressed. */
318
- cancelDrag() {
319
- if (!this.dragging)
320
- return;
321
- const el = this.el;
322
- const original = el.originalEl;
323
- if (original) {
324
- el.replace(original);
325
- this.el = original;
326
- }
327
- this.draw();
328
- this.active = undefined;
329
- MarkupApp.markup.enablePick();
330
- }
331
- }
332
- /** The set of currently selected SVG elements. When elements are added to the set, they are hilited.
333
- * @public
334
- */
335
- export class MarkupSelected {
336
- constructor(svg) {
337
- this.svg = svg;
338
- this.elements = new Set();
339
- /** Called whenever elements are added or removed from this SelectionSet */
340
- this.onChanged = new BeEvent();
341
- }
342
- get size() { return this.elements.size; }
343
- get isEmpty() { return this.size === 0; }
344
- has(el) { return this.elements.has(el); }
345
- emptyAll() {
346
- this.clearEditors();
347
- if (this.isEmpty)
348
- return; // Don't send onChanged if already empty.
349
- this.elements.forEach((el) => el.unHilite());
350
- this.elements.clear();
351
- this.onChanged.raiseEvent(this);
352
- }
353
- restart(el) {
354
- this.emptyAll();
355
- if (el)
356
- this.add(el);
357
- }
358
- clearEditors() {
359
- if (this.handles) {
360
- this.handles.remove();
361
- this.handles = undefined;
362
- }
363
- }
364
- sizeChanged() {
365
- this.clearEditors();
366
- if (this.elements.size === 1)
367
- this.handles = new Handles(this, this.elements.values().next().value);
368
- this.onChanged.raiseEvent(this);
369
- }
370
- /** Add a new element to the SS */
371
- add(el) {
372
- this.elements.add(el);
373
- el.hilite();
374
- this.sizeChanged();
375
- }
376
- /** Remove an element from the selection set and unhilite it.
377
- * @returns true if the element was in the SS and was removed.
378
- */
379
- drop(el) {
380
- el.unHilite();
381
- return this.elements.delete(el) ? (this.sizeChanged(), true) : false;
382
- }
383
- /** Replace an entry in the selection set with a different element. */
384
- replace(oldEl, newEl) {
385
- if (this.drop(oldEl))
386
- this.add(newEl);
387
- }
388
- deleteAll(undo) {
389
- undo.performOperation(MarkupApp.getActionName("delete"), () => this.elements.forEach((el) => {
390
- undo.onDelete(el);
391
- el.remove();
392
- }));
393
- this.emptyAll();
394
- }
395
- groupAll(undo) {
396
- if (this.size < 2)
397
- return;
398
- const first = this.elements.values().next().value;
399
- const parent = first.parent();
400
- const group = parent.group();
401
- const ordered = [];
402
- this.elements.forEach((el) => ordered.push(el));
403
- ordered.sort((lhs, rhs) => parent.index(lhs) - parent.index(rhs)); // Preserve relative z ordering
404
- undo.performOperation(MarkupApp.getActionName("group"), () => {
405
- ordered.forEach((el) => {
406
- const oldParent = el.parent();
407
- const oldPos = el.position();
408
- el.unHilite();
409
- undo.onRepositioned(el.addTo(group), oldPos, oldParent);
410
- }), undo.onAdded(group);
411
- });
412
- this.restart(group);
413
- }
414
- ungroupAll(undo) {
415
- const groups = new Set();
416
- this.elements.forEach((el) => {
417
- if (el instanceof G)
418
- groups.add(el);
419
- });
420
- if (0 === groups.size)
421
- return;
422
- undo.performOperation(MarkupApp.getActionName("ungroup"), () => {
423
- groups.forEach((g) => {
424
- g.unHilite();
425
- this.elements.delete(g);
426
- undo.onDelete(g);
427
- g.each((index, children) => {
428
- const child = children[index];
429
- const oldPos = child.position();
430
- child.toParent(g.parent());
431
- undo.onRepositioned(child, oldPos, g);
432
- }, false);
433
- g.untransform(); // Don't want undo of ungroup to push the current group transform...
434
- g.remove();
435
- });
436
- });
437
- this.sizeChanged();
438
- }
439
- /** Move all of the entries to a new position in the DOM via a callback. */
440
- reposition(cmdName, undo, fn) {
441
- undo.performOperation(cmdName, () => this.elements.forEach((el) => {
442
- const oldParent = el.parent();
443
- const oldPos = el.position();
444
- fn(el);
445
- undo.onRepositioned(el, oldPos, oldParent);
446
- }));
447
- this.sizeChanged();
448
- }
449
- }
450
- /** Provides UI for selection, delete, move, copy, bring-to-front, send-to-back, etc. for Markup SVG elements
451
- * @public
452
- */
453
- export class SelectTool extends MarkupTool {
454
- constructor() {
455
- super(...arguments);
456
- this._dragging = [];
457
- this._isBoxSelect = false;
458
- }
459
- get flashedElement() { return this._flashedElement; }
460
- set flashedElement(el) {
461
- if (el === this._flashedElement)
462
- return;
463
- if (undefined !== this._flashedElement)
464
- this._flashedElement.unFlash();
465
- if (undefined !== el)
466
- el.flash();
467
- this._flashedElement = el;
468
- }
469
- unflashSelected() {
470
- if (undefined !== this._flashedElement && this.markup.selected.has(this._flashedElement))
471
- this.flashedElement = undefined;
472
- }
473
- initSelect() {
474
- this.markup.setCursor("default");
475
- this.markup.enablePick();
476
- this.flashedElement = undefined;
477
- this.boxSelectInit();
478
- }
479
- clearSelect() {
480
- this.cancelDrag();
481
- this.markup.selected.emptyAll();
482
- }
483
- async onCleanup() { this.clearSelect(); }
484
- async onPostInstall() {
485
- this.initSelect();
486
- return super.onPostInstall();
487
- }
488
- async onRestartTool() { this.initSelect(); }
489
- showPrompt() {
490
- const mainInstruction = ToolAssistance.createInstruction(this.iconSpec, IModelApp.localization.getLocalizedString(`${MarkupTool.toolKey}Select.Prompts.IdentifyMarkup`));
491
- const mouseInstructions = [];
492
- const touchInstructions = [];
493
- const acceptMsg = IModelApp.localization.getLocalizedString(`${MarkupTool.toolKey}Select.Prompts.AcceptMarkup`);
494
- touchInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.OneTouchTap, acceptMsg, false, ToolAssistanceInputMethod.Touch));
495
- mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.LeftClick, acceptMsg, false, ToolAssistanceInputMethod.Mouse));
496
- touchInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.OneTouchDrag, CoreTools.translate("ElementSet.Inputs.BoxCorners"), false, ToolAssistanceInputMethod.Touch));
497
- mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.LeftClickDrag, CoreTools.translate("ElementSet.Inputs.BoxCorners"), false, ToolAssistanceInputMethod.Mouse));
498
- mouseInstructions.push(ToolAssistance.createModifierKeyInstruction(ToolAssistance.shiftKey, ToolAssistanceImage.LeftClickDrag, CoreTools.translate("ElementSet.Inputs.OverlapSelection"), false, ToolAssistanceInputMethod.Mouse));
499
- mouseInstructions.push(ToolAssistance.createModifierKeyInstruction(ToolAssistance.ctrlKey, ToolAssistanceImage.LeftClick, CoreTools.translate("ElementSet.Inputs.InvertSelection"), false, ToolAssistanceInputMethod.Mouse));
500
- mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.CursorClick, CoreTools.translate("ElementSet.Inputs.ClearSelection"), false, ToolAssistanceInputMethod.Mouse));
501
- const sections = [];
502
- sections.push(ToolAssistance.createSection(mouseInstructions, ToolAssistance.inputsLabel));
503
- sections.push(ToolAssistance.createSection(touchInstructions, ToolAssistance.inputsLabel));
504
- const instructions = ToolAssistance.createInstructions(mainInstruction, sections);
505
- IModelApp.notifications.setToolAssistance(instructions);
506
- }
507
- /** When we start a drag operation, we add a new set of elements to the DOM and start modifying them.
508
- * If we cancel the operation, we need remove them from the DOM.
509
- */
510
- cancelDrag() {
511
- this._dragging.forEach((el) => el.remove()); // remove temporary elements from DOM
512
- this._dragging.length = 0;
513
- this.boxSelectInit();
514
- }
515
- async onResetButtonUp(_ev) {
516
- const selected = this.markup.selected;
517
- const handles = selected.handles;
518
- if (handles && handles.dragging)
519
- handles.cancelDrag();
520
- this.cancelDrag();
521
- selected.sizeChanged();
522
- return EventHandled.Yes;
523
- }
524
- /** Called when there is a mouse "click" (down+up without any motion) */
525
- async onDataButtonUp(ev) {
526
- const markup = this.markup;
527
- const selected = markup.selected;
528
- const handles = selected.handles;
529
- if (handles) {
530
- if (handles.dragging)
531
- return handles.endDrag(markup.undo);
532
- if (handles.active) { // clicked on a handle
533
- if (ev.isControlKey)
534
- selected.drop(handles.el);
535
- else
536
- await handles.active.onClick(ev);
537
- handles.active = undefined;
538
- return EventHandled.Yes;
539
- }
540
- }
541
- const el = this.flashedElement = this.pickElement(ev.viewPoint);
542
- if (ev.isControlKey) {
543
- if (el && selected.drop(el))
544
- return EventHandled.Yes;
545
- }
546
- else {
547
- selected.emptyAll();
548
- }
549
- if (el !== undefined)
550
- selected.add(el);
551
- return EventHandled.Yes;
552
- }
553
- async onTouchTap(ev) {
554
- // Allow tap with a second touch point to multiselect (similar functionality to control being held with mouse click).
555
- if (ev.isSingleTap && 2 === ev.touchEvent.touches.length) {
556
- const el = this.flashedElement = this.pickElement(ev.viewPoint);
557
- if (el) {
558
- const selected = this.markup.selected;
559
- if (!selected.drop(el))
560
- selected.add(el);
561
- return EventHandled.Yes;
562
- }
563
- }
564
- return super.onTouchTap(ev);
565
- }
566
- boxSelectInit() {
567
- this._isBoxSelect = false;
568
- this.markup.svgDynamics.clear();
569
- }
570
- boxSelectStart(ev) {
571
- if (!ev.isControlKey)
572
- this.markup.selected.emptyAll();
573
- this._anchorPt = MarkupApp.convertVpToVb(ev.viewPoint);
574
- this._isBoxSelect = true;
575
- return true;
576
- }
577
- boxSelect(ev, isDynamics) {
578
- if (!this._isBoxSelect)
579
- return false;
580
- const start = this._anchorPt;
581
- const end = MarkupApp.convertVpToVb(ev.viewPoint);
582
- const vec = start.vectorTo(end);
583
- const width = Math.abs(vec.x);
584
- const height = Math.abs(vec.y);
585
- if (width < 1 || height < 1)
586
- return true;
587
- const rightToLeft = (start.x > end.x);
588
- const overlapMode = (ev.isShiftKey ? !rightToLeft : rightToLeft); // Shift inverts inside/overlap selection...
589
- const offset = Point3d.create(vec.x < 0 ? end.x : start.x, vec.y < 0 ? end.y : start.y); // define location by corner points...
590
- this.markup.svgDynamics.clear();
591
- 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 });
592
- 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" });
593
- const outlinesG = isDynamics ? this.markup.svgDynamics.group() : undefined;
594
- const selectRect = selectBox.node.getBoundingClientRect();
595
- this.markup.svgMarkup.forElementsOfGroup((child) => {
596
- const childRect = child.node.getBoundingClientRect();
597
- const inside = (childRect.left >= selectRect.left && childRect.top >= selectRect.top && childRect.right <= selectRect.right && childRect.bottom <= selectRect.bottom);
598
- const overlap = !inside && (childRect.left < selectRect.right && childRect.right > selectRect.left && childRect.bottom > selectRect.top && childRect.top < selectRect.bottom);
599
- const accept = inside || (overlap && overlapMode);
600
- if (undefined !== outlinesG) {
601
- if (inside || overlap) {
602
- const outline = child.getOutline().attr(MarkupApp.props.handles.moveOutline).addTo(outlinesG);
603
- if (accept)
604
- outline.attr({ "fill": MarkupApp.props.hilite.flash, "fill-opacity": 0.2 });
605
- }
606
- }
607
- else if (accept) {
608
- this.markup.selected.add(child);
609
- }
610
- });
611
- if (!isDynamics)
612
- this.boxSelectInit();
613
- return true;
614
- }
615
- /** called when the mouse moves while the data button is down. */
616
- async onMouseStartDrag(ev) {
617
- if (BeButton.Data !== ev.button)
618
- return EventHandled.No;
619
- const markup = this.markup;
620
- const selected = markup.selected;
621
- const handles = selected.handles;
622
- if (handles && handles.active) {
623
- this.flashedElement = undefined; // make sure there are no elements flashed while dragging
624
- return handles.startDrag(ev);
625
- }
626
- const flashed = this.flashedElement = this.pickElement(ev.viewPoint);
627
- if (undefined === flashed)
628
- return this.boxSelectStart(ev) ? EventHandled.Yes : EventHandled.No;
629
- if (!selected.has(flashed))
630
- selected.restart(flashed); // we clicked on an element not in the selection set, replace current selection with just this element
631
- selected.clearEditors();
632
- this._anchorPt = MarkupApp.convertVpToVb(ev.viewPoint); // save the starting point. This is the point where the "down" occurred.
633
- this.cancelDrag();
634
- selected.elements.forEach((el) => {
635
- const cloned = el.cloneMarkup(); // make a clone of this element
636
- el.after(cloned); // put it into the DOM after its original
637
- cloned.originalEl = el; // save original element so we can remove it if this is a "move" command
638
- this._dragging.push(cloned); // add to dragging set
639
- });
640
- return EventHandled.Yes;
641
- }
642
- /** Called whenever the mouse moves while this tool is active. */
643
- async onMouseMotion(ev) {
644
- const markup = this.markup;
645
- const handles = markup.selected.handles;
646
- if (handles && handles.dragging) {
647
- this.receivedDownEvent = true; // necessary to tell ToolAdmin to send us the button up event
648
- return handles.drag(ev); // drag the handle
649
- }
650
- if (this._dragging.length === 0) {
651
- if (this.boxSelect(ev, true))
652
- return;
653
- if (InputSource.Touch !== ev.inputSource)
654
- this.flashedElement = this.pickElement(ev.viewPoint); // if we're not dragging, try to find an element under the cursor
655
- return;
656
- }
657
- // we have a set of elements being dragged. NOTE: coordinates are viewbox
658
- const vbPt = MarkupApp.convertVpToVb(ev.viewPoint);
659
- const delta = vbPt.minus(this._anchorPt);
660
- this._dragging.forEach((el) => el.translate(delta.x, delta.y));
661
- this._anchorPt = vbPt; // translate moves from last mouse location
662
- }
663
- /** Called when the mouse goes up after dragging. */
664
- async onMouseEndDrag(ev) {
665
- const markup = this.markup;
666
- const selected = markup.selected;
667
- const handles = selected.handles;
668
- if (handles && handles.dragging) // if we have handles up, and if they're in the "dragging" state, send the event to them
669
- return handles.endDrag(markup.undo);
670
- if (this._dragging.length === 0)
671
- return this.boxSelect(ev, false) ? EventHandled.Yes : EventHandled.No;
672
- // NOTE: all units should be in viewbox coordinates
673
- const delta = MarkupApp.convertVpToVb(ev.viewPoint).minus(this._anchorPt);
674
- const undo = markup.undo;
675
- if (ev.isShiftKey) // shift key means "add to existing," otherwise new selection replaces old
676
- selected.emptyAll();
677
- // move or copy all of the elements in dragged set
678
- undo.performOperation(MarkupApp.getActionName("copy"), () => this._dragging.forEach((el) => {
679
- el.translate(delta.x, delta.y); // move to final location
680
- const original = el.originalEl; // save original element
681
- el.originalEl = undefined; // clear original element
682
- if (ev.isShiftKey) {
683
- selected.add(el);
684
- undo.onAdded(el); // shift key means copy element
685
- }
686
- else {
687
- original.replace(el);
688
- undo.onModified(el, original);
689
- }
690
- }));
691
- this._dragging.length = 0; // empty dragging set
692
- selected.sizeChanged(); // notify that size of selection set changed
693
- return EventHandled.Yes;
694
- }
695
- /** called when a modifier key is pressed or released. Updates stretch handles, if present */
696
- async onModifierKeyTransition(_wentDown, modifier, _event) {
697
- if (modifier !== BeModifierKeys.Shift) // we only care about the shift key
698
- return EventHandled.No;
699
- const selected = this.markup.selected;
700
- const handles = selected.handles;
701
- if (undefined === handles || !handles.dragging) // and only if we're currently dragging
702
- return EventHandled.No;
703
- const ev = new BeButtonEvent(); // we need to simulate a mouse motion by sending a drag event at the last cursor position
704
- IModelApp.toolAdmin.fillEventFromCursorLocation(ev);
705
- return (undefined === ev.viewport) ? EventHandled.No : (handles.drag(ev), EventHandled.Yes);
706
- }
707
- /** called whenever a key is pressed while this tool is active. */
708
- async onKeyTransition(wentDown, key) {
709
- if (!wentDown)
710
- return EventHandled.No;
711
- const markup = this.markup;
712
- switch (key.key.toLowerCase()) {
713
- case "delete": // delete key or backspace = delete current selection set
714
- case "backspace":
715
- this.unflashSelected();
716
- markup.deleteSelected();
717
- return EventHandled.Yes;
718
- case "escape": // esc = cancel current operation
719
- await this.exitTool();
720
- return EventHandled.Yes;
721
- case "b": // alt-shift-b = send to back
722
- return (key.altKey && key.shiftKey) ? (markup.sendToBack(), EventHandled.Yes) : EventHandled.No;
723
- case "f": // alt-shift-f = bring to front
724
- return (key.altKey && key.shiftKey) ? (markup.bringToFront(), EventHandled.Yes) : EventHandled.No;
725
- case "g": // ctrl-g = create group
726
- return (key.ctrlKey) ? (this.unflashSelected(), markup.groupSelected(), EventHandled.Yes) : EventHandled.No;
727
- case "u": // ctrl-u = ungroup
728
- return (key.ctrlKey) ? (this.unflashSelected(), markup.ungroupSelected(), EventHandled.Yes) : EventHandled.No;
729
- }
730
- return EventHandled.No;
731
- }
732
- }
733
- SelectTool.toolId = "Markup.Select";
734
- SelectTool.iconSpec = "icon-cursor";
1
+ /*---------------------------------------------------------------------------------------------
2
+ * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
3
+ * See LICENSE.md in the project root for license terms and full copyright notice.
4
+ *--------------------------------------------------------------------------------------------*/
5
+ /** @packageDocumentation
6
+ * @module MarkupTools
7
+ */
8
+ import { BeEvent } from "@itwin/core-bentley";
9
+ import { Point2d, Point3d, Vector2d } from "@itwin/core-geometry";
10
+ import { BeButton, BeButtonEvent, BeModifierKeys, CoreTools, EventHandled, IModelApp, InputSource, ToolAssistance, ToolAssistanceImage, ToolAssistanceInputMethod, } from "@itwin/core-frontend";
11
+ import { G, Line, Text as MarkupText, Matrix, Point } from "@svgdotjs/svg.js";
12
+ import { MarkupApp } from "./Markup";
13
+ import { MarkupTool } from "./MarkupTool";
14
+ import { EditTextTool } from "./TextEdit";
15
+ // cspell:ignore lmultiply untransform unFlash multiselect
16
+ /** Classes added to HTMLElements so they can be customized in CSS by applications.
17
+ * A "modify handle" is a visible position on the screen that provides UI to modify a MarkupElement.
18
+ * @public
19
+ */
20
+ export class ModifyHandle {
21
+ constructor(handles) {
22
+ this.handles = handles;
23
+ }
24
+ async onClick(_ev) { }
25
+ /** the mouse just went down on this handle, begin modification. */
26
+ startDrag(_ev, makeCopy = false) {
27
+ this.vbToStartTrn = this.handles.vbToBoxTrn.clone(); // save the starting vp -> element box transform
28
+ this.startModify(makeCopy);
29
+ }
30
+ startModify(makeCopy) {
31
+ const handles = this.handles;
32
+ const el = handles.el;
33
+ const cloned = handles.el = el.cloneMarkup(); // make a clone of this element
34
+ if (makeCopy) {
35
+ el.after(cloned);
36
+ }
37
+ else {
38
+ cloned.originalEl = el; // save original for undo
39
+ el.replace(cloned); // put it into the DOM in place of the original
40
+ }
41
+ }
42
+ setMouseHandler(target) {
43
+ const node = target.node;
44
+ node.addEventListener("mousedown", (event) => {
45
+ const ev = event;
46
+ if (0 === ev.button && undefined === this.handles.active)
47
+ this.handles.active = this;
48
+ });
49
+ node.addEventListener("touchstart", () => {
50
+ if (undefined === this.handles.active)
51
+ this.handles.active = this;
52
+ });
53
+ }
54
+ addTouchPadding(visible, handles) {
55
+ if (InputSource.Touch !== IModelApp.toolAdmin.currentInputState.inputSource)
56
+ return visible;
57
+ const padding = visible.cloneMarkup().scale(3).attr("opacity", 0);
58
+ const g = handles.group.group();
59
+ padding.addTo(g);
60
+ visible.addTo(g);
61
+ return g;
62
+ }
63
+ }
64
+ /** A ModifyHandle that changes the size of the element
65
+ * @public
66
+ */
67
+ class StretchHandle extends ModifyHandle {
68
+ constructor(handles, xy, cursor) {
69
+ super(handles);
70
+ this.posNpc = new Point2d(xy[0], xy[1]);
71
+ const props = MarkupApp.props.handles;
72
+ this._circle = handles.group.circle(props.size).addClass(MarkupApp.stretchHandleClass).attr(props.stretch).attr("cursor", `${cursor}-resize`); // the visible "circle" for this handle
73
+ this._circle = this.addTouchPadding(this._circle, handles);
74
+ this.setMouseHandler(this._circle);
75
+ }
76
+ setPosition() {
77
+ const pt = this.handles.npcToVb(this.posNpc); // convert to viewbox coords
78
+ this._circle.center(pt.x, pt.y);
79
+ }
80
+ startDrag(_ev) {
81
+ const handles = this.handles;
82
+ this.startCtm = handles.el.screenCTM().lmultiplyO(MarkupApp.screenToVbMtx());
83
+ this.startBox = handles.el.bbox(); // save starting size so we can preserve aspect ratio
84
+ this.startPos = handles.npcToBox(this.posNpc);
85
+ this.opposite = handles.npcToBox({ x: 1 - this.posNpc.x, y: 1 - this.posNpc.y });
86
+ super.startDrag(_ev);
87
+ }
88
+ /** perform the stretch. Always stretch element with anchor at the opposite corner of the one being moved. */
89
+ modify(ev) {
90
+ const evPt = MarkupApp.convertVpToVb(ev.viewPoint); // get cursor location in viewbox coords
91
+ const diff = this.startPos.vectorTo(this.vbToStartTrn.multiplyPoint2d(evPt)); // movement of cursor from start, in viewbox coords
92
+ const diag = this.startPos.vectorTo(this.opposite).normalize(); // vector from opposite corner to this handle
93
+ let diagVec = diag.scaleToLength(diff.dotProduct(diag)); // projected distance along diagonal
94
+ if (diagVec === undefined)
95
+ diagVec = Vector2d.createZero();
96
+ // if the shift key is down, don't preserve aspect ratio
97
+ const adjusted = ev.isShiftKey ? { x: diff.x, y: diff.y } : { x: diagVec.x, y: diagVec.y };
98
+ let { x, y, h, w } = this.startBox;
99
+ if (this.posNpc.x === 0) {
100
+ x += adjusted.x; // left edge
101
+ w -= adjusted.x;
102
+ }
103
+ else if (this.posNpc.x === 1) {
104
+ w += adjusted.x; // right edge
105
+ }
106
+ if (this.posNpc.y === 0) {
107
+ y += adjusted.y; // top edge
108
+ h -= adjusted.y;
109
+ }
110
+ else if (this.posNpc.y === 1) {
111
+ h += adjusted.y; // bottom edge
112
+ }
113
+ const mtx = this.startCtm.inverse().scaleO(this.startBox.w / w, this.startBox.h / h, this.opposite.x, this.opposite.y).inverseO();
114
+ const minSize = 10;
115
+ if (w > minSize && h > minSize) // don't let element get too small
116
+ this.handles.el.markupStretch(w, h, x, y, mtx);
117
+ }
118
+ }
119
+ /** A ModifyHandle to rotate an element
120
+ * @public
121
+ */
122
+ class RotateHandle extends ModifyHandle {
123
+ constructor(handles) {
124
+ super(handles);
125
+ this.handles = handles;
126
+ const props = MarkupApp.props.handles;
127
+ this._line = handles.group.line(0, 0, 1, 1).attr(props.rotateLine).addClass(MarkupApp.rotateLineClass);
128
+ this._circle = handles.group.circle(props.size * 1.25).attr(props.rotate).addClass(MarkupApp.rotateHandleClass);
129
+ this._circle = this.addTouchPadding(this._circle, handles);
130
+ this.setMouseHandler(this._circle);
131
+ }
132
+ get centerVb() { return this.handles.npcToVb({ x: .5, y: .5 }); }
133
+ get anchorVb() { return this.handles.npcToVb({ x: .5, y: 0 }); }
134
+ setPosition() {
135
+ const anchor = this.anchorVb;
136
+ const dir = this.centerVb.vectorTo(anchor).normalize();
137
+ const loc = this.location = anchor.plusScaled(dir, MarkupApp.props.handles.size * 3);
138
+ this._line.plot(anchor.x, anchor.y, loc.x, loc.y);
139
+ this._circle.center(loc.x, loc.y);
140
+ }
141
+ modify(ev) {
142
+ const centerVp = this.centerVb;
143
+ const currDir = centerVp.vectorTo(MarkupApp.convertVpToVb(ev.viewPoint));
144
+ const dir = centerVp.vectorTo(this.location);
145
+ this.handles.el.rotate(dir.angleTo(currDir).degrees);
146
+ }
147
+ }
148
+ /** A VertexHandle to move a point on a line
149
+ * @public
150
+ */
151
+ class VertexHandle extends ModifyHandle {
152
+ constructor(handles, index) {
153
+ super(handles);
154
+ this.handles = handles;
155
+ const props = MarkupApp.props.handles;
156
+ this._circle = handles.group.circle(props.size).attr(props.vertex).addClass(MarkupApp.vertexHandleClass);
157
+ this._x = `x${index + 1}`;
158
+ this._y = `y${index + 1}`;
159
+ this._circle = this.addTouchPadding(this._circle, handles);
160
+ this.setMouseHandler(this._circle);
161
+ }
162
+ setPosition() {
163
+ let point = new Point(this.handles.el.attr(this._x), this.handles.el.attr(this._y));
164
+ const matrix = this.handles.el.screenCTM().lmultiplyO(MarkupApp.screenToVbMtx());
165
+ point = point.transform(matrix);
166
+ this._circle.center(point.x, point.y);
167
+ }
168
+ modify(ev) {
169
+ let point = new Point(ev.viewPoint.x, ev.viewPoint.y);
170
+ const matrix = this.handles.el.screenCTM().inverseO().multiplyO(MarkupApp.getVpToScreenMtx());
171
+ point = point.transform(matrix);
172
+ const el = this.handles.el;
173
+ el.attr(this._x, point.x);
174
+ el.attr(this._y, point.y);
175
+ }
176
+ }
177
+ /** A handle that moves (translates) an element.
178
+ * @public
179
+ */
180
+ class MoveHandle extends ModifyHandle {
181
+ constructor(handles, showBBox) {
182
+ super(handles);
183
+ this.handles = handles;
184
+ const props = MarkupApp.props.handles;
185
+ const clone = this.handles.el.cloneMarkup();
186
+ clone.css(props.move);
187
+ clone.forElementsOfGroup((child) => child.css(props.move));
188
+ if (showBBox) {
189
+ this._outline = handles.group.polygon().attr(props.moveOutline);
190
+ const rect = this.handles.el.getOutline().attr(props.move).attr({ fill: "none" });
191
+ const group = handles.group.group();
192
+ group.add(this._outline);
193
+ group.add(rect);
194
+ group.add(clone);
195
+ this._shape = group;
196
+ }
197
+ else {
198
+ clone.addTo(handles.group);
199
+ this._shape = clone;
200
+ }
201
+ this._shape.addClass(MarkupApp.moveHandleClass);
202
+ this.setMouseHandler(this._shape);
203
+ }
204
+ async onClick(_ev) {
205
+ const el = this.handles.el;
206
+ // eslint-disable-next-line deprecation/deprecation
207
+ if (el instanceof MarkupText || (el instanceof G && el.node.className.baseVal === MarkupApp.boxedTextClass)) // if they click on the move handle of a text element, start the text editor
208
+ await new EditTextTool(el).run();
209
+ }
210
+ /** draw the outline of the element's bbox (in viewbox coordinates) */
211
+ setPosition() {
212
+ if (undefined !== this._outline) {
213
+ const pts = [new Point2d(0, 0), new Point2d(0, 1), new Point2d(1, 1), new Point2d(1, 0)];
214
+ this._outline.plot(this.handles.npcToVbArray(pts).map((pt) => [pt.x, pt.y]));
215
+ }
216
+ }
217
+ startDrag(ev) {
218
+ super.startDrag(ev, ev.isShiftKey);
219
+ this._lastPos = MarkupApp.convertVpToVb(ev.viewPoint); // save stating position in viewbox coordinates
220
+ }
221
+ modify(ev) {
222
+ const evPt = MarkupApp.convertVpToVb(ev.viewPoint);
223
+ const dist = evPt.minus(this._lastPos);
224
+ this._lastPos = evPt;
225
+ this.handles.el.translate(dist.x, dist.y); // move the element
226
+ }
227
+ }
228
+ /** The set of ModifyHandles active. Only applies if there is a single element selected.
229
+ * @public
230
+ */
231
+ export class Handles {
232
+ constructor(ss, el) {
233
+ this.ss = ss;
234
+ this.el = el;
235
+ this.handles = [];
236
+ this.dragging = false;
237
+ this.group = ss.svg.group();
238
+ if (el instanceof Line) {
239
+ this.handles.push(new MoveHandle(this, false));
240
+ this.handles.push(new VertexHandle(this, 0));
241
+ this.handles.push(new VertexHandle(this, 1));
242
+ this.draw(); // show starting state
243
+ return;
244
+ }
245
+ // move box is in the back
246
+ this.handles.push(new MoveHandle(this, true));
247
+ // then rotate handle
248
+ this.handles.push(new RotateHandle(this));
249
+ // then add all the stretch handles
250
+ const pts = [[0, 0], [0, .5], [0, 1], [.5, 1], [1, 1], [1, .5], [1, 0], [.5, 0]];
251
+ const cursors = ["nw", "w", "sw", "s", "se", "e", "ne", "n"];
252
+ const order = [7, 3, 1, 5, 2, 6, 0, 4];
253
+ const angle = el.screenCTM().decompose().rotate || 0;
254
+ const start = Math.round(-angle / 45); // so that we rotate the cursors for rotated elements
255
+ order.forEach((index) => this.handles.push(new StretchHandle(this, pts[index], cursors[(index + start + 8) % 8])));
256
+ this.draw(); // show starting state
257
+ }
258
+ npcToBox(p) {
259
+ const pt = this.npcToVb(p);
260
+ return this.vbToBox(pt, pt);
261
+ }
262
+ npcToVb(p, result) { return this.npcToVbTrn.multiplyPoint2d(p, result); }
263
+ vbToBox(p, result) { return this.vbToBoxTrn.multiplyPoint2d(p, result); }
264
+ npcToVbArray(pts) {
265
+ pts.forEach((pt) => this.npcToVb(pt, pt));
266
+ return pts;
267
+ }
268
+ draw() {
269
+ const el = this.el;
270
+ const bb = el.bbox();
271
+ const ctm = el.screenCTM().lmultiplyO(MarkupApp.screenToVbMtx());
272
+ this.vbToBoxTrn = ctm.inverse().toIModelTransform();
273
+ this.npcToVbTrn = new Matrix().scaleO(bb.w, bb.h).translateO(bb.x, bb.y).lmultiplyO(ctm).toIModelTransform();
274
+ this.handles.forEach((h) => h.setPosition());
275
+ }
276
+ remove() {
277
+ if (this.dragging)
278
+ this.cancelDrag();
279
+ this.group.remove();
280
+ }
281
+ startDrag(ev) {
282
+ if (this.active) {
283
+ this.active.startDrag(ev);
284
+ this.dragging = true;
285
+ MarkupApp.markup.disablePick();
286
+ IModelApp.toolAdmin.setCursor(IModelApp.viewManager.dynamicsCursor);
287
+ }
288
+ return EventHandled.Yes;
289
+ }
290
+ drag(ev) {
291
+ if (this.dragging) {
292
+ this.active.modify(ev);
293
+ this.draw();
294
+ }
295
+ }
296
+ /** complete the modification for the active handle. */
297
+ endDrag(undo) {
298
+ undo.performOperation(MarkupApp.getActionName("modify"), () => {
299
+ const el = this.el;
300
+ const original = el.originalEl; // save original element
301
+ if (original === undefined) {
302
+ this.ss.emptyAll();
303
+ this.ss.add(el);
304
+ undo.onAdded(el);
305
+ }
306
+ else {
307
+ el.originalEl = undefined; // clear original element
308
+ undo.onModified(el, original);
309
+ }
310
+ });
311
+ this.draw();
312
+ this.dragging = false;
313
+ this.active = undefined;
314
+ MarkupApp.markup.enablePick();
315
+ return EventHandled.Yes;
316
+ }
317
+ /** called when the reset button is pressed. */
318
+ cancelDrag() {
319
+ if (!this.dragging)
320
+ return;
321
+ const el = this.el;
322
+ const original = el.originalEl;
323
+ if (original) {
324
+ el.replace(original);
325
+ this.el = original;
326
+ }
327
+ this.draw();
328
+ this.active = undefined;
329
+ MarkupApp.markup.enablePick();
330
+ }
331
+ }
332
+ /** The set of currently selected SVG elements. When elements are added to the set, they are hilited.
333
+ * @public
334
+ */
335
+ export class MarkupSelected {
336
+ get size() { return this.elements.size; }
337
+ get isEmpty() { return this.size === 0; }
338
+ has(el) { return this.elements.has(el); }
339
+ emptyAll() {
340
+ this.clearEditors();
341
+ if (this.isEmpty)
342
+ return; // Don't send onChanged if already empty.
343
+ this.elements.forEach((el) => el.unHilite());
344
+ this.elements.clear();
345
+ this.onChanged.raiseEvent(this);
346
+ }
347
+ restart(el) {
348
+ this.emptyAll();
349
+ if (el)
350
+ this.add(el);
351
+ }
352
+ constructor(svg) {
353
+ this.svg = svg;
354
+ this.elements = new Set();
355
+ /** Called whenever elements are added or removed from this SelectionSet */
356
+ this.onChanged = new BeEvent();
357
+ }
358
+ clearEditors() {
359
+ if (this.handles) {
360
+ this.handles.remove();
361
+ this.handles = undefined;
362
+ }
363
+ }
364
+ sizeChanged() {
365
+ this.clearEditors();
366
+ if (this.elements.size === 1)
367
+ this.handles = new Handles(this, this.elements.values().next().value);
368
+ this.onChanged.raiseEvent(this);
369
+ }
370
+ /** Add a new element to the SS */
371
+ add(el) {
372
+ this.elements.add(el);
373
+ el.hilite();
374
+ this.sizeChanged();
375
+ }
376
+ /** Remove an element from the selection set and unhilite it.
377
+ * @returns true if the element was in the SS and was removed.
378
+ */
379
+ drop(el) {
380
+ el.unHilite();
381
+ return this.elements.delete(el) ? (this.sizeChanged(), true) : false;
382
+ }
383
+ /** Replace an entry in the selection set with a different element. */
384
+ replace(oldEl, newEl) {
385
+ if (this.drop(oldEl))
386
+ this.add(newEl);
387
+ }
388
+ deleteAll(undo) {
389
+ undo.performOperation(MarkupApp.getActionName("delete"), () => this.elements.forEach((el) => {
390
+ undo.onDelete(el);
391
+ el.remove();
392
+ }));
393
+ this.emptyAll();
394
+ }
395
+ groupAll(undo) {
396
+ if (this.size < 2)
397
+ return;
398
+ const first = this.elements.values().next().value;
399
+ const parent = first.parent();
400
+ const group = parent.group();
401
+ const ordered = [];
402
+ this.elements.forEach((el) => ordered.push(el));
403
+ ordered.sort((lhs, rhs) => parent.index(lhs) - parent.index(rhs)); // Preserve relative z ordering
404
+ undo.performOperation(MarkupApp.getActionName("group"), () => {
405
+ ordered.forEach((el) => {
406
+ const oldParent = el.parent();
407
+ const oldPos = el.position();
408
+ el.unHilite();
409
+ undo.onRepositioned(el.addTo(group), oldPos, oldParent);
410
+ }), undo.onAdded(group);
411
+ });
412
+ this.restart(group);
413
+ }
414
+ ungroupAll(undo) {
415
+ const groups = new Set();
416
+ this.elements.forEach((el) => {
417
+ if (el instanceof G)
418
+ groups.add(el);
419
+ });
420
+ if (0 === groups.size)
421
+ return;
422
+ undo.performOperation(MarkupApp.getActionName("ungroup"), () => {
423
+ groups.forEach((g) => {
424
+ g.unHilite();
425
+ this.elements.delete(g);
426
+ undo.onDelete(g);
427
+ g.each((index, children) => {
428
+ const child = children[index];
429
+ const oldPos = child.position();
430
+ child.toParent(g.parent());
431
+ undo.onRepositioned(child, oldPos, g);
432
+ }, false);
433
+ g.untransform(); // Don't want undo of ungroup to push the current group transform...
434
+ g.remove();
435
+ });
436
+ });
437
+ this.sizeChanged();
438
+ }
439
+ /** Move all of the entries to a new position in the DOM via a callback. */
440
+ reposition(cmdName, undo, fn) {
441
+ undo.performOperation(cmdName, () => this.elements.forEach((el) => {
442
+ const oldParent = el.parent();
443
+ const oldPos = el.position();
444
+ fn(el);
445
+ undo.onRepositioned(el, oldPos, oldParent);
446
+ }));
447
+ this.sizeChanged();
448
+ }
449
+ }
450
+ /** Provides UI for selection, delete, move, copy, bring-to-front, send-to-back, etc. for Markup SVG elements
451
+ * @public
452
+ */
453
+ class SelectTool extends MarkupTool {
454
+ constructor() {
455
+ super(...arguments);
456
+ this._dragging = [];
457
+ this._isBoxSelect = false;
458
+ }
459
+ get flashedElement() { return this._flashedElement; }
460
+ set flashedElement(el) {
461
+ if (el === this._flashedElement)
462
+ return;
463
+ if (undefined !== this._flashedElement)
464
+ this._flashedElement.unFlash();
465
+ if (undefined !== el)
466
+ el.flash();
467
+ this._flashedElement = el;
468
+ }
469
+ unflashSelected() {
470
+ if (undefined !== this._flashedElement && this.markup.selected.has(this._flashedElement))
471
+ this.flashedElement = undefined;
472
+ }
473
+ initSelect() {
474
+ this.markup.setCursor("default");
475
+ this.markup.enablePick();
476
+ this.flashedElement = undefined;
477
+ this.boxSelectInit();
478
+ }
479
+ clearSelect() {
480
+ this.cancelDrag();
481
+ this.markup.selected.emptyAll();
482
+ }
483
+ async onCleanup() { this.clearSelect(); }
484
+ async onPostInstall() {
485
+ this.initSelect();
486
+ return super.onPostInstall();
487
+ }
488
+ async onRestartTool() { this.initSelect(); }
489
+ showPrompt() {
490
+ const mainInstruction = ToolAssistance.createInstruction(this.iconSpec, IModelApp.localization.getLocalizedString(`${MarkupTool.toolKey}Select.Prompts.IdentifyMarkup`));
491
+ const mouseInstructions = [];
492
+ const touchInstructions = [];
493
+ const acceptMsg = IModelApp.localization.getLocalizedString(`${MarkupTool.toolKey}Select.Prompts.AcceptMarkup`);
494
+ touchInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.OneTouchTap, acceptMsg, false, ToolAssistanceInputMethod.Touch));
495
+ mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.LeftClick, acceptMsg, false, ToolAssistanceInputMethod.Mouse));
496
+ touchInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.OneTouchDrag, CoreTools.translate("ElementSet.Inputs.BoxCorners"), false, ToolAssistanceInputMethod.Touch));
497
+ mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.LeftClickDrag, CoreTools.translate("ElementSet.Inputs.BoxCorners"), false, ToolAssistanceInputMethod.Mouse));
498
+ mouseInstructions.push(ToolAssistance.createModifierKeyInstruction(ToolAssistance.shiftKey, ToolAssistanceImage.LeftClickDrag, CoreTools.translate("ElementSet.Inputs.OverlapSelection"), false, ToolAssistanceInputMethod.Mouse));
499
+ mouseInstructions.push(ToolAssistance.createModifierKeyInstruction(ToolAssistance.ctrlKey, ToolAssistanceImage.LeftClick, CoreTools.translate("ElementSet.Inputs.InvertSelection"), false, ToolAssistanceInputMethod.Mouse));
500
+ mouseInstructions.push(ToolAssistance.createInstruction(ToolAssistanceImage.CursorClick, CoreTools.translate("ElementSet.Inputs.ClearSelection"), false, ToolAssistanceInputMethod.Mouse));
501
+ const sections = [];
502
+ sections.push(ToolAssistance.createSection(mouseInstructions, ToolAssistance.inputsLabel));
503
+ sections.push(ToolAssistance.createSection(touchInstructions, ToolAssistance.inputsLabel));
504
+ const instructions = ToolAssistance.createInstructions(mainInstruction, sections);
505
+ IModelApp.notifications.setToolAssistance(instructions);
506
+ }
507
+ /** When we start a drag operation, we add a new set of elements to the DOM and start modifying them.
508
+ * If we cancel the operation, we need remove them from the DOM.
509
+ */
510
+ cancelDrag() {
511
+ this._dragging.forEach((el) => el.remove()); // remove temporary elements from DOM
512
+ this._dragging.length = 0;
513
+ this.boxSelectInit();
514
+ }
515
+ async onResetButtonUp(_ev) {
516
+ const selected = this.markup.selected;
517
+ const handles = selected.handles;
518
+ if (handles && handles.dragging)
519
+ handles.cancelDrag();
520
+ this.cancelDrag();
521
+ selected.sizeChanged();
522
+ return EventHandled.Yes;
523
+ }
524
+ /** Called when there is a mouse "click" (down+up without any motion) */
525
+ async onDataButtonUp(ev) {
526
+ const markup = this.markup;
527
+ const selected = markup.selected;
528
+ const handles = selected.handles;
529
+ if (handles) {
530
+ if (handles.dragging)
531
+ return handles.endDrag(markup.undo);
532
+ if (handles.active) { // clicked on a handle
533
+ if (ev.isControlKey)
534
+ selected.drop(handles.el);
535
+ else
536
+ await handles.active.onClick(ev);
537
+ handles.active = undefined;
538
+ return EventHandled.Yes;
539
+ }
540
+ }
541
+ const el = this.flashedElement = this.pickElement(ev.viewPoint);
542
+ if (ev.isControlKey) {
543
+ if (el && selected.drop(el))
544
+ return EventHandled.Yes;
545
+ }
546
+ else {
547
+ selected.emptyAll();
548
+ }
549
+ if (el !== undefined)
550
+ selected.add(el);
551
+ return EventHandled.Yes;
552
+ }
553
+ async onTouchTap(ev) {
554
+ // Allow tap with a second touch point to multiselect (similar functionality to control being held with mouse click).
555
+ if (ev.isSingleTap && 2 === ev.touchEvent.touches.length) {
556
+ const el = this.flashedElement = this.pickElement(ev.viewPoint);
557
+ if (el) {
558
+ const selected = this.markup.selected;
559
+ if (!selected.drop(el))
560
+ selected.add(el);
561
+ return EventHandled.Yes;
562
+ }
563
+ }
564
+ return super.onTouchTap(ev);
565
+ }
566
+ boxSelectInit() {
567
+ this._isBoxSelect = false;
568
+ this.markup.svgDynamics.clear();
569
+ }
570
+ boxSelectStart(ev) {
571
+ if (!ev.isControlKey)
572
+ this.markup.selected.emptyAll();
573
+ this._anchorPt = MarkupApp.convertVpToVb(ev.viewPoint);
574
+ this._isBoxSelect = true;
575
+ return true;
576
+ }
577
+ boxSelect(ev, isDynamics) {
578
+ if (!this._isBoxSelect)
579
+ return false;
580
+ const start = this._anchorPt;
581
+ const end = MarkupApp.convertVpToVb(ev.viewPoint);
582
+ const vec = start.vectorTo(end);
583
+ const width = Math.abs(vec.x);
584
+ const height = Math.abs(vec.y);
585
+ if (width < 1 || height < 1)
586
+ return true;
587
+ const rightToLeft = (start.x > end.x);
588
+ const overlapMode = (ev.isShiftKey ? !rightToLeft : rightToLeft); // Shift inverts inside/overlap selection...
589
+ const offset = Point3d.create(vec.x < 0 ? end.x : start.x, vec.y < 0 ? end.y : start.y); // define location by corner points...
590
+ this.markup.svgDynamics.clear();
591
+ 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 });
592
+ 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" });
593
+ const outlinesG = isDynamics ? this.markup.svgDynamics.group() : undefined;
594
+ const selectRect = selectBox.node.getBoundingClientRect();
595
+ this.markup.svgMarkup.forElementsOfGroup((child) => {
596
+ const childRect = child.node.getBoundingClientRect();
597
+ const inside = (childRect.left >= selectRect.left && childRect.top >= selectRect.top && childRect.right <= selectRect.right && childRect.bottom <= selectRect.bottom);
598
+ const overlap = !inside && (childRect.left < selectRect.right && childRect.right > selectRect.left && childRect.bottom > selectRect.top && childRect.top < selectRect.bottom);
599
+ const accept = inside || (overlap && overlapMode);
600
+ if (undefined !== outlinesG) {
601
+ if (inside || overlap) {
602
+ const outline = child.getOutline().attr(MarkupApp.props.handles.moveOutline).addTo(outlinesG);
603
+ if (accept)
604
+ outline.attr({ "fill": MarkupApp.props.hilite.flash, "fill-opacity": 0.2 });
605
+ }
606
+ }
607
+ else if (accept) {
608
+ this.markup.selected.add(child);
609
+ }
610
+ });
611
+ if (!isDynamics)
612
+ this.boxSelectInit();
613
+ return true;
614
+ }
615
+ /** called when the mouse moves while the data button is down. */
616
+ async onMouseStartDrag(ev) {
617
+ if (BeButton.Data !== ev.button)
618
+ return EventHandled.No;
619
+ const markup = this.markup;
620
+ const selected = markup.selected;
621
+ const handles = selected.handles;
622
+ if (handles && handles.active) {
623
+ this.flashedElement = undefined; // make sure there are no elements flashed while dragging
624
+ return handles.startDrag(ev);
625
+ }
626
+ const flashed = this.flashedElement = this.pickElement(ev.viewPoint);
627
+ if (undefined === flashed)
628
+ return this.boxSelectStart(ev) ? EventHandled.Yes : EventHandled.No;
629
+ if (!selected.has(flashed))
630
+ selected.restart(flashed); // we clicked on an element not in the selection set, replace current selection with just this element
631
+ selected.clearEditors();
632
+ this._anchorPt = MarkupApp.convertVpToVb(ev.viewPoint); // save the starting point. This is the point where the "down" occurred.
633
+ this.cancelDrag();
634
+ selected.elements.forEach((el) => {
635
+ const cloned = el.cloneMarkup(); // make a clone of this element
636
+ el.after(cloned); // put it into the DOM after its original
637
+ cloned.originalEl = el; // save original element so we can remove it if this is a "move" command
638
+ this._dragging.push(cloned); // add to dragging set
639
+ });
640
+ return EventHandled.Yes;
641
+ }
642
+ /** Called whenever the mouse moves while this tool is active. */
643
+ async onMouseMotion(ev) {
644
+ const markup = this.markup;
645
+ const handles = markup.selected.handles;
646
+ if (handles && handles.dragging) {
647
+ this.receivedDownEvent = true; // necessary to tell ToolAdmin to send us the button up event
648
+ return handles.drag(ev); // drag the handle
649
+ }
650
+ if (this._dragging.length === 0) {
651
+ if (this.boxSelect(ev, true))
652
+ return;
653
+ if (InputSource.Touch !== ev.inputSource)
654
+ this.flashedElement = this.pickElement(ev.viewPoint); // if we're not dragging, try to find an element under the cursor
655
+ return;
656
+ }
657
+ // we have a set of elements being dragged. NOTE: coordinates are viewbox
658
+ const vbPt = MarkupApp.convertVpToVb(ev.viewPoint);
659
+ const delta = vbPt.minus(this._anchorPt);
660
+ this._dragging.forEach((el) => el.translate(delta.x, delta.y));
661
+ this._anchorPt = vbPt; // translate moves from last mouse location
662
+ }
663
+ /** Called when the mouse goes up after dragging. */
664
+ async onMouseEndDrag(ev) {
665
+ const markup = this.markup;
666
+ const selected = markup.selected;
667
+ const handles = selected.handles;
668
+ if (handles && handles.dragging) // if we have handles up, and if they're in the "dragging" state, send the event to them
669
+ return handles.endDrag(markup.undo);
670
+ if (this._dragging.length === 0)
671
+ return this.boxSelect(ev, false) ? EventHandled.Yes : EventHandled.No;
672
+ // NOTE: all units should be in viewbox coordinates
673
+ const delta = MarkupApp.convertVpToVb(ev.viewPoint).minus(this._anchorPt);
674
+ const undo = markup.undo;
675
+ if (ev.isShiftKey) // shift key means "add to existing," otherwise new selection replaces old
676
+ selected.emptyAll();
677
+ // move or copy all of the elements in dragged set
678
+ undo.performOperation(MarkupApp.getActionName("copy"), () => this._dragging.forEach((el) => {
679
+ el.translate(delta.x, delta.y); // move to final location
680
+ const original = el.originalEl; // save original element
681
+ el.originalEl = undefined; // clear original element
682
+ if (ev.isShiftKey) {
683
+ selected.add(el);
684
+ undo.onAdded(el); // shift key means copy element
685
+ }
686
+ else {
687
+ original.replace(el);
688
+ undo.onModified(el, original);
689
+ }
690
+ }));
691
+ this._dragging.length = 0; // empty dragging set
692
+ selected.sizeChanged(); // notify that size of selection set changed
693
+ return EventHandled.Yes;
694
+ }
695
+ /** called when a modifier key is pressed or released. Updates stretch handles, if present */
696
+ async onModifierKeyTransition(_wentDown, modifier, _event) {
697
+ if (modifier !== BeModifierKeys.Shift) // we only care about the shift key
698
+ return EventHandled.No;
699
+ const selected = this.markup.selected;
700
+ const handles = selected.handles;
701
+ if (undefined === handles || !handles.dragging) // and only if we're currently dragging
702
+ return EventHandled.No;
703
+ const ev = new BeButtonEvent(); // we need to simulate a mouse motion by sending a drag event at the last cursor position
704
+ IModelApp.toolAdmin.fillEventFromCursorLocation(ev);
705
+ return (undefined === ev.viewport) ? EventHandled.No : (handles.drag(ev), EventHandled.Yes);
706
+ }
707
+ /** called whenever a key is pressed while this tool is active. */
708
+ async onKeyTransition(wentDown, key) {
709
+ if (!wentDown)
710
+ return EventHandled.No;
711
+ const markup = this.markup;
712
+ switch (key.key.toLowerCase()) {
713
+ case "delete": // delete key or backspace = delete current selection set
714
+ case "backspace":
715
+ this.unflashSelected();
716
+ markup.deleteSelected();
717
+ return EventHandled.Yes;
718
+ case "escape": // esc = cancel current operation
719
+ await this.exitTool();
720
+ return EventHandled.Yes;
721
+ case "b": // alt-shift-b = send to back
722
+ return (key.altKey && key.shiftKey) ? (markup.sendToBack(), EventHandled.Yes) : EventHandled.No;
723
+ case "f": // alt-shift-f = bring to front
724
+ return (key.altKey && key.shiftKey) ? (markup.bringToFront(), EventHandled.Yes) : EventHandled.No;
725
+ case "g": // ctrl-g = create group
726
+ return (key.ctrlKey) ? (this.unflashSelected(), markup.groupSelected(), EventHandled.Yes) : EventHandled.No;
727
+ case "u": // ctrl-u = ungroup
728
+ return (key.ctrlKey) ? (this.unflashSelected(), markup.ungroupSelected(), EventHandled.Yes) : EventHandled.No;
729
+ }
730
+ return EventHandled.No;
731
+ }
732
+ }
733
+ SelectTool.toolId = "Markup.Select";
734
+ SelectTool.iconSpec = "icon-cursor";
735
+ export { SelectTool };
735
736
  //# sourceMappingURL=SelectTool.js.map