@inweb/markup 25.7.4
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/LICENSE +20 -0
- package/README.md +7 -0
- package/dist/markup.js +13390 -0
- package/dist/markup.js.map +1 -0
- package/dist/markup.min.js +1 -0
- package/dist/markup.module.js +1838 -0
- package/dist/markup.module.js.map +1 -0
- package/lib/index.d.ts +12 -0
- package/lib/markup/IMarkup.d.ts +130 -0
- package/lib/markup/IMarkupArrow.d.ts +52 -0
- package/lib/markup/IMarkupCloud.d.ts +50 -0
- package/lib/markup/IMarkupColorable.d.ts +15 -0
- package/lib/markup/IMarkupEllipse.d.ts +50 -0
- package/lib/markup/IMarkupImage.d.ts +50 -0
- package/lib/markup/IMarkupLine.d.ts +43 -0
- package/lib/markup/IMarkupObject.d.ts +47 -0
- package/lib/markup/IMarkupRectangle.d.ts +50 -0
- package/lib/markup/IMarkupText.d.ts +40 -0
- package/lib/markup/IWorldTransform.d.ts +39 -0
- package/lib/markup/Konva/KonvaArrow.d.ts +46 -0
- package/lib/markup/Konva/KonvaCloud.d.ts +35 -0
- package/lib/markup/Konva/KonvaEllipse.d.ts +40 -0
- package/lib/markup/Konva/KonvaImage.d.ts +36 -0
- package/lib/markup/Konva/KonvaLine.d.ts +35 -0
- package/lib/markup/Konva/KonvaMarkup.d.ts +82 -0
- package/lib/markup/Konva/KonvaRectangle.d.ts +38 -0
- package/lib/markup/Konva/KonvaText.d.ts +37 -0
- package/lib/markup/Konva/MarkupColor.d.ts +38 -0
- package/package.json +40 -0
- package/src/index.ts +35 -0
- package/src/markup/IMarkup.ts +173 -0
- package/src/markup/IMarkupArrow.ts +69 -0
- package/src/markup/IMarkupCloud.ts +78 -0
- package/src/markup/IMarkupColorable.ts +39 -0
- package/src/markup/IMarkupEllipse.ts +78 -0
- package/src/markup/IMarkupImage.ts +78 -0
- package/src/markup/IMarkupLine.ts +70 -0
- package/src/markup/IMarkupObject.ts +78 -0
- package/src/markup/IMarkupRectangle.ts +78 -0
- package/src/markup/IMarkupText.ts +66 -0
- package/src/markup/IWorldTransform.ts +46 -0
- package/src/markup/Konva/KonvaArrow.ts +147 -0
- package/src/markup/Konva/KonvaCloud.ts +213 -0
- package/src/markup/Konva/KonvaEllipse.ts +150 -0
- package/src/markup/Konva/KonvaImage.ts +149 -0
- package/src/markup/Konva/KonvaLine.ts +136 -0
- package/src/markup/Konva/KonvaMarkup.ts +1264 -0
- package/src/markup/Konva/KonvaRectangle.ts +149 -0
- package/src/markup/Konva/KonvaText.ts +141 -0
- package/src/markup/Konva/MarkupColor.ts +82 -0
|
@@ -0,0 +1,1264 @@
|
|
|
1
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
2
|
+
// Copyright (C) 2002-2024, Open Design Alliance (the "Alliance").
|
|
3
|
+
// All rights reserved.
|
|
4
|
+
//
|
|
5
|
+
// This software and its documentation and related materials are owned by
|
|
6
|
+
// the Alliance. The software may only be incorporated into application
|
|
7
|
+
// programs owned by members of the Alliance, subject to a signed
|
|
8
|
+
// Membership Agreement and Supplemental Software License Agreement with the
|
|
9
|
+
// Alliance. The structure and organization of this software are the valuable
|
|
10
|
+
// trade secrets of the Alliance and its suppliers. The software is also
|
|
11
|
+
// protected by copyright law and international treaty provisions. Application
|
|
12
|
+
// programs incorporating this software must include the following statement
|
|
13
|
+
// with their copyright notices:
|
|
14
|
+
//
|
|
15
|
+
// This application incorporates Open Design Alliance software pursuant to a
|
|
16
|
+
// license agreement with Open Design Alliance.
|
|
17
|
+
// Open Design Alliance Copyright (C) 2002-2024 by Open Design Alliance.
|
|
18
|
+
// All rights reserved.
|
|
19
|
+
//
|
|
20
|
+
// By use of this software, its documentation or related materials, you
|
|
21
|
+
// acknowledge and accept the above terms.
|
|
22
|
+
///////////////////////////////////////////////////////////////////////////////
|
|
23
|
+
|
|
24
|
+
import Konva from "konva";
|
|
25
|
+
import { IEventEmitter } from "@inweb/eventemitter2";
|
|
26
|
+
import { ChangeActiveDraggerEvent, IViewpoint, PanEvent } from "@inweb/viewer-core";
|
|
27
|
+
|
|
28
|
+
import { IMarkup, MarkupMode } from "../IMarkup";
|
|
29
|
+
import { IWorldTransform } from "../IWorldTransform";
|
|
30
|
+
import { MarkupColor } from "./MarkupColor";
|
|
31
|
+
import { IMarkupObject } from "../IMarkupObject";
|
|
32
|
+
import { IMarkupColorable } from "../IMarkupColorable";
|
|
33
|
+
import { KonvaLine } from "./KonvaLine";
|
|
34
|
+
import { KonvaText } from "./KonvaText";
|
|
35
|
+
import { KonvaRectangle } from "./KonvaRectangle";
|
|
36
|
+
import { KonvaEllipse } from "./KonvaEllipse";
|
|
37
|
+
import { KonvaArrow } from "./KonvaArrow";
|
|
38
|
+
import { KonvaImage } from "./KonvaImage";
|
|
39
|
+
import { KonvaCloud } from "./KonvaCloud";
|
|
40
|
+
import { IMarkupLine, MarkupLineType } from "../IMarkupLine";
|
|
41
|
+
import { IMarkupArrow } from "../IMarkupArrow";
|
|
42
|
+
import { IMarkupEllipse } from "../IMarkupEllipse";
|
|
43
|
+
import { IMarkupRectangle } from "../IMarkupRectangle";
|
|
44
|
+
import { IMarkupCloud } from "../IMarkupCloud";
|
|
45
|
+
import { IMarkupImage } from "../IMarkupImage";
|
|
46
|
+
import { IMarkupText } from "../IMarkupText";
|
|
47
|
+
|
|
48
|
+
class KonvaShape {
|
|
49
|
+
name: string;
|
|
50
|
+
initializer: (ref: any) => any;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// move to separate class and create factory with enum?
|
|
54
|
+
const MarkupMode2Konva = new Map<MarkupMode, KonvaShape>([
|
|
55
|
+
[
|
|
56
|
+
MarkupMode.SelectMarkup,
|
|
57
|
+
{
|
|
58
|
+
name: "SelectMarkup",
|
|
59
|
+
initializer: () => {},
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
MarkupMode.Line,
|
|
64
|
+
{
|
|
65
|
+
name: "Line",
|
|
66
|
+
initializer: (ref) => {
|
|
67
|
+
return new KonvaLine(null, ref);
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
[
|
|
72
|
+
MarkupMode.Text,
|
|
73
|
+
{
|
|
74
|
+
name: "Text",
|
|
75
|
+
initializer: (ref) => {
|
|
76
|
+
return new KonvaText(null, ref);
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
[
|
|
81
|
+
MarkupMode.Rectangle,
|
|
82
|
+
{
|
|
83
|
+
name: "Rect",
|
|
84
|
+
initializer: (ref) => {
|
|
85
|
+
return new KonvaRectangle(null, ref);
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
[
|
|
90
|
+
MarkupMode.Ellipse,
|
|
91
|
+
{
|
|
92
|
+
name: "Ellipse",
|
|
93
|
+
initializer: (ref) => {
|
|
94
|
+
return new KonvaEllipse(null, ref);
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
[
|
|
99
|
+
MarkupMode.Arrow,
|
|
100
|
+
{
|
|
101
|
+
name: "Arrow",
|
|
102
|
+
initializer: (ref) => {
|
|
103
|
+
return new KonvaArrow(null, ref);
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
[
|
|
108
|
+
MarkupMode.Image,
|
|
109
|
+
{
|
|
110
|
+
name: "Image",
|
|
111
|
+
initializer: (ref) => {
|
|
112
|
+
return new KonvaImage(null, ref);
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
[
|
|
117
|
+
MarkupMode.Cloud,
|
|
118
|
+
{
|
|
119
|
+
name: "Cloud",
|
|
120
|
+
initializer: (ref) => {
|
|
121
|
+
return new KonvaCloud(null, ref);
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* 2D markup core.
|
|
129
|
+
*/
|
|
130
|
+
export class KonvaMarkup implements IMarkup {
|
|
131
|
+
private _isInitialized = false;
|
|
132
|
+
private _viewer: IEventEmitter;
|
|
133
|
+
private _worldTransformer: IWorldTransform;
|
|
134
|
+
private _canvasOriginal: HTMLCanvasElement;
|
|
135
|
+
private _canvasEvents: string[];
|
|
136
|
+
private _markupIsActive = false;
|
|
137
|
+
private _markupMode: MarkupMode;
|
|
138
|
+
private _markupColor = new MarkupColor(255, 0, 0);
|
|
139
|
+
private _konvaStage: Konva.Stage;
|
|
140
|
+
private _konvaLayer: Konva.Layer;
|
|
141
|
+
private _konvaTransformer: Konva.Transformer;
|
|
142
|
+
|
|
143
|
+
private _textInputRef: HTMLTextAreaElement;
|
|
144
|
+
private _textInputPos: Konva.Vector2d;
|
|
145
|
+
private _textInputAngle: number;
|
|
146
|
+
private _imageInputRef: HTMLInputElement;
|
|
147
|
+
private _imageInputPos: Konva.Vector2d;
|
|
148
|
+
private _markupContainer: HTMLDivElement;
|
|
149
|
+
private _zIndex = 1;
|
|
150
|
+
|
|
151
|
+
private readonly _markupContainerName = "markupContainer";
|
|
152
|
+
|
|
153
|
+
public lineWidth = 4;
|
|
154
|
+
public lineType: MarkupLineType = "solid";
|
|
155
|
+
public fontSize = 34;
|
|
156
|
+
|
|
157
|
+
initialize(
|
|
158
|
+
canvas: HTMLCanvasElement,
|
|
159
|
+
canvasEvents: string[],
|
|
160
|
+
viewer?: IEventEmitter,
|
|
161
|
+
worldTransformer?: IWorldTransform
|
|
162
|
+
): void {
|
|
163
|
+
if (!Konva)
|
|
164
|
+
throw new Error(
|
|
165
|
+
'Markup: Error during initialization. Konva is not initialized. Update node_modules or add to your page <script src="https://unpkg.com/konva@9/konva.min.js"></script>'
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
this._viewer = viewer;
|
|
169
|
+
this._worldTransformer = worldTransformer;
|
|
170
|
+
this._canvasOriginal = canvas;
|
|
171
|
+
this._canvasEvents = canvasEvents ?? [];
|
|
172
|
+
|
|
173
|
+
this._markupContainer = document.createElement("div");
|
|
174
|
+
this._markupContainer.id = this._markupContainerName;
|
|
175
|
+
this._markupContainer.style.position = "absolute";
|
|
176
|
+
this._markupContainer.style.top = "0px";
|
|
177
|
+
this._markupContainer.style.left = "0px";
|
|
178
|
+
// to eliminate grey box during delete elements
|
|
179
|
+
this._markupContainer.style.outline = "0px";
|
|
180
|
+
|
|
181
|
+
const parentDiv = this._canvasOriginal.parentElement;
|
|
182
|
+
parentDiv.appendChild(this._markupContainer);
|
|
183
|
+
|
|
184
|
+
this._markupColor.setColor(255, 0, 0);
|
|
185
|
+
|
|
186
|
+
this.initializeKonva();
|
|
187
|
+
this.resize();
|
|
188
|
+
|
|
189
|
+
this._canvasEvents.forEach((x) => this._markupContainer.addEventListener(x, this.redirectToViewer));
|
|
190
|
+
|
|
191
|
+
if (this._viewer) {
|
|
192
|
+
this._viewer.addEventListener("resize", this.resize);
|
|
193
|
+
this._viewer.addEventListener("changeactivedragger", this.changeActiveDragger);
|
|
194
|
+
this._viewer.addEventListener("pan", this.pan);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this._isInitialized = true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
dispose(): void {
|
|
201
|
+
if (!this._isInitialized) return;
|
|
202
|
+
|
|
203
|
+
if (this._viewer) {
|
|
204
|
+
this._viewer.removeEventListener("pan", this.pan);
|
|
205
|
+
this._viewer.removeEventListener("changeactivedragger", this.changeActiveDragger);
|
|
206
|
+
this._viewer.removeEventListener("resize", this.resize);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this._canvasEvents.forEach((x) => this._markupContainer.removeEventListener(x, this.redirectToViewer));
|
|
210
|
+
|
|
211
|
+
this.destroyKonva();
|
|
212
|
+
|
|
213
|
+
this._markupContainer.remove();
|
|
214
|
+
this._markupContainer = undefined;
|
|
215
|
+
|
|
216
|
+
this._canvasOriginal = undefined;
|
|
217
|
+
this._viewer = undefined;
|
|
218
|
+
this._worldTransformer = undefined;
|
|
219
|
+
|
|
220
|
+
this._markupIsActive = false;
|
|
221
|
+
this._isInitialized = false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
changeActiveDragger = (event: ChangeActiveDraggerEvent) => {
|
|
225
|
+
const draggerName = event.data;
|
|
226
|
+
|
|
227
|
+
this._markupContainer.className = this._canvasOriginal.className
|
|
228
|
+
.split(" ")
|
|
229
|
+
.filter((x) => !x.startsWith("oda-cursor-"))
|
|
230
|
+
.filter((x) => x)
|
|
231
|
+
.concat(`oda-cursor-${draggerName.toLowerCase()}`)
|
|
232
|
+
.join(" ");
|
|
233
|
+
|
|
234
|
+
this.removeTextInput();
|
|
235
|
+
this.removeImageInput();
|
|
236
|
+
|
|
237
|
+
const markupMode = MarkupMode[draggerName];
|
|
238
|
+
const konvaMode = MarkupMode2Konva.get(markupMode);
|
|
239
|
+
if (konvaMode) {
|
|
240
|
+
this._markupMode = markupMode;
|
|
241
|
+
this._markupIsActive = true;
|
|
242
|
+
} else {
|
|
243
|
+
this._markupIsActive = false;
|
|
244
|
+
this._konvaTransformer.nodes([]);
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
resize = () => {
|
|
249
|
+
this._konvaStage?.width(this._canvasOriginal.clientWidth);
|
|
250
|
+
this._konvaStage?.height(this._canvasOriginal.clientHeight);
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
pan = (event: PanEvent) => {
|
|
254
|
+
const dX = event.dX / window.devicePixelRatio;
|
|
255
|
+
const dY = event.dY / window.devicePixelRatio;
|
|
256
|
+
|
|
257
|
+
Object.values(MarkupMode).forEach((mode) => this.konvaLayerFind(mode).forEach((x) => x.move({ x: dX, y: dY })));
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
redirectToViewer = (event: any) => {
|
|
261
|
+
if (this._viewer) this._viewer.emit(event);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
syncOverlay(): void {}
|
|
265
|
+
|
|
266
|
+
clearOverlay(): void {
|
|
267
|
+
this.removeTextInput();
|
|
268
|
+
this.removeImageInput();
|
|
269
|
+
this._konvaTransformer.nodes([]);
|
|
270
|
+
Object.values(MarkupMode).forEach((mode) => this.konvaLayerFind(mode).forEach((x) => x.destroy()));
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
getMarkupColor(): { r: number; g: number; b: number } {
|
|
274
|
+
return this._markupColor.RGB;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
setMarkupColor(r: number, g: number, b: number): void {
|
|
278
|
+
this._markupColor.setColor(r, g, b);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
colorizeAllMarkup(r = 255, g = 0, b = 0): void {
|
|
282
|
+
const hex = new MarkupColor(r, g, b).HexColor;
|
|
283
|
+
Object.values(MarkupMode).forEach((mode) => {
|
|
284
|
+
this.konvaLayerFind(mode).forEach((x) => {
|
|
285
|
+
const konvaObj = MarkupMode2Konva.get(mode)?.initializer(x);
|
|
286
|
+
if (konvaObj && konvaObj.setColor) konvaObj.setColor(hex);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
this._konvaLayer.draw();
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
colorizeSelectedMarkups(r: number, g: number, b: number): void {
|
|
294
|
+
this.getSelectedObjects().forEach((obj) => {
|
|
295
|
+
const colorable = obj as unknown as IMarkupColorable;
|
|
296
|
+
if (colorable && colorable.setColor) {
|
|
297
|
+
colorable.setColor(new MarkupColor(r, g, b).HexColor);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
setViewpoint(viewpoint: IViewpoint): void {
|
|
303
|
+
const markupColor = viewpoint.custom_fields.markup_color || { r: 255, g: 0, b: 0 };
|
|
304
|
+
this.setMarkupColor(markupColor.r, markupColor.g, markupColor.b);
|
|
305
|
+
|
|
306
|
+
this.loadMarkup(viewpoint);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
getViewpoint(): IViewpoint {
|
|
310
|
+
const viewpoint: IViewpoint = {
|
|
311
|
+
lines: [],
|
|
312
|
+
texts: [],
|
|
313
|
+
arrows: [],
|
|
314
|
+
clouds: [],
|
|
315
|
+
ellipses: [],
|
|
316
|
+
images: [],
|
|
317
|
+
rectangles: [],
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
viewpoint.snapshot = {
|
|
321
|
+
data: this.combineMarkupWithDrawing(),
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
viewpoint.custom_fields = {
|
|
325
|
+
markup_color: this.getMarkupColor(),
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
this.fillViewpointShapes(viewpoint);
|
|
329
|
+
|
|
330
|
+
viewpoint.description = new Date().toDateString();
|
|
331
|
+
return viewpoint;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
createObject(type: string, params: any): IMarkupObject {
|
|
335
|
+
let object = null;
|
|
336
|
+
let zIndex = this._zIndex;
|
|
337
|
+
|
|
338
|
+
// TODO: factory?
|
|
339
|
+
switch (type.toLocaleLowerCase()) {
|
|
340
|
+
case "line":
|
|
341
|
+
object = new KonvaLine(params);
|
|
342
|
+
zIndex = 1;
|
|
343
|
+
break;
|
|
344
|
+
case "text":
|
|
345
|
+
object = new KonvaText(params);
|
|
346
|
+
break;
|
|
347
|
+
case "rectangle":
|
|
348
|
+
object = new KonvaRectangle(params);
|
|
349
|
+
zIndex = 1;
|
|
350
|
+
break;
|
|
351
|
+
case "ellipse":
|
|
352
|
+
object = new KonvaEllipse(params);
|
|
353
|
+
zIndex = 1;
|
|
354
|
+
break;
|
|
355
|
+
case "arrow":
|
|
356
|
+
object = new KonvaArrow(params);
|
|
357
|
+
break;
|
|
358
|
+
case "image":
|
|
359
|
+
object = new KonvaImage(params);
|
|
360
|
+
zIndex = 0;
|
|
361
|
+
break;
|
|
362
|
+
case "cloud":
|
|
363
|
+
object = new KonvaCloud(params);
|
|
364
|
+
zIndex = 1;
|
|
365
|
+
break;
|
|
366
|
+
default:
|
|
367
|
+
throw new Error("Markup CreateObject - unsupported type has been detected.");
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
this.addObject(object);
|
|
371
|
+
|
|
372
|
+
// Set zIndex only when shape has been added to Layer else we will get "Konva warning: Node has no parent. zIndex parameter is ignored."
|
|
373
|
+
object.setZIndex(zIndex);
|
|
374
|
+
this._zIndex++;
|
|
375
|
+
return object;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
getObjects(): IMarkupObject[] {
|
|
379
|
+
const objects = [];
|
|
380
|
+
this.konvaLayerFind(MarkupMode.Line).forEach((line) => {
|
|
381
|
+
objects.push(new KonvaLine(null, line));
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
this.konvaLayerFind(MarkupMode.Text).forEach((text) => {
|
|
385
|
+
objects.push(new KonvaText(null, text));
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
this.konvaLayerFind(MarkupMode.Rectangle).forEach((rectangle) => {
|
|
389
|
+
objects.push(new KonvaRectangle(null, rectangle));
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
this.konvaLayerFind(MarkupMode.Ellipse).forEach((ellipse) => {
|
|
393
|
+
objects.push(new KonvaEllipse(null, ellipse));
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
this.konvaLayerFind(MarkupMode.Arrow).forEach((arrow) => {
|
|
397
|
+
objects.push(new KonvaArrow(null, arrow));
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
this.konvaLayerFind(MarkupMode.Image).forEach((image) => {
|
|
401
|
+
objects.push(new KonvaImage(null, image));
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
this.konvaLayerFind(MarkupMode.Cloud).forEach((cloud) => {
|
|
405
|
+
objects.push(new KonvaCloud(null, cloud));
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
return objects;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
getSelectedObjects(): IMarkupObject[] {
|
|
412
|
+
const objects = [];
|
|
413
|
+
|
|
414
|
+
this._konvaTransformer.nodes().forEach((obj) => {
|
|
415
|
+
const konvaShapeName = obj.className;
|
|
416
|
+
switch (konvaShapeName) {
|
|
417
|
+
case "Line":
|
|
418
|
+
objects.push(new KonvaLine(null, obj));
|
|
419
|
+
break;
|
|
420
|
+
case "Text":
|
|
421
|
+
objects.push(new KonvaText(null, obj));
|
|
422
|
+
break;
|
|
423
|
+
case "Rect":
|
|
424
|
+
objects.push(new KonvaRectangle(null, obj));
|
|
425
|
+
break;
|
|
426
|
+
case "Ellipse":
|
|
427
|
+
objects.push(new KonvaEllipse(null, obj));
|
|
428
|
+
break;
|
|
429
|
+
case "Arrow":
|
|
430
|
+
objects.push(new KonvaArrow(null, obj));
|
|
431
|
+
break;
|
|
432
|
+
case "Image":
|
|
433
|
+
objects.push(new KonvaImage(null, obj));
|
|
434
|
+
break;
|
|
435
|
+
case "Cloud":
|
|
436
|
+
objects.push(new KonvaCloud(null, obj));
|
|
437
|
+
break;
|
|
438
|
+
default:
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
return objects;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
selectObjects(objects: IMarkupObject[]) {
|
|
447
|
+
const selectedObjs = this._konvaTransformer.nodes().concat(objects.map((x) => x.ref()));
|
|
448
|
+
this._konvaTransformer.nodes(selectedObjs);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
clearSelected(): void {
|
|
452
|
+
this._konvaTransformer.nodes([]);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
private getPoint3dFromArray(array) {
|
|
456
|
+
return { x: array[0], y: array[1], z: array[2] };
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private fillViewpointShapes(viewpoint) {
|
|
460
|
+
const markupLines = this.getMarkupLines();
|
|
461
|
+
if (markupLines && markupLines.length > 0) {
|
|
462
|
+
markupLines?.forEach((line) => {
|
|
463
|
+
viewpoint.lines.push(line);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const markupTexts = this.getMarkupTexts();
|
|
468
|
+
if (markupTexts && markupTexts.length > 0) {
|
|
469
|
+
markupTexts?.forEach((text) => {
|
|
470
|
+
viewpoint.texts.push(text);
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const markupRectangles = this.getMarkupRectangles();
|
|
475
|
+
if (markupRectangles && markupRectangles.length > 0) {
|
|
476
|
+
markupRectangles?.forEach((rectangle) => {
|
|
477
|
+
viewpoint.rectangles.push(rectangle);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const markupEllipses = this.getMarkupEllipses();
|
|
482
|
+
if (markupEllipses && markupEllipses.length > 0) {
|
|
483
|
+
markupEllipses?.forEach((ellipse) => {
|
|
484
|
+
viewpoint.ellipses.push(ellipse);
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const markupArrows = this.getMarkupArrows();
|
|
489
|
+
if (markupArrows && markupArrows.length > 0) {
|
|
490
|
+
markupArrows?.forEach((arrow) => {
|
|
491
|
+
viewpoint.arrows.push(arrow);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
const markupImages = this.getMarkupImages();
|
|
496
|
+
if (markupImages && markupImages.length > 0) {
|
|
497
|
+
markupImages?.forEach((image) => {
|
|
498
|
+
viewpoint.images.push(image);
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const markupClouds = this.getMarkupClouds();
|
|
503
|
+
if (markupClouds && markupClouds.length > 0) {
|
|
504
|
+
markupClouds?.forEach((cloud) => {
|
|
505
|
+
viewpoint.clouds.push(cloud);
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
private addObject(object: IMarkupObject): void {
|
|
511
|
+
this._konvaLayer.add(object.ref());
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
private konvaLayerFind(markupShape: MarkupMode): any {
|
|
515
|
+
const konvaShape = MarkupMode2Konva.get(markupShape);
|
|
516
|
+
if (konvaShape) {
|
|
517
|
+
// for "draggable" Konva uses Rectangles in Transformer. We need only Shapes from Layer.
|
|
518
|
+
const konvaShapes = this._konvaLayer.find(konvaShape.name).filter((x) => x.parent instanceof Konva.Layer);
|
|
519
|
+
return konvaShapes;
|
|
520
|
+
}
|
|
521
|
+
return [];
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
private initializeKonva(): any {
|
|
525
|
+
// first we need Konva core things: stage and layer
|
|
526
|
+
this._konvaStage = new Konva.Stage({
|
|
527
|
+
container: this._markupContainerName,
|
|
528
|
+
width: this._canvasOriginal.clientWidth,
|
|
529
|
+
height: this._canvasOriginal.clientHeight,
|
|
530
|
+
});
|
|
531
|
+
const stage = this._konvaStage;
|
|
532
|
+
const layer = new Konva.Layer({ pixelRation: window.devicePixelRatio });
|
|
533
|
+
stage.add(layer);
|
|
534
|
+
this._konvaLayer = layer;
|
|
535
|
+
|
|
536
|
+
const transformer = new Konva.Transformer({
|
|
537
|
+
shouldOverdrawWholeArea: false,
|
|
538
|
+
keepRatio: false,
|
|
539
|
+
flipEnabled: false,
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
this._konvaTransformer = transformer;
|
|
543
|
+
layer.add(transformer);
|
|
544
|
+
|
|
545
|
+
let isPaint = false;
|
|
546
|
+
let lastLine;
|
|
547
|
+
let mouseDownPos;
|
|
548
|
+
let lastObj;
|
|
549
|
+
|
|
550
|
+
stage.on("mousedown touchstart", (e) => {
|
|
551
|
+
// do nothing if we mousedown on any shape
|
|
552
|
+
if (
|
|
553
|
+
!this._markupIsActive ||
|
|
554
|
+
e.target !== stage ||
|
|
555
|
+
this._markupMode === MarkupMode.Text ||
|
|
556
|
+
this._markupMode === MarkupMode.Image
|
|
557
|
+
)
|
|
558
|
+
return;
|
|
559
|
+
|
|
560
|
+
if (e.target === stage && transformer.nodes().length > 0) {
|
|
561
|
+
transformer.nodes([]);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const pos = stage.getPointerPosition();
|
|
566
|
+
mouseDownPos = pos;
|
|
567
|
+
|
|
568
|
+
isPaint = [MarkupMode.Arrow, MarkupMode.Cloud, MarkupMode.Ellipse, MarkupMode.Line, MarkupMode.Rectangle].some(
|
|
569
|
+
(m) => m === this._markupMode
|
|
570
|
+
);
|
|
571
|
+
if (this._markupMode === MarkupMode.Line) {
|
|
572
|
+
// add point twice, so we have some drawings even on a simple click
|
|
573
|
+
lastLine = this.addLine([pos.x, pos.y, pos.x, pos.y]);
|
|
574
|
+
}
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
stage.on("mouseup touchend", (e) => {
|
|
578
|
+
if (!this._markupIsActive) return;
|
|
579
|
+
|
|
580
|
+
if (isPaint) {
|
|
581
|
+
const pos = stage.getPointerPosition();
|
|
582
|
+
const defParams = mouseDownPos && pos.x === mouseDownPos.x && pos.y === mouseDownPos.y;
|
|
583
|
+
const startX = defParams ? mouseDownPos.x : Math.min(mouseDownPos.x, pos.x);
|
|
584
|
+
const startY = defParams ? mouseDownPos.y : Math.min(mouseDownPos.y, pos.y);
|
|
585
|
+
const dX = defParams ? 200 : Math.abs(mouseDownPos.x - pos.x);
|
|
586
|
+
const dY = defParams ? 200 : Math.abs(mouseDownPos.y - pos.y);
|
|
587
|
+
if (defParams) {
|
|
588
|
+
if (this._markupMode === MarkupMode.Rectangle) {
|
|
589
|
+
this.addRectangle({ x: startX, y: startY }, dX, dY);
|
|
590
|
+
} else if (this._markupMode === MarkupMode.Ellipse) {
|
|
591
|
+
this.addEllipse({ x: startX, y: startY }, { x: dX / 2, y: dY / 2 });
|
|
592
|
+
} else if (this._markupMode === MarkupMode.Arrow) {
|
|
593
|
+
this.addArrow(
|
|
594
|
+
{ x: mouseDownPos.x, y: mouseDownPos.y },
|
|
595
|
+
{ x: defParams ? mouseDownPos.x + 200 : pos.x, y: defParams ? startY : pos.y }
|
|
596
|
+
);
|
|
597
|
+
} else if (this._markupMode === MarkupMode.Cloud) {
|
|
598
|
+
this.addCloud({ x: startX, y: startY }, Math.max(100, dX), Math.max(100, dY));
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
lastObj = undefined;
|
|
604
|
+
isPaint = false;
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
stage.on("mousemove touchmove", (e) => {
|
|
608
|
+
if (!this._markupIsActive) return;
|
|
609
|
+
if (!isPaint) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// prevent scrolling on touch devices
|
|
614
|
+
//e.evt.preventDefault();
|
|
615
|
+
|
|
616
|
+
const pos = stage.getPointerPosition();
|
|
617
|
+
const defParams = mouseDownPos && pos.x === mouseDownPos.x && pos.y === mouseDownPos.y;
|
|
618
|
+
const startX = defParams ? mouseDownPos.x : Math.min(mouseDownPos.x, pos.x);
|
|
619
|
+
const startY = defParams ? mouseDownPos.y : Math.min(mouseDownPos.y, pos.y);
|
|
620
|
+
const dX = defParams ? 200 : Math.abs(mouseDownPos.x - pos.x);
|
|
621
|
+
const dY = defParams ? 200 : Math.abs(mouseDownPos.y - pos.y);
|
|
622
|
+
|
|
623
|
+
if (this._markupMode === MarkupMode.Line) {
|
|
624
|
+
lastLine.addPoints([{ x: pos.x, y: pos.y }]);
|
|
625
|
+
} else if (this._markupMode === MarkupMode.Arrow) {
|
|
626
|
+
if (lastObj) lastObj.setEndPoint(pos.x, pos.y);
|
|
627
|
+
else lastObj = this.addArrow({ x: mouseDownPos.x, y: mouseDownPos.y }, { x: pos.x, y: pos.y });
|
|
628
|
+
} else if (this._markupMode === MarkupMode.Rectangle) {
|
|
629
|
+
if (lastObj) {
|
|
630
|
+
lastObj.setPosition(startX, startY);
|
|
631
|
+
lastObj.setWidth(dX);
|
|
632
|
+
lastObj.setHeight(dY);
|
|
633
|
+
} else lastObj = this.addRectangle({ x: startX, y: startY }, dX, dY);
|
|
634
|
+
} else if (this._markupMode === MarkupMode.Ellipse) {
|
|
635
|
+
if (lastObj) {
|
|
636
|
+
lastObj.setPosition(startX, startY);
|
|
637
|
+
lastObj.setRadiusX(dX);
|
|
638
|
+
lastObj.setRadiusY(dY);
|
|
639
|
+
} else lastObj = this.addEllipse({ x: startX, y: startY }, { x: dX, y: dY });
|
|
640
|
+
} else if (this._markupMode === MarkupMode.Cloud) {
|
|
641
|
+
if (lastObj) {
|
|
642
|
+
lastObj.setPosition(startX, startY);
|
|
643
|
+
lastObj.setWidth(Math.max(100, dX));
|
|
644
|
+
lastObj.setHeight(Math.max(100, dY));
|
|
645
|
+
} else lastObj = this.addCloud({ x: startX, y: startY }, dX, dY);
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// clicks should select/deselect shapes
|
|
650
|
+
stage.on("click tap", (e) => {
|
|
651
|
+
if (!this._markupIsActive) return;
|
|
652
|
+
|
|
653
|
+
// if click on empty area - remove all selections
|
|
654
|
+
if (e.target === stage) {
|
|
655
|
+
if (this._markupMode === MarkupMode.Text) {
|
|
656
|
+
if (this._textInputRef && this._textInputRef.value)
|
|
657
|
+
this.addText(this._textInputRef.value, this._textInputPos, this._textInputAngle);
|
|
658
|
+
else if (transformer.nodes().length === 0) {
|
|
659
|
+
const pos = stage.getPointerPosition();
|
|
660
|
+
this.createTextInput(pos, e.evt.pageX, e.evt.pageY, 0, null);
|
|
661
|
+
}
|
|
662
|
+
} else if (this._markupMode === MarkupMode.Image) {
|
|
663
|
+
if (this._imageInputRef && this._imageInputRef.value)
|
|
664
|
+
this.addImage(
|
|
665
|
+
{ x: this._imageInputPos.x, y: this._imageInputPos.y },
|
|
666
|
+
this._imageInputRef.value,
|
|
667
|
+
0,
|
|
668
|
+
0,
|
|
669
|
+
this._imageInputRef.value
|
|
670
|
+
);
|
|
671
|
+
else if (transformer.nodes().length === 0) {
|
|
672
|
+
const pos = stage.getPointerPosition();
|
|
673
|
+
this.createImageInput(pos);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
transformer.nodes([]);
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (this._markupMode === MarkupMode.Text || this._markupMode === MarkupMode.SelectMarkup) {
|
|
681
|
+
if (e.target.className === "Text" && transformer.nodes().length === 1 && transformer.nodes()[0] === e.target) {
|
|
682
|
+
if (this._textInputRef && this._textInputRef.value)
|
|
683
|
+
this.addText(this._textInputRef.value, this._textInputPos, this._textInputAngle);
|
|
684
|
+
else
|
|
685
|
+
this.createTextInput(
|
|
686
|
+
{ x: e.target.attrs.x, y: e.target.attrs.y },
|
|
687
|
+
e.evt.pageX,
|
|
688
|
+
e.evt.pageY,
|
|
689
|
+
e.target.attrs.rotation,
|
|
690
|
+
e.target.attrs.text
|
|
691
|
+
);
|
|
692
|
+
return;
|
|
693
|
+
} else {
|
|
694
|
+
this.removeTextInput();
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (this._markupMode === MarkupMode.Image || this._markupMode === MarkupMode.SelectMarkup) {
|
|
699
|
+
if (e.target.className === "Image" && transformer.nodes().length === 1 && transformer.nodes()[0] === e.target) {
|
|
700
|
+
if (this._imageInputRef && this._imageInputRef.value)
|
|
701
|
+
this.addImage(this._imageInputPos, this._imageInputRef.value, 0, 0);
|
|
702
|
+
else this.createImageInput({ x: e.target.attrs.x, y: e.target.attrs.y });
|
|
703
|
+
return;
|
|
704
|
+
} else {
|
|
705
|
+
this.removeImageInput();
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
if (transformer.nodes().filter((x) => x.className === "Cloud").length > 0 || e.target.className === "Cloud") {
|
|
710
|
+
transformer.rotateEnabled(false);
|
|
711
|
+
} else {
|
|
712
|
+
transformer.rotateEnabled(true);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// do we pressed shift or ctrl?
|
|
716
|
+
const metaPressed = e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey;
|
|
717
|
+
const isSelected = transformer.nodes().indexOf(e.target) >= 0;
|
|
718
|
+
|
|
719
|
+
if (!metaPressed && !isSelected) {
|
|
720
|
+
// if no key pressed and the node is not selected
|
|
721
|
+
// select just one
|
|
722
|
+
transformer.nodes([e.target]);
|
|
723
|
+
} else if (metaPressed && isSelected) {
|
|
724
|
+
// if we pressed keys and node was selected
|
|
725
|
+
// we need to remove it from selection:
|
|
726
|
+
const nodes = transformer.nodes().slice(); // use slice to have new copy of array
|
|
727
|
+
// remove node from array
|
|
728
|
+
nodes.splice(nodes.indexOf(e.target), 1);
|
|
729
|
+
transformer.nodes(nodes);
|
|
730
|
+
} else if (metaPressed && !isSelected) {
|
|
731
|
+
// add the node into selection
|
|
732
|
+
const nodes = transformer.nodes().concat([e.target]);
|
|
733
|
+
transformer.nodes(nodes);
|
|
734
|
+
}
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
const container = stage.container();
|
|
738
|
+
container.tabIndex = 1;
|
|
739
|
+
// focus it
|
|
740
|
+
// also stage will be in focus on its click
|
|
741
|
+
container.focus();
|
|
742
|
+
|
|
743
|
+
container.addEventListener("keydown", (e) => {
|
|
744
|
+
if (!this._markupIsActive) return;
|
|
745
|
+
if (e.code === "Delete") {
|
|
746
|
+
const trNodes = this._konvaTransformer.nodes();
|
|
747
|
+
if (trNodes.length > 0) {
|
|
748
|
+
this._konvaTransformer.nodes().forEach((x) => x.destroy());
|
|
749
|
+
this._konvaTransformer.nodes([]);
|
|
750
|
+
}
|
|
751
|
+
layer.draw();
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
e.preventDefault();
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
private destroyKonva() {
|
|
759
|
+
this.clearOverlay();
|
|
760
|
+
|
|
761
|
+
this._konvaStage.destroy();
|
|
762
|
+
|
|
763
|
+
this._konvaLayer = undefined;
|
|
764
|
+
this._konvaTransformer = undefined;
|
|
765
|
+
this._konvaStage = undefined;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
private getMarkupLines() {
|
|
769
|
+
const lines = [];
|
|
770
|
+
this.konvaLayerFind(MarkupMode.Line).forEach((line) => {
|
|
771
|
+
const linePoints = line.points();
|
|
772
|
+
if (!linePoints) return;
|
|
773
|
+
const worldPoints = [];
|
|
774
|
+
const absoluteTransform = line.getAbsoluteTransform();
|
|
775
|
+
for (let i = 0; i < linePoints.length; i += 2) {
|
|
776
|
+
// we need getAbsoluteTransform because inside Konva position starts from {0, 0}
|
|
777
|
+
// https://stackoverflow.com/a/57641487 - check answer's comments
|
|
778
|
+
const atPoint = absoluteTransform.point({ x: linePoints[i], y: linePoints[i + 1] });
|
|
779
|
+
const worldPoint = this._worldTransformer.screenToWorld(atPoint);
|
|
780
|
+
|
|
781
|
+
worldPoints.push(worldPoint);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const konvaLine = new KonvaLine(null, line);
|
|
785
|
+
lines.push({
|
|
786
|
+
id: konvaLine.id(),
|
|
787
|
+
points: worldPoints.map((p) => this.getPoint3dFromArray(p)),
|
|
788
|
+
color: konvaLine.getColor() || "#ff0000",
|
|
789
|
+
type: konvaLine.getLineType() || this.lineType,
|
|
790
|
+
width: konvaLine.getLineWidth() || this.lineWidth,
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
return lines;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
private getMarkupTexts() {
|
|
798
|
+
const texts = [];
|
|
799
|
+
|
|
800
|
+
const textSize = 0.02;
|
|
801
|
+
const textScale = this._worldTransformer.getScale();
|
|
802
|
+
|
|
803
|
+
this.konvaLayerFind(MarkupMode.Text).forEach((text) => {
|
|
804
|
+
if (!text) return;
|
|
805
|
+
|
|
806
|
+
const position = this._worldTransformer.screenToWorld({
|
|
807
|
+
x: text.x(),
|
|
808
|
+
y: text.y(),
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
const shape = new KonvaText(null, text);
|
|
812
|
+
texts.push({
|
|
813
|
+
id: shape.id(),
|
|
814
|
+
position: this.getPoint3dFromArray(position),
|
|
815
|
+
text: shape.getText(),
|
|
816
|
+
text_size: textSize * textScale.y,
|
|
817
|
+
angle: shape.getRotation(),
|
|
818
|
+
color: shape.getColor(),
|
|
819
|
+
font_size: shape.getFontSize(),
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
return texts;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
private getMarkupRectangles() {
|
|
827
|
+
const rectangles = [];
|
|
828
|
+
this.konvaLayerFind(MarkupMode.Rectangle).forEach((rect) => {
|
|
829
|
+
const position = rect.position();
|
|
830
|
+
const worldPoint = this._worldTransformer.screenToWorld(position);
|
|
831
|
+
|
|
832
|
+
const shape = new KonvaRectangle(null, rect);
|
|
833
|
+
rectangles.push({
|
|
834
|
+
id: shape.id(),
|
|
835
|
+
position: this.getPoint3dFromArray(worldPoint),
|
|
836
|
+
width: shape.getWidth(),
|
|
837
|
+
height: shape.getHeigth(),
|
|
838
|
+
line_width: shape.getLineWidth(),
|
|
839
|
+
color: shape.getColor(),
|
|
840
|
+
});
|
|
841
|
+
});
|
|
842
|
+
|
|
843
|
+
return rectangles;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
private getMarkupEllipses() {
|
|
847
|
+
const ellipses = [];
|
|
848
|
+
this.konvaLayerFind(MarkupMode.Ellipse).forEach((ellipse) => {
|
|
849
|
+
const position = ellipse.position();
|
|
850
|
+
const worldPoint = this._worldTransformer.screenToWorld(position);
|
|
851
|
+
|
|
852
|
+
const shape = new KonvaEllipse(null, ellipse);
|
|
853
|
+
ellipses.push({
|
|
854
|
+
id: shape.id(),
|
|
855
|
+
position: this.getPoint3dFromArray(worldPoint),
|
|
856
|
+
radius: { x: ellipse.getRadiusX(), y: ellipse.getRadiusY() },
|
|
857
|
+
line_width: shape.getLineWidth(),
|
|
858
|
+
color: shape.getColor(),
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
return ellipses;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
private getMarkupArrows() {
|
|
866
|
+
const arrows = [];
|
|
867
|
+
this.konvaLayerFind(MarkupMode.Arrow).forEach((arrow) => {
|
|
868
|
+
// we need getAbsoluteTransform because inside Konva position starts from {0, 0}
|
|
869
|
+
const absoluteTransform = arrow.getAbsoluteTransform();
|
|
870
|
+
|
|
871
|
+
const atStartPoint = absoluteTransform.point({ x: arrow.points()[0], y: arrow.points()[1] });
|
|
872
|
+
const worldStartPoint = this._worldTransformer.screenToWorld(atStartPoint);
|
|
873
|
+
|
|
874
|
+
const atEndPoint = absoluteTransform.point({ x: arrow.points()[2], y: arrow.points()[3] });
|
|
875
|
+
const worldEndPoint = this._worldTransformer.screenToWorld(atEndPoint);
|
|
876
|
+
|
|
877
|
+
const shape = new KonvaArrow(null, arrow);
|
|
878
|
+
arrows.push({
|
|
879
|
+
id: shape.id(),
|
|
880
|
+
start: this.getPoint3dFromArray(worldStartPoint),
|
|
881
|
+
end: this.getPoint3dFromArray(worldEndPoint),
|
|
882
|
+
color: shape.getColor(),
|
|
883
|
+
});
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
return arrows;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
private getMarkupImages() {
|
|
890
|
+
const images = [];
|
|
891
|
+
this.konvaLayerFind(MarkupMode.Image).forEach((image) => {
|
|
892
|
+
const position = image.position();
|
|
893
|
+
const worldPoint = this._worldTransformer.screenToWorld(position);
|
|
894
|
+
|
|
895
|
+
const shape = new KonvaImage(null, image);
|
|
896
|
+
images.push({
|
|
897
|
+
id: shape.id(),
|
|
898
|
+
position: this.getPoint3dFromArray(worldPoint),
|
|
899
|
+
src: shape.getSrc(),
|
|
900
|
+
width: shape.getWidth(),
|
|
901
|
+
height: shape.getHeight(),
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
return images;
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
private getMarkupClouds() {
|
|
909
|
+
const clouds = [];
|
|
910
|
+
this.konvaLayerFind(MarkupMode.Cloud).forEach((cloud) => {
|
|
911
|
+
const position = cloud.position();
|
|
912
|
+
const worldPoint = this._worldTransformer.screenToWorld(position);
|
|
913
|
+
|
|
914
|
+
const shape = new KonvaCloud(null, cloud);
|
|
915
|
+
clouds.push({
|
|
916
|
+
id: shape.id(),
|
|
917
|
+
position: this.getPoint3dFromArray(worldPoint),
|
|
918
|
+
width: shape.getWidth(),
|
|
919
|
+
height: shape.getHeigth(),
|
|
920
|
+
line_width: shape.getLineWidth(),
|
|
921
|
+
color: shape.getColor(),
|
|
922
|
+
});
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
return clouds;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
private loadMarkup(viewpoint: IViewpoint) {
|
|
929
|
+
viewpoint.lines?.forEach((vpLine) => {
|
|
930
|
+
const linePoints = [];
|
|
931
|
+
vpLine.points.forEach((point) => {
|
|
932
|
+
const screenPoint = this._worldTransformer.worldToScreen(point);
|
|
933
|
+
linePoints.push(screenPoint.x);
|
|
934
|
+
linePoints.push(screenPoint.y);
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
this.addLine(linePoints, vpLine.color, vpLine.type as MarkupLineType, vpLine.width, vpLine.id);
|
|
938
|
+
});
|
|
939
|
+
|
|
940
|
+
viewpoint.texts?.forEach((vpText) => {
|
|
941
|
+
const screenPoint = this._worldTransformer.worldToScreen(vpText.position);
|
|
942
|
+
this.addText(vpText.text, screenPoint, vpText.angle, vpText.color, vpText.text_size, vpText.font_size, vpText.id);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
viewpoint.rectangles?.forEach((vpRect) => {
|
|
946
|
+
const screenPoint = this._worldTransformer.worldToScreen(vpRect.position);
|
|
947
|
+
this.addRectangle(screenPoint, vpRect.width, vpRect.height, vpRect.line_width, vpRect.color, vpRect.id);
|
|
948
|
+
});
|
|
949
|
+
|
|
950
|
+
viewpoint.ellipses?.forEach((vpEllipse) => {
|
|
951
|
+
const screenPoint = this._worldTransformer.worldToScreen(vpEllipse.position);
|
|
952
|
+
this.addEllipse(screenPoint, vpEllipse.radius, vpEllipse.line_width, vpEllipse.color, vpEllipse.id);
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
viewpoint.arrows?.forEach((vpArrow) => {
|
|
956
|
+
const startPoint = this._worldTransformer.worldToScreen(vpArrow.start);
|
|
957
|
+
const endPoint = this._worldTransformer.worldToScreen(vpArrow.end);
|
|
958
|
+
this.addArrow(startPoint, endPoint, vpArrow.color, vpArrow.id);
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
viewpoint.clouds?.forEach((vpCloud) => {
|
|
962
|
+
const screenPoint = this._worldTransformer.worldToScreen(vpCloud.position);
|
|
963
|
+
this.addCloud(screenPoint, vpCloud.width, vpCloud.height, vpCloud.line_width, vpCloud.color, vpCloud.id);
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
viewpoint.images?.forEach((vpImage) => {
|
|
967
|
+
const screenPoint = this._worldTransformer.worldToScreen(vpImage.position);
|
|
968
|
+
this.addImage(screenPoint, vpImage.src, vpImage.width, vpImage.height, vpImage.id);
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
private combineMarkupWithDrawing() {
|
|
973
|
+
const trNodes = this._konvaTransformer.nodes();
|
|
974
|
+
if (trNodes.length > 0) {
|
|
975
|
+
this._konvaTransformer.nodes([]);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
const tempCanvas = document.createElement("canvas");
|
|
979
|
+
tempCanvas.height = this._canvasOriginal.height;
|
|
980
|
+
tempCanvas.width = this._canvasOriginal.width;
|
|
981
|
+
const ctx = tempCanvas.getContext("2d");
|
|
982
|
+
ctx.drawImage(this._canvasOriginal, 0, 0);
|
|
983
|
+
ctx.drawImage(this._konvaStage.toCanvas({ pixelRatio: window.devicePixelRatio }), 0, 0);
|
|
984
|
+
return tempCanvas.toDataURL("image/jpeg", 0.25);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
private addLine(
|
|
988
|
+
linePoints: number[],
|
|
989
|
+
color?: string,
|
|
990
|
+
type?: MarkupLineType,
|
|
991
|
+
width?: number,
|
|
992
|
+
id?: string
|
|
993
|
+
): IMarkupLine | void {
|
|
994
|
+
if (!linePoints || linePoints.length === 0) return;
|
|
995
|
+
const points: { x: number; y: number }[] = [];
|
|
996
|
+
for (let i = 0; i < linePoints.length; i += 2) {
|
|
997
|
+
points.push({ x: linePoints[i], y: linePoints[i + 1] });
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const konvaLine = new KonvaLine({
|
|
1001
|
+
points,
|
|
1002
|
+
type: type || this.lineType,
|
|
1003
|
+
width: width || this.lineWidth,
|
|
1004
|
+
color: color || this._markupColor.HexColor,
|
|
1005
|
+
id,
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
const obj = konvaLine.ref();
|
|
1009
|
+
this._konvaLayer.add(obj);
|
|
1010
|
+
return konvaLine;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
private createTextInput(pos: Konva.Vector2d, inputX: number, inputY: number, angle: number, text: string): void {
|
|
1014
|
+
if (!this._textInputRef) {
|
|
1015
|
+
this._textInputPos = pos;
|
|
1016
|
+
this._textInputAngle = angle;
|
|
1017
|
+
this._textInputRef = document.createElement("textarea");
|
|
1018
|
+
this._textInputRef.style.zIndex = "9999";
|
|
1019
|
+
this._textInputRef.style.position = "absolute";
|
|
1020
|
+
this._textInputRef.style.display = "block";
|
|
1021
|
+
this._textInputRef.style.top = inputY + "px";
|
|
1022
|
+
this._textInputRef.style.left = inputX + "px";
|
|
1023
|
+
this._textInputRef.onkeydown = (event) => {
|
|
1024
|
+
if (event.key === "Enter" && !event.shiftKey) {
|
|
1025
|
+
event.preventDefault();
|
|
1026
|
+
this.addText(this._textInputRef.value, this._textInputPos, this._textInputAngle);
|
|
1027
|
+
}
|
|
1028
|
+
if (event.key === "Escape") {
|
|
1029
|
+
event.preventDefault();
|
|
1030
|
+
this.removeTextInput();
|
|
1031
|
+
}
|
|
1032
|
+
};
|
|
1033
|
+
if (text) this._textInputRef.value = text;
|
|
1034
|
+
document.body.appendChild(this._textInputRef);
|
|
1035
|
+
|
|
1036
|
+
setTimeout(() => {
|
|
1037
|
+
this._textInputRef.focus();
|
|
1038
|
+
}, 50);
|
|
1039
|
+
} else {
|
|
1040
|
+
this.removeTextInput();
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
private removeTextInput(): void {
|
|
1045
|
+
this._textInputRef?.remove();
|
|
1046
|
+
this._textInputRef = null;
|
|
1047
|
+
this._textInputPos = null;
|
|
1048
|
+
this._textInputAngle = 0;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
private createImageInput(pos: Konva.Vector2d): void {
|
|
1052
|
+
if (!this._imageInputRef) {
|
|
1053
|
+
const convertBase64 = (file) => {
|
|
1054
|
+
return new Promise<string | ArrayBuffer>((resolve, reject) => {
|
|
1055
|
+
const fileReader = new FileReader();
|
|
1056
|
+
fileReader.readAsDataURL(file);
|
|
1057
|
+
|
|
1058
|
+
fileReader.onload = () => {
|
|
1059
|
+
resolve(fileReader.result);
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
fileReader.onerror = (error) => {
|
|
1063
|
+
reject(error);
|
|
1064
|
+
};
|
|
1065
|
+
});
|
|
1066
|
+
};
|
|
1067
|
+
|
|
1068
|
+
this._imageInputPos = pos;
|
|
1069
|
+
this._imageInputRef = document.createElement("input");
|
|
1070
|
+
this._imageInputRef.style.display = "none";
|
|
1071
|
+
this._imageInputRef.type = "file";
|
|
1072
|
+
this._imageInputRef.accept = "image/png, image/jpeg";
|
|
1073
|
+
this._imageInputRef.onchange = async (event) => {
|
|
1074
|
+
const file = (event.target as HTMLInputElement).files[0];
|
|
1075
|
+
const base64 = await convertBase64(file);
|
|
1076
|
+
this.addImage({ x: this._imageInputPos.x, y: this._imageInputPos.y }, base64.toString(), 0, 0);
|
|
1077
|
+
};
|
|
1078
|
+
this._imageInputRef.oncancel = (event) => {
|
|
1079
|
+
this.removeImageInput();
|
|
1080
|
+
};
|
|
1081
|
+
document.body.appendChild(this._imageInputRef);
|
|
1082
|
+
|
|
1083
|
+
setTimeout(() => {
|
|
1084
|
+
this._imageInputRef.click();
|
|
1085
|
+
}, 50);
|
|
1086
|
+
} else {
|
|
1087
|
+
this.removeImageInput();
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
private removeImageInput(): void {
|
|
1092
|
+
this._imageInputRef?.remove();
|
|
1093
|
+
this._imageInputRef = null;
|
|
1094
|
+
this._imageInputPos = null;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
private addText(
|
|
1098
|
+
specifiedText: string,
|
|
1099
|
+
position: Konva.Vector2d,
|
|
1100
|
+
angle?: number,
|
|
1101
|
+
color?: string,
|
|
1102
|
+
textSize?: number,
|
|
1103
|
+
fontSize?: number,
|
|
1104
|
+
id?: string
|
|
1105
|
+
): IMarkupText | void {
|
|
1106
|
+
const trNodes = this._konvaTransformer.nodes();
|
|
1107
|
+
if (trNodes.length > 0) {
|
|
1108
|
+
// in case of edit - remove old Konva.Text object
|
|
1109
|
+
trNodes[0].destroy();
|
|
1110
|
+
this._konvaTransformer.nodes([]);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
this.removeTextInput();
|
|
1114
|
+
|
|
1115
|
+
if (specifiedText) {
|
|
1116
|
+
const tolerance = 1.0e-6;
|
|
1117
|
+
|
|
1118
|
+
// in case we have old viewpoint without font_size
|
|
1119
|
+
if (textSize && textSize > tolerance && (!fontSize || fontSize < tolerance)) {
|
|
1120
|
+
const size = 0.02;
|
|
1121
|
+
const scale = this._worldTransformer.getScale();
|
|
1122
|
+
fontSize = textSize / (scale.y / size) / 34;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const konvaText = new KonvaText({
|
|
1126
|
+
position: { x: position.x, y: position.y },
|
|
1127
|
+
text: specifiedText,
|
|
1128
|
+
rotation: angle,
|
|
1129
|
+
fontSize: fontSize || this.fontSize,
|
|
1130
|
+
color: color || this._markupColor.HexColor,
|
|
1131
|
+
id,
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
this._konvaLayer.add(konvaText.ref());
|
|
1135
|
+
return konvaText;
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
private addRectangle(
|
|
1140
|
+
position: Konva.Vector2d,
|
|
1141
|
+
width: number,
|
|
1142
|
+
height: number,
|
|
1143
|
+
lineWidth?: number,
|
|
1144
|
+
color?: string,
|
|
1145
|
+
id?: string
|
|
1146
|
+
): IMarkupRectangle | void {
|
|
1147
|
+
if (!position) return;
|
|
1148
|
+
|
|
1149
|
+
const konvaRectangle = new KonvaRectangle({
|
|
1150
|
+
position,
|
|
1151
|
+
width,
|
|
1152
|
+
height,
|
|
1153
|
+
lineWidth: lineWidth || this.lineWidth,
|
|
1154
|
+
color: color || this._markupColor.HexColor,
|
|
1155
|
+
id,
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
const obj = konvaRectangle.ref();
|
|
1159
|
+
this._konvaLayer.add(obj);
|
|
1160
|
+
return konvaRectangle;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
private addEllipse(
|
|
1164
|
+
position: { x: number; y: number },
|
|
1165
|
+
radius: { x: number; y: number },
|
|
1166
|
+
lineWidth?: number,
|
|
1167
|
+
color?: string,
|
|
1168
|
+
id?: string
|
|
1169
|
+
): IMarkupEllipse | void {
|
|
1170
|
+
if (!position) return;
|
|
1171
|
+
|
|
1172
|
+
const konvaEllipse = new KonvaEllipse({
|
|
1173
|
+
position,
|
|
1174
|
+
radius,
|
|
1175
|
+
lineWidth,
|
|
1176
|
+
color: color || this._markupColor.HexColor,
|
|
1177
|
+
id,
|
|
1178
|
+
});
|
|
1179
|
+
|
|
1180
|
+
const obj = konvaEllipse.ref();
|
|
1181
|
+
this._konvaLayer.add(obj);
|
|
1182
|
+
return konvaEllipse;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
private addArrow(
|
|
1186
|
+
start: { x: number; y: number },
|
|
1187
|
+
end: { x: number; y: number },
|
|
1188
|
+
color?: string,
|
|
1189
|
+
id?: string
|
|
1190
|
+
): IMarkupArrow | void {
|
|
1191
|
+
if (!start || !end) return;
|
|
1192
|
+
|
|
1193
|
+
const konvaArrow = new KonvaArrow({
|
|
1194
|
+
start,
|
|
1195
|
+
end,
|
|
1196
|
+
color: color || this._markupColor.HexColor,
|
|
1197
|
+
id,
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
const obj = konvaArrow.ref();
|
|
1201
|
+
this._konvaLayer.add(obj);
|
|
1202
|
+
return konvaArrow;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
private addCloud(
|
|
1206
|
+
position: { x: number; y: number },
|
|
1207
|
+
width: number,
|
|
1208
|
+
height: number,
|
|
1209
|
+
lineWidth?: number,
|
|
1210
|
+
color?: string,
|
|
1211
|
+
id?: string
|
|
1212
|
+
): IMarkupCloud | void {
|
|
1213
|
+
if (!position || !width || !height) return;
|
|
1214
|
+
|
|
1215
|
+
const konvaCloud = new KonvaCloud({
|
|
1216
|
+
position,
|
|
1217
|
+
width,
|
|
1218
|
+
height,
|
|
1219
|
+
color: color || this._markupColor.HexColor,
|
|
1220
|
+
lineWidth: lineWidth || this.lineWidth,
|
|
1221
|
+
id,
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
const obj = konvaCloud.ref();
|
|
1225
|
+
this._konvaLayer.add(obj);
|
|
1226
|
+
return konvaCloud;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
private addImage(
|
|
1230
|
+
position: { x: number; y: number },
|
|
1231
|
+
src: string,
|
|
1232
|
+
width?: number,
|
|
1233
|
+
height?: number,
|
|
1234
|
+
id?: string
|
|
1235
|
+
): IMarkupImage | void {
|
|
1236
|
+
if (!position) return;
|
|
1237
|
+
|
|
1238
|
+
let konvaImage: IMarkupImage;
|
|
1239
|
+
|
|
1240
|
+
if (src) {
|
|
1241
|
+
konvaImage = new KonvaImage({
|
|
1242
|
+
position,
|
|
1243
|
+
src,
|
|
1244
|
+
width,
|
|
1245
|
+
height,
|
|
1246
|
+
id,
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
const obj = konvaImage.ref();
|
|
1250
|
+
this._konvaLayer.add(obj);
|
|
1251
|
+
|
|
1252
|
+
const trNodes = this._konvaTransformer.nodes();
|
|
1253
|
+
if (trNodes.length > 0) {
|
|
1254
|
+
// in case of edit - remove old Image placeholder object
|
|
1255
|
+
trNodes[0].destroy();
|
|
1256
|
+
this._konvaTransformer.nodes([]);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
this.removeImageInput();
|
|
1261
|
+
|
|
1262
|
+
return konvaImage;
|
|
1263
|
+
}
|
|
1264
|
+
}
|