@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.
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
package/lib/cjs/Markup.js CHANGED
@@ -1,421 +1,452 @@
1
- "use strict";
2
- /*---------------------------------------------------------------------------------------------
3
- * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
4
- * See LICENSE.md in the project root for license terms and full copyright notice.
5
- *--------------------------------------------------------------------------------------------*/
6
- /** @packageDocumentation
7
- * @module MarkupApp
8
- */
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.Markup = exports.MarkupApp = void 0;
11
- const core_bentley_1 = require("@itwin/core-bentley");
12
- const core_geometry_1 = require("@itwin/core-geometry");
13
- const core_common_1 = require("@itwin/core-common");
14
- const core_frontend_1 = require("@itwin/core-frontend");
15
- const svg_js_1 = require("@svgdotjs/svg.js");
16
- const redlineTool = require("./RedlineTool");
17
- const SelectTool_1 = require("./SelectTool");
18
- const textTool = require("./TextEdit");
19
- const Undo_1 = require("./Undo");
20
- /**
21
- * The main object for the Markup package. It is a singleton that stores the state of the Markup application.
22
- * It has only static members and methods. Applications may customize and control the behavior of the Markup by
23
- * setting members of [[MarkupApp.props]]. When [[MarkupApp.start]] is first called, it registers a set of "Markup.xxx"
24
- * tools that may be invoked from UI controls.
25
- * @public
26
- */
27
- class MarkupApp {
28
- /** @internal */
29
- static screenToVbMtx() {
30
- var _a, _b;
31
- const matrix = (_b = (_a = this.markup) === null || _a === void 0 ? void 0 : _a.svgMarkup) === null || _b === void 0 ? void 0 : _b.screenCTM().inverse();
32
- return (undefined !== matrix ? matrix : new svg_js_1.Matrix());
33
- }
34
- /** @internal */
35
- static getVpToScreenMtx() {
36
- const rect = this.markup.markupDiv.getBoundingClientRect();
37
- return (new svg_js_1.Matrix()).translateO(rect.left, rect.top);
38
- }
39
- /** @internal */
40
- static getVpToVbMtx() { return this.getVpToScreenMtx().lmultiplyO(this.screenToVbMtx()); }
41
- /** @internal */
42
- static convertVpToVb(pt) {
43
- const pt0 = new svg_js_1.Point(pt.x, pt.y);
44
- pt0.transformO(this.getVpToVbMtx());
45
- return new core_geometry_1.Point3d(pt0.x, pt0.y, 0);
46
- }
47
- /** determine whether there's a markup session currently active */
48
- static get isActive() { return undefined !== this.markup; }
49
- static createMarkup(view, markupData) { return new Markup(view, markupData); }
50
- static lockViewportSize(view, markupData) {
51
- const parentDiv = view.vpDiv;
52
- const rect = parentDiv.getBoundingClientRect();
53
- let width = rect.width;
54
- let height = rect.height;
55
- if (markupData) {
56
- const aspect = markupData.rect.height / markupData.rect.width;
57
- if ((width * aspect) > height)
58
- width = Math.floor(height / aspect);
59
- else
60
- height = Math.floor(width * aspect);
61
- }
62
- const style = parentDiv.style;
63
- style.width = `${width}px`;
64
- style.height = `${height}px`;
65
- }
66
- /** @internal */
67
- static getActionName(action) { return core_frontend_1.IModelApp.localization.getLocalizedString(`${this.namespace}:actions.${action}`); }
68
- /** Start a markup session */
69
- static async start(view, markupData) {
70
- if (this.markup)
71
- return; // a markup session is already active.
72
- await this.initialize();
73
- // first, lock the viewport to its current size while the markup session is running
74
- this.lockViewportSize(view, markupData);
75
- this.markup = this.createMarkup(view, markupData); // start a markup against the provided view.
76
- if (!this.markup.svgMarkup) {
77
- core_frontend_1.ScreenViewport.setToParentSize(this.markup.vp.vpDiv);
78
- this.markup.markupDiv.remove();
79
- return;
80
- }
81
- core_frontend_1.IModelApp.toolAdmin.markupView = view; // so viewing tools won't operate on the view.
82
- // set the markup Select tool as the default tool and start it, saving current default tool
83
- this._saveDefaultToolId = core_frontend_1.IModelApp.toolAdmin.defaultToolId;
84
- core_frontend_1.IModelApp.toolAdmin.defaultToolId = this.markupSelectToolId;
85
- return core_frontend_1.IModelApp.toolAdmin.startDefaultTool();
86
- }
87
- /** Read the result of a Markup session, then stop the session.
88
- * @note see [MarkupApp.props.result] for options.
89
- */
90
- static async stop() {
91
- const data = await this.readMarkup();
92
- if (!this.markup)
93
- return data;
94
- // restore original size for vp.
95
- core_frontend_1.ScreenViewport.setToParentSize(this.markup.vp.vpDiv);
96
- core_frontend_1.IModelApp.toolAdmin.markupView = undefined; // re-enable viewing tools for the view being marked-up
97
- this.markup.destroy();
98
- this.markup = undefined;
99
- // now restore the default tool and start it
100
- core_frontend_1.IModelApp.toolAdmin.defaultToolId = this._saveDefaultToolId;
101
- this._saveDefaultToolId = "";
102
- await core_frontend_1.IModelApp.toolAdmin.startDefaultTool();
103
- return data;
104
- }
105
- /** Call this method to initialize the Markup system.
106
- * It asynchronously loads the MarkupTools namespace for the prompts and tool names for the Markup system, and
107
- * also registers all of the Markup tools.
108
- * @return a Promise that may be awaited to ensure that the MarkupTools namespace had been loaded.
109
- * @note This method is automatically called every time you call [[start]]. Since the Markup tools cannot
110
- * start unless there is a Markup active, there's really no need to call this method directly.
111
- * The only virtue in doing so is to pre-load the Markup namespace if you have an opportunity to do so earlier in your
112
- * startup code.
113
- * @note This method may be called multiple times, but only the first time initiates the loading/registering. Subsequent
114
- * calls return the same Promise.
115
- */
116
- static async initialize() {
117
- if (undefined === this.namespace) { // only need to do this once
118
- this.namespace = "MarkupTools";
119
- const namespacePromise = core_frontend_1.IModelApp.localization.registerNamespace(this.namespace);
120
- core_frontend_1.IModelApp.tools.register(SelectTool_1.SelectTool, this.namespace);
121
- core_frontend_1.IModelApp.tools.registerModule(redlineTool, this.namespace);
122
- core_frontend_1.IModelApp.tools.registerModule(textTool, this.namespace);
123
- return namespacePromise;
124
- }
125
- return core_frontend_1.IModelApp.localization.getNamespacePromise(this.namespace); // so caller can make sure localized messages are ready.
126
- }
127
- /** convert the current markup SVG into a string, but don't include decorations or dynamics
128
- * @internal
129
- */
130
- static readMarkupSvg() {
131
- const markup = this.markup;
132
- if (!markup || !markup.svgContainer)
133
- return undefined;
134
- markup.svgDecorations.remove(); // we don't want the decorations or dynamics to be included
135
- markup.svgDynamics.remove();
136
- void core_frontend_1.IModelApp.toolAdmin.startDefaultTool();
137
- return markup.svgContainer.svg(); // string-ize the SVG data
138
- }
139
- /** convert the current markup SVG into a string (after calling readMarkupSvg) making sure width and height are specified.
140
- * @internal
141
- */
142
- static readMarkupSvgForDrawImage() {
143
- const markup = this.markup;
144
- if (!markup || !markup.svgContainer)
145
- return undefined;
146
- // Firefox requires width and height on top-level svg or drawImage does nothing, passing width/height to drawImage doesn't work.
147
- const rect = markup.markupDiv.getBoundingClientRect();
148
- markup.svgContainer.width(rect.width);
149
- markup.svgContainer.height(rect.height);
150
- return markup.svgContainer.svg(); // string-ize the SVG data
151
- }
152
- /** @internal */
153
- static async readMarkup() {
154
- const result = this.props.result;
155
- let canvas = this.markup.vp.readImageToCanvas();
156
- let svg, image;
157
- try {
158
- svg = this.readMarkupSvg(); // read the current svg data for the markup
159
- const svgForImage = (svg && result.imprintSvgOnImage ? this.readMarkupSvgForDrawImage() : undefined);
160
- if (svgForImage) {
161
- const svgImage = await (0, core_frontend_1.imageElementFromImageSource)(new core_common_1.ImageSource(svgForImage, core_common_1.ImageSourceFormat.Svg));
162
- canvas.getContext("2d").drawImage(svgImage, 0, 0); // draw markup svg onto view's canvas2d
163
- }
164
- // is the source view too wide? If so, we need to scale the image down.
165
- if (canvas.width > result.maxWidth) {
166
- // yes, we have to scale it down, create a new canvas and set the new canvas' size
167
- const newCanvas = document.createElement("canvas");
168
- newCanvas.width = result.maxWidth;
169
- newCanvas.height = canvas.height * (result.maxWidth / canvas.width);
170
- newCanvas.getContext("2d").drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, newCanvas.width, newCanvas.height);
171
- canvas = newCanvas; // return the image from adjusted canvas, not view canvas.
172
- }
173
- // return the markup data to be saved by the application.
174
- image = (!result.imageFormat ? undefined : canvas.toDataURL(result.imageFormat));
175
- }
176
- catch (e) {
177
- core_bentley_1.Logger.logError(`${core_frontend_1.FrontendLoggerCategory.Package}.markup`, "Error creating image from svg", core_bentley_1.BentleyError.getErrorProps(e));
178
- }
179
- return { rect: { width: canvas.width, height: canvas.height }, svg, image };
180
- }
181
- /** @internal */
182
- static get dropShadowId() { return `${this.markupPrefix}dropShadow`; } // this is referenced in the markup Svg to apply the drop-shadow filter to all markup elements.
183
- /** @internal */
184
- static get cornerId() { return `${this.markupPrefix}photoCorner`; }
185
- /** @internal */
186
- static get containerClass() { return `${this.markupPrefix}container`; }
187
- /** @internal */
188
- static get dynamicsClass() { return `${this.markupPrefix}dynamics`; }
189
- /** @internal */
190
- static get decorationsClass() { return `${this.markupPrefix}decorations`; }
191
- /** @internal */
192
- static get markupSvgClass() { return `${this.markupPrefix}svg`; }
193
- /** @internal */
194
- static get boxedTextClass() { return `${this.markupPrefix}boxedText`; }
195
- /** @internal */
196
- static get textClass() { return `${this.markupPrefix}text`; }
197
- /** @internal */
198
- static get stretchHandleClass() { return `${this.markupPrefix}stretchHandle`; }
199
- /** @internal */
200
- static get rotateLineClass() { return `${this.markupPrefix}rotateLine`; }
201
- /** @internal */
202
- static get rotateHandleClass() { return `${this.markupPrefix}rotateHandle`; }
203
- /** @internal */
204
- static get vertexHandleClass() { return `${this.markupPrefix}vertexHandle`; }
205
- /** @internal */
206
- static get moveHandleClass() { return `${this.markupPrefix}moveHandle`; }
207
- /** @internal */
208
- static get textOutlineClass() { return `${this.markupPrefix}textOutline`; }
209
- /** @internal */
210
- static get textEditorClass() { return `${this.markupPrefix}textEditor`; }
211
- }
212
- exports.MarkupApp = MarkupApp;
213
- /** By setting members of this object, applications can control the appearance and behavior of various parts of MarkupApp. */
214
- MarkupApp.props = {
215
- /** the UI controls displayed on Elements by the Select Tool to allow users to modify them. */
216
- handles: {
217
- /** The diameter of the circles for the handles. */
218
- size: 10,
219
- /** The attributes of the stretch handles */
220
- stretch: { "fill-opacity": .85, "stroke": "black", "fill": "white" },
221
- /** The attributes of the line that connects the top-center stretch handle to the rotate handle. */
222
- rotateLine: { "stroke": "grey", "fill-opacity": .85 },
223
- /** The attributes of the rotate handle. */
224
- rotate: { "cursor": `url(${core_frontend_1.IModelApp.publicPath}Markup/rotate.png) 12 12, auto`, "fill-opacity": .85, "stroke": "black", "fill": "lightBlue" },
225
- /** The attributes of box around the element. */
226
- moveOutline: { "cursor": "move", "stroke-dasharray": "6,6", "fill": "none", "stroke-opacity": .85, "stroke": "white" },
227
- /** The attributes of box that provides the move cursor. */
228
- move: { "cursor": "move", "opacity": 0, "stroke-width": 10, "stroke": "white" },
229
- /** The attributes of handles on the vertices of lines. */
230
- vertex: { "cursor": `url(${core_frontend_1.IModelApp.publicPath}cursors/crosshair.cur), crosshair`, "fill-opacity": .85, "stroke": "black", "fill": "white" },
231
- },
232
- /** properties for providing feedback about selected elements. */
233
- hilite: {
234
- /** the color of selected elements */
235
- color: "magenta",
236
- /** the color of an element as the cursor passes over it */
237
- flash: "cyan",
238
- },
239
- /** optionally, show a drop-shadow behind all markup elements. */
240
- dropShadow: {
241
- /** if false, no drop shadow */
242
- enable: true,
243
- /** the attributes of the drop shadow. See https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feDropShadow */
244
- attr: {
245
- "stdDeviation": 2,
246
- "dx": 1.2,
247
- "dy": 1.4,
248
- "flood-color": "#1B3838",
249
- },
250
- },
251
- /** The "active placement" parameters. New elements are created with these parameters, so UI controls should set them. */
252
- active: {
253
- /** the CSS style properties of new text elements. */
254
- text: {
255
- "font-family": "sans-serif",
256
- "font-size": "30px",
257
- "stroke": "none",
258
- "fill": "red",
259
- },
260
- /** the CSS style properties of new elements. */
261
- element: {
262
- "stroke": "red",
263
- "stroke-opacity": 0.8,
264
- "stroke-width": 3,
265
- "stroke-dasharray": 0,
266
- "stroke-linecap": "round",
267
- "stroke-linejoin": "round",
268
- "fill": "blue",
269
- "fill-opacity": 0.2,
270
- },
271
- arrow: {
272
- length: 7,
273
- width: 6,
274
- },
275
- cloud: {
276
- path: "M3.0,2.5 C3.9,.78 5.6,-.4 8.1,1.0 C9.1,0 11.3,-.2 12.5,.5 C14.2,-.5 17,.16 17.9,2.5 C21,3 20.2,7.3 17.6,7.5 C16.5,9.2 14.4,9.8 12.7,8.9 C11.6,10 9.5,10.3 8.1,9.4 C5.7,10.8 3.3,9.4 2.6,7.5 C-.9,7.7 .6,1.7 3.0,2.5z",
277
- },
278
- },
279
- /** Values for placing and editing Text. */
280
- text: {
281
- /** A default string for the Markup.Text.Place command. Applications can turn this off, or supply the user's initials, for example. */
282
- startValue: "Note: ",
283
- /** Parameters for the size and appearance of the text editor */
284
- edit: {
285
- background: "blanchedalmond",
286
- /** Starting size, will be updated if user stretches the box */
287
- size: { width: "25%", height: "4em" },
288
- /** font size of the text editor */
289
- fontSize: "14pt",
290
- /** A background box drawn around text so user can tell what's being selected */
291
- textBox: { "fill": "lightGrey", "fill-opacity": .1, "stroke-opacity": .85, "stroke": "lightBlue" },
292
- },
293
- },
294
- /** Used to draw the border outline around the view while it is being marked up so the user can tell Markup is active */
295
- borderOutline: {
296
- "stroke": "gold",
297
- "stroke-width": 6,
298
- "stroke-opacity": 0.4,
299
- "fill": "none",
300
- },
301
- /** Used to draw the border corner symbols for the view while it is being marked up so the user can tell Markup is active */
302
- borderCorners: {
303
- "stroke": "black",
304
- "stroke-width": 2,
305
- "stroke-opacity": 0.2,
306
- "fill": "gold",
307
- "fill-opacity": 0.2,
308
- },
309
- /** Determines what is returned by MarkupApp.stop */
310
- result: {
311
- /** The format for the image data. */
312
- imageFormat: "image/png",
313
- /** If true, the markup graphics will be imprinted in the returned image. */
314
- imprintSvgOnImage: true,
315
- /** the maximum width for the returned image. If the source view width is larger than this, it will be scaled down to this size. */
316
- maxWidth: 2048,
317
- },
318
- };
319
- MarkupApp._saveDefaultToolId = "";
320
- MarkupApp.markupSelectToolId = "Markup.Select";
321
- /** @internal */
322
- MarkupApp.markupPrefix = "markup-";
323
- const removeSvgNamespace = (svg) => {
324
- svg.node.removeAttribute("xmlns:svgjs");
325
- return svg;
326
- };
327
- const newSvgElement = (name) => (0, svg_js_1.adopt)((0, svg_js_1.create)(name));
328
- /**
329
- * The current markup being created/edited. Holds the SVG elements, plus the active [[MarkupTool]].
330
- * When starting a Markup, a new Div is added as a child of the ScreenViewport's vpDiv.
331
- * @public
332
- */
333
- class Markup {
334
- /** Create a new Markup for the supplied ScreenViewport. Adds a new "overlay-markup" div into the "vpDiv"
335
- * of the viewport.
336
- * @note you must call destroy on this object at end of markup to remove the markup div.
337
- */
338
- constructor(vp, markupData) {
339
- this.vp = vp;
340
- /** @internal */
341
- this.undo = new Undo_1.UndoManager();
342
- this.markupDiv = vp.addNewDiv("overlay-markup", true, 20); // this div goes on top of the canvas, but behind UI layers
343
- const rect = this.markupDiv.getBoundingClientRect();
344
- // First, see if there is a markup passed in as an argument
345
- if (markupData && markupData.svg) {
346
- this.markupDiv.innerHTML = markupData.svg; // make it a child of the markupDiv
347
- this.svgContainer = (0, svg_js_1.SVG)(`.${MarkupApp.containerClass}`); // get it in svg.js format
348
- this.svgMarkup = (0, svg_js_1.SVG)(`.${MarkupApp.markupSvgClass}`);
349
- if (!this.svgContainer || !this.svgMarkup) // if either isn't present, its not a valid markup
350
- return;
351
- removeSvgNamespace(this.svgContainer); // the SVG call above adds this - remove it
352
- this.svgMarkup.each(() => { }, true); // create an SVG.Element for each entry in the supplied markup.
353
- }
354
- else {
355
- // create the container that will be returned as the "svg" data for this markup
356
- this.svgContainer = (0, svg_js_1.SVG)().addTo(this.markupDiv).addClass(MarkupApp.containerClass).viewbox(0, 0, rect.width, rect.height);
357
- removeSvgNamespace(this.svgContainer);
358
- this.svgMarkup = this.addNested(MarkupApp.markupSvgClass);
359
- }
360
- if (MarkupApp.props.dropShadow.enable) {
361
- this.createDropShadow(this.svgContainer);
362
- this.svgContainer.attr("filter", `url(#${MarkupApp.dropShadowId})`);
363
- }
364
- /** add two nested groups for providing feedback during the markup session. These Svgs are removed before the data is returned. */
365
- this.svgDynamics = this.addNested(MarkupApp.dynamicsClass); // only for tool dynamics of SVG graphics.
366
- this.svgDecorations = this.addNested(MarkupApp.decorationsClass); // only for temporary decorations of SVG graphics.
367
- this.addBorder();
368
- this.selected = new SelectTool_1.MarkupSelected(this.svgDecorations);
369
- }
370
- /** create the drop-shadow filter in the Defs section of the supplied svg element */
371
- createDropShadow(svg) {
372
- const filter = (0, svg_js_1.SVG)(`#${MarkupApp.dropShadowId}`); // see if we already have one?
373
- if (filter)
374
- filter.remove(); // yes, remove it. This must be someone modifying the drop shadow properties
375
- // create a new filter, and add it to the Defs of the supplied svg
376
- svg.defs()
377
- .add(newSvgElement("filter").id(MarkupApp.dropShadowId)
378
- .add(newSvgElement("feDropShadow").attr(MarkupApp.props.dropShadow.attr)));
379
- }
380
- addNested(className) { return this.svgContainer.group().addClass(className); }
381
- addBorder() {
382
- const rect = this.svgContainer.viewbox();
383
- const inset = MarkupApp.props.borderOutline["stroke-width"];
384
- const cornerSize = inset * 6;
385
- const cornerPts = [0, 0, cornerSize, 0, cornerSize * .7, cornerSize * .3, cornerSize * .3, cornerSize * .3, cornerSize * .3, cornerSize * .7, 0, cornerSize];
386
- const decorations = this.svgDecorations;
387
- const photoCorner = decorations.symbol().polygon(cornerPts).attr(MarkupApp.props.borderCorners).id(MarkupApp.cornerId);
388
- const cornerGroup = decorations.group();
389
- cornerGroup.rect(rect.width - inset, rect.height - inset).move(inset / 2, inset / 2).attr(MarkupApp.props.borderOutline);
390
- cornerGroup.use(photoCorner);
391
- cornerGroup.use(photoCorner).rotate(90).translate(rect.width - cornerSize, 0);
392
- cornerGroup.use(photoCorner).rotate(180).translate(rect.width - cornerSize, rect.height - cornerSize);
393
- cornerGroup.use(photoCorner).rotate(270).translate(0, rect.height - cornerSize);
394
- }
395
- /** Called when the Markup is destroyed */
396
- destroy() { this.markupDiv.remove(); }
397
- /** Turn on picking the markup elements in the markup view */
398
- enablePick() { this.markupDiv.style.pointerEvents = "auto"; }
399
- /** Turn off picking the markup elements in the markup view */
400
- disablePick() { this.markupDiv.style.pointerEvents = "none"; }
401
- /** Change the default cursor for the markup view */
402
- setCursor(cursor) { this.markupDiv.style.cursor = cursor; }
403
- /** Delete all the entries in the selection set, then empty it. */
404
- deleteSelected() { this.selected.deleteAll(this.undo); }
405
- /** Bring all the entries in the selection set to the front. */
406
- bringToFront() { this.selected.reposition(MarkupApp.getActionName("toFront"), this.undo, (el) => el.front()); }
407
- /** Send all the entries in the selection set to the back. */
408
- sendToBack() { this.selected.reposition(MarkupApp.getActionName("toBack"), this.undo, (el) => el.back()); }
409
- /** Group all the entries in the selection set, then select the group. */
410
- groupSelected() {
411
- if (undefined !== this.svgMarkup)
412
- this.selected.groupAll(this.undo);
413
- }
414
- /** Ungroup all the group entries in the selection set. */
415
- ungroupSelected() {
416
- if (undefined !== this.svgMarkup)
417
- this.selected.ungroupAll(this.undo);
418
- }
419
- }
420
- exports.Markup = Markup;
1
+ "use strict";
2
+ /*---------------------------------------------------------------------------------------------
3
+ * Copyright (c) Bentley Systems, Incorporated. All rights reserved.
4
+ * See LICENSE.md in the project root for license terms and full copyright notice.
5
+ *--------------------------------------------------------------------------------------------*/
6
+ /** @packageDocumentation
7
+ * @module MarkupApp
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.Markup = exports.MarkupApp = void 0;
11
+ const core_bentley_1 = require("@itwin/core-bentley");
12
+ const core_geometry_1 = require("@itwin/core-geometry");
13
+ const core_common_1 = require("@itwin/core-common");
14
+ const core_frontend_1 = require("@itwin/core-frontend");
15
+ const svg_js_1 = require("@svgdotjs/svg.js");
16
+ const redlineTool = require("./RedlineTool");
17
+ const SelectTool_1 = require("./SelectTool");
18
+ const textTool = require("./TextEdit");
19
+ const Undo_1 = require("./Undo");
20
+ /**
21
+ * The main object for the Markup package. It is a singleton that stores the state of the Markup application.
22
+ * It has only static members and methods. Applications may customize and control the behavior of the Markup by
23
+ * setting members of [[MarkupApp.props]]. When [[MarkupApp.start]] is first called, it registers a set of "Markup.xxx"
24
+ * tools that may be invoked from UI controls.
25
+ * @public
26
+ */
27
+ class MarkupApp {
28
+ /** @internal */
29
+ static screenToVbMtx() {
30
+ const matrix = this.markup?.svgMarkup?.screenCTM().inverse();
31
+ return (undefined !== matrix ? matrix : new svg_js_1.Matrix());
32
+ }
33
+ /** @internal */
34
+ static getVpToScreenMtx() {
35
+ const rect = this.markup.markupDiv.getBoundingClientRect();
36
+ return (new svg_js_1.Matrix()).translateO(rect.left, rect.top);
37
+ }
38
+ /** @internal */
39
+ static getVpToVbMtx() { return this.getVpToScreenMtx().lmultiplyO(this.screenToVbMtx()); }
40
+ /** @internal */
41
+ static convertVpToVb(pt) {
42
+ const pt0 = new svg_js_1.Point(pt.x, pt.y);
43
+ pt0.transformO(this.getVpToVbMtx());
44
+ return new core_geometry_1.Point3d(pt0.x, pt0.y, 0);
45
+ }
46
+ /** determine whether there's a markup session currently active */
47
+ static get isActive() { return undefined !== this.markup; }
48
+ static createMarkup(view, markupData) { return new Markup(view, markupData); }
49
+ static lockViewportSize(view, markupData) {
50
+ const parentDiv = view.vpDiv;
51
+ const rect = parentDiv.getBoundingClientRect();
52
+ let width = rect.width;
53
+ let height = rect.height;
54
+ if (markupData) {
55
+ const aspect = markupData.rect.height / markupData.rect.width;
56
+ if ((width * aspect) > height)
57
+ width = Math.floor(height / aspect);
58
+ else
59
+ height = Math.floor(width * aspect);
60
+ }
61
+ const style = parentDiv.style;
62
+ style.width = `${width}px`;
63
+ style.height = `${height}px`;
64
+ }
65
+ /** @internal */
66
+ static getActionName(action) { return core_frontend_1.IModelApp.localization.getLocalizedString(`${this.namespace}:actions.${action}`); }
67
+ /** Start a markup session */
68
+ static async start(view, markupData) {
69
+ if (this.markup)
70
+ return; // a markup session is already active.
71
+ await this.initialize();
72
+ // first, lock the viewport to its current size while the markup session is running
73
+ this.lockViewportSize(view, markupData);
74
+ this.markup = this.createMarkup(view, markupData); // start a markup against the provided view.
75
+ if (!this.markup.svgMarkup) {
76
+ core_frontend_1.ScreenViewport.setToParentSize(this.markup.vp.vpDiv);
77
+ this.markup.markupDiv.remove();
78
+ return;
79
+ }
80
+ core_frontend_1.IModelApp.toolAdmin.markupView = view; // so viewing tools won't operate on the view.
81
+ // set the markup Select tool as the default tool and start it, saving current default tool
82
+ this._saveDefaultToolId = core_frontend_1.IModelApp.toolAdmin.defaultToolId;
83
+ core_frontend_1.IModelApp.toolAdmin.defaultToolId = this.markupSelectToolId;
84
+ return core_frontend_1.IModelApp.toolAdmin.startDefaultTool();
85
+ }
86
+ /** Read the result of a Markup session, then stop the session.
87
+ * @note see [MarkupApp.props.result] for options.
88
+ */
89
+ static async stop() {
90
+ const data = await this.readMarkup();
91
+ if (!this.markup)
92
+ return data;
93
+ // restore original size for vp.
94
+ core_frontend_1.ScreenViewport.setToParentSize(this.markup.vp.vpDiv);
95
+ core_frontend_1.IModelApp.toolAdmin.markupView = undefined; // re-enable viewing tools for the view being marked-up
96
+ this.markup.destroy();
97
+ this.markup = undefined;
98
+ // now restore the default tool and start it
99
+ core_frontend_1.IModelApp.toolAdmin.defaultToolId = this._saveDefaultToolId;
100
+ this._saveDefaultToolId = "";
101
+ await core_frontend_1.IModelApp.toolAdmin.startDefaultTool();
102
+ return data;
103
+ }
104
+ /** Call this method to initialize the Markup system.
105
+ * It asynchronously loads the MarkupTools namespace for the prompts and tool names for the Markup system, and
106
+ * also registers all of the Markup tools.
107
+ * @return a Promise that may be awaited to ensure that the MarkupTools namespace had been loaded.
108
+ * @note This method is automatically called every time you call [[start]]. Since the Markup tools cannot
109
+ * start unless there is a Markup active, there's really no need to call this method directly.
110
+ * The only virtue in doing so is to pre-load the Markup namespace if you have an opportunity to do so earlier in your
111
+ * startup code.
112
+ * @note This method may be called multiple times, but only the first time initiates the loading/registering. Subsequent
113
+ * calls return the same Promise.
114
+ */
115
+ static async initialize() {
116
+ if (undefined === this.namespace) { // only need to do this once
117
+ this.namespace = "MarkupTools";
118
+ const namespacePromise = core_frontend_1.IModelApp.localization.registerNamespace(this.namespace);
119
+ core_frontend_1.IModelApp.tools.register(SelectTool_1.SelectTool, this.namespace);
120
+ core_frontend_1.IModelApp.tools.registerModule(redlineTool, this.namespace);
121
+ core_frontend_1.IModelApp.tools.registerModule(textTool, this.namespace);
122
+ return namespacePromise;
123
+ }
124
+ return core_frontend_1.IModelApp.localization.getNamespacePromise(this.namespace); // so caller can make sure localized messages are ready.
125
+ }
126
+ /** convert the current markup SVG into a string, but don't include decorations or dynamics
127
+ * @internal
128
+ */
129
+ static readMarkupSvg() {
130
+ const markup = this.markup;
131
+ if (!markup || !markup.svgContainer)
132
+ return undefined;
133
+ markup.svgDecorations.remove(); // we don't want the decorations or dynamics to be included
134
+ markup.svgDynamics.remove();
135
+ void core_frontend_1.IModelApp.toolAdmin.startDefaultTool();
136
+ return markup.svgContainer.svg(); // string-ize the SVG data
137
+ }
138
+ /** convert the current markup SVG into a string (after calling readMarkupSvg) making sure width and height are specified.
139
+ * @internal
140
+ */
141
+ static readMarkupSvgForDrawImage() {
142
+ const markup = this.markup;
143
+ if (!markup || !markup.svgContainer)
144
+ return undefined;
145
+ // Firefox requires width and height on top-level svg or drawImage does nothing, passing width/height to drawImage doesn't work.
146
+ const rect = markup.markupDiv.getBoundingClientRect();
147
+ markup.svgContainer.width(rect.width);
148
+ markup.svgContainer.height(rect.height);
149
+ return markup.svgContainer.svg(); // string-ize the SVG data
150
+ }
151
+ /** @internal */
152
+ static async readMarkup() {
153
+ const result = this.props.result;
154
+ let canvas = this.markup.vp.readImageToCanvas();
155
+ let svg, image;
156
+ try {
157
+ svg = this.readMarkupSvg(); // read the current svg data for the markup
158
+ const svgForImage = (svg && result.imprintSvgOnImage ? this.readMarkupSvgForDrawImage() : undefined);
159
+ if (svgForImage) {
160
+ const svgImage = await (0, core_frontend_1.imageElementFromImageSource)(new core_common_1.ImageSource(svgForImage, core_common_1.ImageSourceFormat.Svg));
161
+ canvas.getContext("2d").drawImage(svgImage, 0, 0); // draw markup svg onto view's canvas2d
162
+ }
163
+ // is the source view too wide? If so, we need to scale the image down.
164
+ if (canvas.width > result.maxWidth) {
165
+ // yes, we have to scale it down, create a new canvas and set the new canvas' size
166
+ const newCanvas = document.createElement("canvas");
167
+ newCanvas.width = result.maxWidth;
168
+ newCanvas.height = canvas.height * (result.maxWidth / canvas.width);
169
+ newCanvas.getContext("2d").drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, newCanvas.width, newCanvas.height);
170
+ canvas = newCanvas; // return the image from adjusted canvas, not view canvas.
171
+ }
172
+ // return the markup data to be saved by the application.
173
+ image = (!result.imageFormat ? undefined : canvas.toDataURL(result.imageFormat));
174
+ }
175
+ catch (e) {
176
+ core_bentley_1.Logger.logError(`${core_frontend_1.FrontendLoggerCategory.Package}.markup`, "Error creating image from svg", core_bentley_1.BentleyError.getErrorProps(e));
177
+ }
178
+ return { rect: { width: canvas.width, height: canvas.height }, svg, image };
179
+ }
180
+ /** @internal */
181
+ static get dropShadowId() { return `${this.markupPrefix}dropShadow`; } // this is referenced in the markup Svg to apply the drop-shadow filter to all markup elements.
182
+ /** @internal */
183
+ static get cornerId() { return `${this.markupPrefix}photoCorner`; }
184
+ /** @internal */
185
+ static get containerClass() { return `${this.markupPrefix}container`; }
186
+ /** @internal */
187
+ static get dynamicsClass() { return `${this.markupPrefix}dynamics`; }
188
+ /** @internal */
189
+ static get decorationsClass() { return `${this.markupPrefix}decorations`; }
190
+ /** @internal */
191
+ static get markupSvgClass() { return `${this.markupPrefix}svg`; }
192
+ /** @internal */
193
+ static get boxedTextClass() { return `${this.markupPrefix}boxedText`; }
194
+ /** @internal */
195
+ static get textClass() { return `${this.markupPrefix}text`; }
196
+ /** @internal */
197
+ static get stretchHandleClass() { return `${this.markupPrefix}stretchHandle`; }
198
+ /** @internal */
199
+ static get rotateLineClass() { return `${this.markupPrefix}rotateLine`; }
200
+ /** @internal */
201
+ static get rotateHandleClass() { return `${this.markupPrefix}rotateHandle`; }
202
+ /** @internal */
203
+ static get vertexHandleClass() { return `${this.markupPrefix}vertexHandle`; }
204
+ /** @internal */
205
+ static get moveHandleClass() { return `${this.markupPrefix}moveHandle`; }
206
+ /** @internal */
207
+ static get textOutlineClass() { return `${this.markupPrefix}textOutline`; }
208
+ /** @internal */
209
+ static get textEditorClass() { return `${this.markupPrefix}textEditor`; }
210
+ }
211
+ /** By setting members of this object, applications can control the appearance and behavior of various parts of MarkupApp. */
212
+ MarkupApp.props = {
213
+ /** the UI controls displayed on Elements by the Select Tool to allow users to modify them. */
214
+ handles: {
215
+ /** The diameter of the circles for the handles. */
216
+ size: 10,
217
+ /** The attributes of the stretch handles */
218
+ stretch: { "fill-opacity": .85, "stroke": "black", "fill": "white" },
219
+ /** The attributes of the line that connects the top-center stretch handle to the rotate handle. */
220
+ rotateLine: { "stroke": "grey", "fill-opacity": .85 },
221
+ /** The attributes of the rotate handle. */
222
+ rotate: { "cursor": `url(${core_frontend_1.IModelApp.publicPath}Markup/rotate.png) 12 12, auto`, "fill-opacity": .85, "stroke": "black", "fill": "lightBlue" },
223
+ /** The attributes of box around the element. */
224
+ moveOutline: { "cursor": "move", "stroke-dasharray": "6,6", "fill": "none", "stroke-opacity": .85, "stroke": "white" },
225
+ /** The attributes of box that provides the move cursor. */
226
+ move: { "cursor": "move", "opacity": 0, "stroke-width": 10, "stroke": "white" },
227
+ /** The attributes of handles on the vertices of lines. */
228
+ vertex: { "cursor": `url(${core_frontend_1.IModelApp.publicPath}cursors/crosshair.cur), crosshair`, "fill-opacity": .85, "stroke": "black", "fill": "white" },
229
+ },
230
+ /** properties for providing feedback about selected elements. */
231
+ hilite: {
232
+ /** the color of selected elements */
233
+ color: "magenta",
234
+ /** the color of an element as the cursor passes over it */
235
+ flash: "cyan",
236
+ },
237
+ /** optionally, show a drop-shadow behind all markup elements. */
238
+ dropShadow: {
239
+ /** if false, no drop shadow */
240
+ enable: true,
241
+ /** the attributes of the drop shadow. See https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feDropShadow */
242
+ attr: {
243
+ "stdDeviation": 2,
244
+ "dx": 1.2,
245
+ "dy": 1.4,
246
+ "flood-color": "#1B3838",
247
+ },
248
+ },
249
+ /** The "active placement" parameters. New elements are created with these parameters, so UI controls should set them. */
250
+ active: {
251
+ /** the CSS style properties of new text elements. */
252
+ text: {
253
+ "font-family": "sans-serif",
254
+ "font-size": "30px",
255
+ "stroke": "none",
256
+ "fill": "red",
257
+ },
258
+ /** the CSS style properties of new elements. */
259
+ element: {
260
+ "stroke": "red",
261
+ "stroke-opacity": 0.8,
262
+ "stroke-width": 3,
263
+ "stroke-dasharray": 0,
264
+ "stroke-linecap": "round",
265
+ "stroke-linejoin": "round",
266
+ "fill": "blue",
267
+ "fill-opacity": 0.2,
268
+ },
269
+ arrow: {
270
+ length: 7,
271
+ width: 6,
272
+ },
273
+ cloud: {
274
+ path: "M3.0,2.5 C3.9,.78 5.6,-.4 8.1,1.0 C9.1,0 11.3,-.2 12.5,.5 C14.2,-.5 17,.16 17.9,2.5 C21,3 20.2,7.3 17.6,7.5 C16.5,9.2 14.4,9.8 12.7,8.9 C11.6,10 9.5,10.3 8.1,9.4 C5.7,10.8 3.3,9.4 2.6,7.5 C-.9,7.7 .6,1.7 3.0,2.5z",
275
+ },
276
+ },
277
+ /** Values for placing and editing Text. */
278
+ text: {
279
+ /** A default string for the Markup.Text.Place command. Applications can turn this off, or supply the user's initials, for example. */
280
+ startValue: "Note: ",
281
+ /** Parameters for the size and appearance of the text editor */
282
+ edit: {
283
+ background: "blanchedalmond",
284
+ /** Starting size, will be updated if user stretches the box */
285
+ size: { width: "25%", height: "4em" },
286
+ /** font size of the text editor */
287
+ fontSize: "14pt",
288
+ /** A background box drawn around text so user can tell what's being selected */
289
+ textBox: { "fill": "lightGrey", "fill-opacity": .1, "stroke-opacity": .85, "stroke": "lightBlue" },
290
+ },
291
+ },
292
+ /** Used to draw the border outline around the view while it is being marked up so the user can tell Markup is active */
293
+ borderOutline: {
294
+ "stroke": "gold",
295
+ "stroke-width": 6,
296
+ "stroke-opacity": 0.4,
297
+ "fill": "none",
298
+ },
299
+ /** Used to draw the border corner symbols for the view while it is being marked up so the user can tell Markup is active */
300
+ borderCorners: {
301
+ "stroke": "black",
302
+ "stroke-width": 2,
303
+ "stroke-opacity": 0.2,
304
+ "fill": "gold",
305
+ "fill-opacity": 0.2,
306
+ },
307
+ /** Determines what is returned by MarkupApp.stop */
308
+ result: {
309
+ /** The format for the image data. */
310
+ imageFormat: "image/png",
311
+ /** If true, the markup graphics will be imprinted in the returned image. */
312
+ imprintSvgOnImage: true,
313
+ /** the maximum width for the returned image. If the source view width is larger than this, it will be scaled down to this size. */
314
+ maxWidth: 2048,
315
+ },
316
+ };
317
+ MarkupApp._saveDefaultToolId = "";
318
+ MarkupApp.markupSelectToolId = "Markup.Select";
319
+ /** @internal */
320
+ MarkupApp.markupPrefix = "markup-";
321
+ exports.MarkupApp = MarkupApp;
322
+ const removeSvgNamespace = (svg) => {
323
+ svg.node.removeAttribute("xmlns:svgjs");
324
+ return svg;
325
+ };
326
+ const newSvgElement = (name) => (0, svg_js_1.adopt)((0, svg_js_1.create)(name));
327
+ /**
328
+ * The current markup being created/edited. Holds the SVG elements, plus the active [[MarkupTool]].
329
+ * When starting a Markup, a new Div is added as a child of the ScreenViewport's vpDiv.
330
+ * @public
331
+ */
332
+ class Markup {
333
+ /** create the drop-shadow filter in the Defs section of the supplied svg element */
334
+ createDropShadow(svg) {
335
+ const filter = (0, svg_js_1.SVG)(`#${MarkupApp.dropShadowId}`); // see if we already have one?
336
+ if (filter)
337
+ filter.remove(); // yes, remove it. This must be someone modifying the drop shadow properties
338
+ // create a new filter, and add it to the Defs of the supplied svg
339
+ svg.defs()
340
+ .add(newSvgElement("filter").id(MarkupApp.dropShadowId)
341
+ .add(newSvgElement("feDropShadow").attr(MarkupApp.props.dropShadow.attr)));
342
+ }
343
+ addNested(className) { return this.svgContainer.group().addClass(className); }
344
+ addBorder() {
345
+ const rect = this.svgContainer.viewbox();
346
+ const inset = MarkupApp.props.borderOutline["stroke-width"];
347
+ const cornerSize = inset * 6;
348
+ const cornerPts = [0, 0, cornerSize, 0, cornerSize * .7, cornerSize * .3, cornerSize * .3, cornerSize * .3, cornerSize * .3, cornerSize * .7, 0, cornerSize];
349
+ const decorations = this.svgDecorations;
350
+ const photoCorner = decorations.symbol().polygon(cornerPts).attr(MarkupApp.props.borderCorners).id(MarkupApp.cornerId);
351
+ const cornerGroup = decorations.group();
352
+ cornerGroup.rect(rect.width - inset, rect.height - inset).move(inset / 2, inset / 2).attr(MarkupApp.props.borderOutline);
353
+ cornerGroup.use(photoCorner);
354
+ cornerGroup.use(photoCorner).rotate(90).translate(rect.width - cornerSize, 0);
355
+ cornerGroup.use(photoCorner).rotate(180).translate(rect.width - cornerSize, rect.height - cornerSize);
356
+ cornerGroup.use(photoCorner).rotate(270).translate(0, rect.height - cornerSize);
357
+ }
358
+ /** Create a new Markup for the supplied ScreenViewport. Adds a new "overlay-markup" div into the "vpDiv"
359
+ * of the viewport.
360
+ * @note you must call destroy on this object at end of markup to remove the markup div.
361
+ */
362
+ constructor(vp, markupData) {
363
+ this.vp = vp;
364
+ /** Support undo/redo of markup operations */
365
+ this.undo = new Undo_1.UndoManager();
366
+ this.markupDiv = vp.addNewDiv("overlay-markup", true, 20); // this div goes on top of the canvas, but behind UI layers
367
+ const rect = this.markupDiv.getBoundingClientRect();
368
+ // First, see if there is a markup passed in as an argument
369
+ if (markupData && markupData.svg) {
370
+ this.markupDiv.innerHTML = markupData.svg; // make it a child of the markupDiv
371
+ this.svgContainer = (0, svg_js_1.SVG)(`.${MarkupApp.containerClass}`); // get it in svg.js format
372
+ this.svgMarkup = (0, svg_js_1.SVG)(`.${MarkupApp.markupSvgClass}`);
373
+ if (!this.svgContainer || !this.svgMarkup) // if either isn't present, its not a valid markup
374
+ return;
375
+ removeSvgNamespace(this.svgContainer); // the SVG call above adds this - remove it
376
+ this.svgMarkup.each(() => { }, true); // create an SVG.Element for each entry in the supplied markup.
377
+ }
378
+ else {
379
+ // create the container that will be returned as the "svg" data for this markup
380
+ this.svgContainer = (0, svg_js_1.SVG)().addTo(this.markupDiv).addClass(MarkupApp.containerClass).viewbox(0, 0, rect.width, rect.height);
381
+ removeSvgNamespace(this.svgContainer);
382
+ this.svgMarkup = this.addNested(MarkupApp.markupSvgClass);
383
+ }
384
+ if (MarkupApp.props.dropShadow.enable) {
385
+ this.createDropShadow(this.svgContainer);
386
+ this.svgContainer.attr("filter", `url(#${MarkupApp.dropShadowId})`);
387
+ }
388
+ /** add two nested groups for providing feedback during the markup session. These Svgs are removed before the data is returned. */
389
+ this.svgDynamics = this.addNested(MarkupApp.dynamicsClass); // only for tool dynamics of SVG graphics.
390
+ this.svgDecorations = this.addNested(MarkupApp.decorationsClass); // only for temporary decorations of SVG graphics.
391
+ this.addBorder();
392
+ this.selected = new SelectTool_1.MarkupSelected(this.svgDecorations);
393
+ }
394
+ /** Called when the Markup is destroyed */
395
+ destroy() { this.markupDiv.remove(); }
396
+ /** Turn on picking the markup elements in the markup view */
397
+ enablePick() { this.markupDiv.style.pointerEvents = "auto"; }
398
+ /** Turn off picking the markup elements in the markup view */
399
+ disablePick() { this.markupDiv.style.pointerEvents = "none"; }
400
+ /** Change the default cursor for the markup view */
401
+ setCursor(cursor) { this.markupDiv.style.cursor = cursor; }
402
+ /** Delete all the entries in the selection set, then empty it. */
403
+ deleteSelected() { this.selected.deleteAll(this.undo); }
404
+ /** Bring all the entries in the selection set to the front. */
405
+ bringToFront() { this.selected.reposition(MarkupApp.getActionName("toFront"), this.undo, (el) => el.front()); }
406
+ /** Send all the entries in the selection set to the back. */
407
+ sendToBack() { this.selected.reposition(MarkupApp.getActionName("toBack"), this.undo, (el) => el.back()); }
408
+ /** Group all the entries in the selection set, then select the group. */
409
+ groupSelected() {
410
+ if (undefined !== this.svgMarkup)
411
+ this.selected.groupAll(this.undo);
412
+ }
413
+ /** Ungroup all the group entries in the selection set. */
414
+ ungroupSelected() {
415
+ if (undefined !== this.svgMarkup)
416
+ this.selected.ungroupAll(this.undo);
417
+ }
418
+ /** Check if the supplied MarkupElement is a group of MarkupText and the MarkupText's outline Rect.
419
+ * @param el the markup element to check
420
+ * @returns true if boxed text
421
+ */
422
+ isBoxedText(el) {
423
+ return el.type === "g" &&
424
+ el.node.classList.length > 0 &&
425
+ el.node.classList[0] === MarkupApp.boxedTextClass &&
426
+ el.children().length === 2;
427
+ }
428
+ /** Get an existing or create a new reusable symbol representing an arrow head.
429
+ * If a Marker for the supplied color and size already exists it is returned, otherwise a new Marker is created.
430
+ * @param color the arrow head color
431
+ * @param length the arrow head length
432
+ * @param width the arrow head width
433
+ * @note Flashing doesn't currently affect markers, need support for "context-stroke" and "context-fill". For now encode color in name...
434
+ */
435
+ createArrowMarker(color, length, width) {
436
+ length = Math.ceil(length); // Don't allow "." in selector string...
437
+ width = Math.ceil(width);
438
+ const arrowMarkerId = `ArrowMarker${length}x${width}-${color}`;
439
+ let marker = (0, svg_js_1.SVG)(`#${arrowMarkerId}`);
440
+ if (null === marker) {
441
+ marker = this.svgMarkup.marker(length, width).id(arrowMarkerId);
442
+ marker.polygon([0, 0, length, width * 0.5, 0, width]);
443
+ marker.attr("orient", "auto-start-reverse");
444
+ marker.attr("overflow", "visible"); // Don't clip the stroke that is being applied to allow the specified start/end to be used directly while hiding the arrow tail fully under the arrow head...
445
+ marker.attr("refX", length);
446
+ marker.css({ stroke: color, fill: color });
447
+ }
448
+ return marker;
449
+ }
450
+ }
451
+ exports.Markup = Markup;
421
452
  //# sourceMappingURL=Markup.js.map