@itwin/core-markup 4.0.0-dev.8 → 4.0.0-dev.80
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.
- package/CHANGELOG.md +41 -1
- package/lib/cjs/Markup.d.ts +323 -310
- package/lib/cjs/Markup.d.ts.map +1 -1
- package/lib/cjs/Markup.js +451 -420
- package/lib/cjs/Markup.js.map +1 -1
- package/lib/cjs/MarkupTool.d.ts +38 -38
- package/lib/cjs/MarkupTool.js +88 -88
- package/lib/cjs/MarkupTool.js.map +1 -1
- package/lib/cjs/RedlineTool.d.ts +145 -145
- package/lib/cjs/RedlineTool.d.ts.map +1 -1
- package/lib/cjs/RedlineTool.js +498 -512
- package/lib/cjs/RedlineTool.js.map +1 -1
- package/lib/cjs/SelectTool.d.ts +126 -126
- package/lib/cjs/SelectTool.js +741 -741
- package/lib/cjs/SelectTool.js.map +1 -1
- package/lib/cjs/SvgJsExt.d.ts +85 -85
- package/lib/cjs/SvgJsExt.js +185 -185
- package/lib/cjs/TextEdit.d.ts +43 -43
- package/lib/cjs/TextEdit.js +196 -196
- package/lib/cjs/TextEdit.js.map +1 -1
- package/lib/cjs/Undo.d.ts +46 -46
- package/lib/cjs/Undo.js +168 -168
- package/lib/cjs/core-markup.d.ts +18 -18
- package/lib/cjs/core-markup.js +38 -34
- package/lib/cjs/core-markup.js.map +1 -1
- package/lib/esm/Markup.d.ts +323 -310
- package/lib/esm/Markup.d.ts.map +1 -1
- package/lib/esm/Markup.js +447 -415
- package/lib/esm/Markup.js.map +1 -1
- package/lib/esm/MarkupTool.d.ts +38 -38
- package/lib/esm/MarkupTool.js +85 -84
- package/lib/esm/MarkupTool.js.map +1 -1
- package/lib/esm/RedlineTool.d.ts +145 -145
- package/lib/esm/RedlineTool.d.ts.map +1 -1
- package/lib/esm/RedlineTool.js +494 -498
- package/lib/esm/RedlineTool.js.map +1 -1
- package/lib/esm/SelectTool.d.ts +126 -126
- package/lib/esm/SelectTool.js +735 -734
- package/lib/esm/SelectTool.js.map +1 -1
- package/lib/esm/SvgJsExt.d.ts +85 -85
- package/lib/esm/SvgJsExt.js +180 -180
- package/lib/esm/TextEdit.d.ts +43 -43
- package/lib/esm/TextEdit.js +193 -191
- package/lib/esm/TextEdit.js.map +1 -1
- package/lib/esm/Undo.d.ts +46 -46
- package/lib/esm/Undo.js +164 -164
- package/lib/esm/core-markup.d.ts +18 -18
- package/lib/esm/core-markup.js +22 -22
- package/package.json +19 -19
package/lib/esm/SelectTool.js
CHANGED
|
@@ -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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
this.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
this.
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
|
|
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
|