import Konva from "konva"; import { IMarkup, MarkupMode } from "../../IMarkup"; import { Viewer } from "../../../Viewer"; import { ChangeActiveDraggerEvent, PanEvent } from "../../../ViewerEvents"; import { MarkupColor } from "./MarkupColor"; import * as utils from "../../../Draggers/MeasureLineDragger/MeasureUtils"; import { OdBaseDragger } from "../../../Draggers/Common/OdBaseDragger"; import { IMarkupObject } from "../../Api/IMarkupObject"; import { KonvaLine } from "../../Api/Impl/Konva/KonvaLine"; import { LineType } from "../../Api/IMarkupLine"; import { KonvaText } from "../../Api/Impl/Konva/KonvaText"; import { KonvaRectangle } from "../../Api/Impl/Konva/KonvaRectangle"; import { KonvaEllipse } from "../../Api/Impl/Konva/KonvaEllipse"; import { KonvaArrow } from "../../Api/Impl/Konva/KonvaArrow"; import { KonvaImage } from "../../Api/Impl/Konva/KonvaImage"; import { KonvaCloud } from "../../Api/Impl/Konva/KonvaCloud"; class KonvaShape { name: string; initializer: (ref: any) => any; } // move to separate class and create factory with enum? const MarkupMode2Konva = new Map([ [ MarkupMode.Line, { name: "Line", initializer: (ref) => { return new KonvaLine(null, ref); }, }, ], [ MarkupMode.Text, { name: "Text", initializer: (ref) => { return new KonvaText(null, ref); }, }, ], [ MarkupMode.Rectangle, { name: "Rect", initializer: (ref) => { return new KonvaRectangle(null, ref); }, }, ], [ MarkupMode.Ellipse, { name: "Ellipse", initializer: (ref) => { return new KonvaEllipse(null, ref); }, }, ], [ MarkupMode.Arrow, { name: "Arrow", initializer: (ref) => { return new KonvaArrow(null, ref); }, }, ], [ MarkupMode.Image, { name: "Image", initializer: (ref) => { return new KonvaImage(null, ref); }, }, ], [ MarkupMode.Cloud, { name: "Cloud", initializer: (ref) => { return new KonvaCloud(null, ref); }, }, ], ]); export class KonvaMarkup implements IMarkup { private _isInitialized = false; private _viewer: Viewer; private _canvasOriginal: HTMLCanvasElement; private _canvasEvents: string[]; private _markupIsActive: boolean; private _markupMode: MarkupMode; private _markupColor: MarkupColor; private _konvaStage: Konva.Stage; private _konvaLayer: Konva.Layer; private _konvaTransformer: Konva.Transformer; private _textInputRef: HTMLTextAreaElement; private _textInputPos: Konva.Vector2d; private _textInputAngle: number; private _markupContainer: HTMLDivElement; private _zIndex = 1; private readonly _markupContainerName = "markupContainer"; private readonly TEXT_FONT_FAMILY = "Calibri"; public lineWidth = 4; initialize(viewer: Viewer, canvas: HTMLCanvasElement, canvasEvents: string[] = []): void { if (!Konva) throw new Error( 'Konva Markup: Error during Markup Initialization. Konva is not initialized. Update node_modules or add to your page ' ); this._viewer = viewer; this._canvasOriginal = canvas; this._canvasEvents = canvasEvents; this._markupContainer = document.createElement("div"); this._markupContainer.id = this._markupContainerName; this._markupContainer.style.position = "absolute"; this._markupContainer.style.top = "0px"; this._markupContainer.style.left = "0px"; // to eliminate grey box during delete elements this._markupContainer.style.outline = "0px"; const parentDiv = this._canvasOriginal.parentElement; parentDiv.appendChild(this._markupContainer); this._markupColor = new MarkupColor(255, 0, 0); this._markupIsActive = false; this.initializeKonva(); this.resize(); this._canvasEvents.forEach((x) => this._markupContainer.addEventListener(x, this.redirectToViewer)); this._viewer.addEventListener("resize", this.resize); this._viewer.addEventListener("changeactivedragger", this.changeActiveDragger); this._viewer.addEventListener("pan", this.pan); this._isInitialized = true; } dispose(): void { if (!this._isInitialized) return; this._canvasEvents.forEach((x) => this._markupContainer.removeEventListener(x, this.redirectToViewer)); this._viewer.removeEventListener("pan", this.pan); this._viewer.removeEventListener("changeactivedragger", this.changeActiveDragger); this._viewer.removeEventListener("resize", this.resize); this.destroyKonva(); this._markupContainer.remove(); this._markupContainer = undefined; this._canvasOriginal = undefined; this._viewer = undefined; this._isInitialized = false; } changeActiveDragger = (event: ChangeActiveDraggerEvent) => { const draggerName = event.data; this._markupContainer.className = this._canvasOriginal.className .split(" ") .filter((x) => !x.startsWith("oda-cursor-")) .filter((x) => x) .concat(`oda-cursor-${draggerName.toLowerCase()}`) .join(" "); this.removeTextInput(); const markupMode = MarkupMode[draggerName]; const konvaMode = MarkupMode2Konva.get(markupMode); if (konvaMode) { this._markupMode = markupMode; this._markupIsActive = true; } else { this._markupIsActive = false; this._konvaTransformer.nodes([]); } }; resize = () => { this._konvaStage?.width(this._canvasOriginal.clientWidth); this._konvaStage?.height(this._canvasOriginal.clientHeight); }; pan = (event: PanEvent) => { const dX = event.dX / window.devicePixelRatio; const dY = event.dY / window.devicePixelRatio; Object.values(MarkupMode).forEach((mode) => this.konvaLayerFind(mode).forEach((x) => x.move({ x: dX, y: dY }))); }; redirectToViewer = (event) => { this._viewer.emit(event); }; getDraggers(): Map { return null; } clearOverlay(): void { this.removeTextInput(); this._konvaTransformer.nodes([]); Object.values(MarkupMode).forEach((mode) => this.konvaLayerFind(mode).forEach((x) => x.destroy())); } getMarkupColor(): { r: number; g: number; b: number } { return this._markupColor.RGB; } setMarkupColor(r: number, g: number, b: number): void { this._markupColor.setColor(r, g, b); } colorizeAllMarkup(r = 255, g = 0, b = 0): void { const hex = new MarkupColor(r, g, b).HexColor; Object.values(MarkupMode).forEach((mode) => { this.konvaLayerFind(mode).forEach((x) => { const konvaObj = MarkupMode2Konva.get(mode).initializer(x); if (konvaObj.setColor) konvaObj.setColor(hex); }); }); this._konvaLayer.draw(); } drawViewpoint(viewpoint: any): void { function getLogicalPoint3dAsArray(point3d) { return [point3d.x, point3d.y, point3d.z]; } if (!this._isInitialized) return; if (!this._viewer.visualizeJs) return; const visLib = this._viewer.visLib(); const visViewer = visLib.getViewer(); const activeView = visViewer.activeView; this._viewer.resetActiveDragger(); this._viewer.clearSlices(); this._viewer.clearOverlay(); if (viewpoint.orthogonal_camera) { activeView.setView( getLogicalPoint3dAsArray(viewpoint.orthogonal_camera.view_point), getLogicalPoint3dAsArray(viewpoint.orthogonal_camera.direction), getLogicalPoint3dAsArray(viewpoint.orthogonal_camera.up_vector), viewpoint.orthogonal_camera.field_width, viewpoint.orthogonal_camera.field_height, true ); } this._viewer.syncOverlay(); const markupColor = viewpoint.custom_fields.markup_color || { r: 255, g: 0, b: 0 }; this.setMarkupColor(markupColor.r, markupColor.g, markupColor.b); if (viewpoint.clipping_planes) { for (const plane of viewpoint.clipping_planes) { const cuttingPlane = new visLib.OdTvPlane(); cuttingPlane.set(getLogicalPoint3dAsArray(plane.location), getLogicalPoint3dAsArray(plane.direction)); activeView.addCuttingPlane(cuttingPlane); activeView.setEnableCuttingPlaneFill(true, 0x66, 0x66, 0x66); } } this.loadMarkup(viewpoint); this._viewer.update(); } createViewpoint(): object { if (!this._viewer.visualizeJs) return {}; const visLib = this._viewer.visLib(); const visViewer = visLib.getViewer(); const activeView = visViewer.activeView; const viewpoint = { lines: [], texts: [], arrows: [], clouds: [], ellipses: [], images: [], rectangles: [], clipping_planes: [], } as any; viewpoint.orthogonal_camera = { view_point: this.getPoint3dFromArray(activeView.viewPosition), direction: this.getPoint3dFromArray(activeView.viewTarget), up_vector: this.getPoint3dFromArray(activeView.upVector), field_width: activeView.viewFieldWidth, field_height: activeView.viewFieldHeight, }; for (let i = 0; i < activeView.numCuttingPlanes(); i++) { const cuttingPlane = activeView.getCuttingPlane(i); const plane = { location: this.getPoint3dFromArray(cuttingPlane.getOrigin()), direction: this.getPoint3dFromArray(cuttingPlane.normal()), }; viewpoint.clipping_planes.push(plane); } viewpoint.snapshot = { data: this.combineMarkupWithDrawing(), }; viewpoint.custom_fields = { markup_color: this.getMarkupColor(), }; this.fillViewpointShapes(viewpoint); viewpoint.description = new Date().toDateString(); return viewpoint; } createObject(type: string, params: any): IMarkupObject { let object; let zIndex = this._zIndex; // TODO: factory? switch (type) { case "line": object = new KonvaLine(params); zIndex = 1; break; case "text": object = new KonvaText(params); break; case "rectangle": object = new KonvaRectangle(params); zIndex = 1; break; case "ellipse": object = new KonvaEllipse(params); zIndex = 1; break; case "arrow": object = new KonvaArrow(params); break; case "image": object = new KonvaImage(params); zIndex = 0; break; case "cloud": object = new KonvaCloud(params); zIndex = 1; break; default: throw new Error("Markup CreateObject - unsupported type has been detected."); } this.addObject(object); // Set zIndex only when shape has been added to Layer else we will get "Konva warning: Node has no parent. zIndex parameter is ignored." object.setZIndex(zIndex); this._zIndex++; return object; } getObjects(): IMarkupObject[] { const objects = []; this.konvaLayerFind(MarkupMode.Line).forEach((line) => { objects.push(new KonvaLine(null, line)); }); this.konvaLayerFind(MarkupMode.Text).forEach((text) => { objects.push(new KonvaText(null, text)); }); this.konvaLayerFind(MarkupMode.Rectangle).forEach((rectangle) => { objects.push(new KonvaRectangle(null, rectangle)); }); this.konvaLayerFind(MarkupMode.Ellipse).forEach((ellipse) => { objects.push(new KonvaEllipse(null, ellipse)); }); this.konvaLayerFind(MarkupMode.Arrow).forEach((arrow) => { objects.push(new KonvaArrow(null, arrow)); }); this.konvaLayerFind(MarkupMode.Image).forEach((image) => { objects.push(new KonvaImage(null, image)); }); this.konvaLayerFind(MarkupMode.Cloud).forEach((cloud) => { objects.push(new KonvaCloud(null, cloud)); }); return objects; } getSelectedObjects(): IMarkupObject[] { const objects = []; this._konvaTransformer.nodes().forEach((obj) => { const konvaShapeName = obj.className; switch (konvaShapeName) { case "Line": objects.push(new KonvaLine(null, obj)); break; case "Text": objects.push(new KonvaText(null, obj)); break; case "Rect": objects.push(new KonvaRectangle(null, obj)); break; case "Ellipse": objects.push(new KonvaEllipse(null, obj)); break; case "Arrow": objects.push(new KonvaArrow(null, obj)); break; case "Image": objects.push(new KonvaImage(null, obj)); break; case "Cloud": objects.push(new KonvaCloud(null, obj)); break; default: break; } }); return objects; } selectObjects(objects: IMarkupObject[]) { const selectedObjs = this._konvaTransformer.nodes().concat(objects.map((x) => x.ref())); this._konvaTransformer.nodes(selectedObjs); } clearSelected(): void { this._konvaTransformer.nodes([]); } private getPoint3dFromArray(array) { return { x: array[0], y: array[1], z: array[2] }; } private fillViewpointShapes(viewpoint) { const markupLines = this.getMarkupLines(); if (markupLines && markupLines.length > 0) { markupLines?.forEach((line) => { viewpoint.lines.push(line); }); } const markupTexts = this.getMarkupTexts(); if (markupTexts && markupTexts.length > 0) { markupTexts?.forEach((text) => { viewpoint.texts.push(text); }); } const markupRectangles = this.getMarkupRectangles(); if (markupRectangles && markupRectangles.length > 0) { markupRectangles?.forEach((rectangle) => { viewpoint.rectangles.push(rectangle); }); } const markupEllipses = this.getMarkupEllipses(); if (markupEllipses && markupEllipses.length > 0) { markupEllipses?.forEach((ellipse) => { viewpoint.ellipses.push(ellipse); }); } const markupArrows = this.getMarkupArrows(); if (markupArrows && markupArrows.length > 0) { markupArrows?.forEach((arrow) => { viewpoint.arrows.push(arrow); }); } const markupImages = this.getMarkupImages(); if (markupImages && markupImages.length > 0) { markupImages?.forEach((image) => { viewpoint.images.push(image); }); } const markupClouds = this.getMarkupClouds(); if (markupClouds && markupClouds.length > 0) { markupClouds?.forEach((cloud) => { viewpoint.clouds.push(cloud); }); } } private addObject(object: IMarkupObject): void { this._konvaLayer.add(object.ref()); } private konvaLayerFind(markupShape: MarkupMode): any { const konvaShape = MarkupMode2Konva.get(markupShape); if (konvaShape) { // for "draggable" Konva uses Rectangles in Transformer. We need only Shapes from Layer. const konvaShapes = this._konvaLayer.find(konvaShape.name).filter((x) => x.parent instanceof Konva.Layer); return konvaShapes; } } private initializeKonva(): any { // first we need Konva core things: stage and layer this._konvaStage = new Konva.Stage({ container: this._markupContainerName, width: this._canvasOriginal.clientWidth, height: this._canvasOriginal.clientHeight, }); const stage = this._konvaStage; const layer = new Konva.Layer({ pixelRation: window.devicePixelRatio }); stage.add(layer); this._konvaLayer = layer; const transformer = new Konva.Transformer({ shouldOverdrawWholeArea: false, }); this._konvaTransformer = transformer; layer.add(transformer); let isPaint = false; let lastLine; stage.on("mousedown touchstart", (e) => { // do nothing if we mousedown on any shape if (!this._markupIsActive || e.target !== stage || this._markupMode === MarkupMode.Text) return; if (e.target === stage && transformer.nodes().length > 0) { transformer.nodes([]); return; } const pos = stage.getPointerPosition(); if (this._markupMode === MarkupMode.Line) { // add point twice, so we have some drawings even on a simple click lastLine = this.addLine([pos.x, pos.y, pos.x, pos.y]); isPaint = true; } // 25.2 - currently only for debug purposes: else if (this._markupMode === MarkupMode.Rectangle) { this.addRectangle({ x: pos.x, y: pos.y }, 50, 50); } else if (this._markupMode === MarkupMode.Ellipse) { this.addEllipse({ x: pos.x, y: pos.y }, { x: 50, y: 50 }); } else if (this._markupMode === MarkupMode.Arrow) { this.addArrow({ x: pos.x, y: pos.y }, { x: pos.x + 50, y: pos.y + 50 }); } else if (this._markupMode === MarkupMode.Cloud) { this.addCloud({ x: pos.x, y: pos.y }, 200, 400); } }); stage.on("mouseup touchend", (e) => { if (!this._markupIsActive) return; isPaint = false; }); stage.on("mousemove touchmove", (e) => { if (!this._markupIsActive) return; if (!isPaint) { return; } // prevent scrolling on touch devices //e.evt.preventDefault(); const pos = stage.getPointerPosition(); const newPoints = lastLine.points().concat([pos.x, pos.y]); lastLine.points(newPoints); }); // clicks should select/deselect shapes stage.on("click tap", (e) => { if (!this._markupIsActive) return; // if click on empty area - remove all selections if (e.target === stage) { if (this._markupMode === MarkupMode.Text) { if (this._textInputRef) this.addText(this._textInputRef.value, this._textInputPos, this._textInputAngle); else if (transformer.nodes().length === 0) { const pos = stage.getPointerPosition(); this.createTextInput(pos, e.evt.pageX, e.evt.pageY, 0, null); } } transformer.nodes([]); return; } if (e.target.className === "Text" && transformer.nodes().length === 1 && transformer.nodes()[0] === e.target) { if (this._textInputRef) this.addText(this._textInputRef.value, this._textInputPos, this._textInputAngle); else this.createTextInput( { x: e.target.attrs.x, y: e.target.attrs.y }, e.evt.pageX, e.evt.pageY, e.target.attrs.rotation, e.target.attrs.text ); return; } else { this.removeTextInput(); } if (transformer.nodes().filter((x) => x.className === "Cloud").length > 0 || e.target.className === "Cloud") { transformer.rotateEnabled(false); } else { transformer.rotateEnabled(true); } // do we pressed shift or ctrl? const metaPressed = e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey; const isSelected = transformer.nodes().indexOf(e.target) >= 0; if (!metaPressed && !isSelected) { // if no key pressed and the node is not selected // select just one transformer.nodes([e.target]); } else if (metaPressed && isSelected) { // if we pressed keys and node was selected // we need to remove it from selection: const nodes = transformer.nodes().slice(); // use slice to have new copy of array // remove node from array nodes.splice(nodes.indexOf(e.target), 1); transformer.nodes(nodes); } else if (metaPressed && !isSelected) { // add the node into selection const nodes = transformer.nodes().concat([e.target]); transformer.nodes(nodes); } }); const container = stage.container(); container.tabIndex = 1; // focus it // also stage will be in focus on its click container.focus(); container.addEventListener("keydown", (e) => { if (!this._markupIsActive) return; if (e.code === "Delete") { const trNodes = this._konvaTransformer.nodes(); if (trNodes.length > 0) { this._konvaTransformer.nodes().forEach((x) => x.destroy()); this._konvaTransformer.nodes([]); } layer.draw(); return; } e.preventDefault(); }); } private destroyKonva() { this.clearOverlay(); this._konvaStage.destroy(); this._konvaLayer = undefined; this._konvaTransformer = undefined; this._konvaStage = undefined; } private getMarkupLines() { const lines = []; this.konvaLayerFind(MarkupMode.Line).forEach((line) => { const linePoints = line.points(); if (!linePoints) return; const worldPoints = []; const absoluteTransform = line.getAbsoluteTransform(); for (let i = 0; i < linePoints.length; i += 2) { // we need getAbsoluteTransform because inside Konva position starts from {0, 0} // https://stackoverflow.com/a/57641487 - check answer's comments const atPoint = absoluteTransform.point({ x: linePoints[i], y: linePoints[i + 1] }); const worldPoint = this._viewer .visViewer() .screenToWorld(atPoint.x * window.devicePixelRatio, atPoint.y * window.devicePixelRatio); worldPoints.push(worldPoint); } const konvaLine = new KonvaLine(null, line); lines.push({ id: konvaLine.id(), points: worldPoints.map((p) => this.getPoint3dFromArray(p)), color: konvaLine.getColor() || "ff0000", type: konvaLine.getLineType() || "solid", width: konvaLine.getLineWidth() || 3, }); }); return lines; } private getMarkupTexts() { const texts = []; const textSize = 0.02; let textScale = 1.0; const projMtrx = this._viewer.visViewer().activeView.projectionMatrix; const mtrxNumber = projMtrx.get(1, 1); const tol = 1.0e-6; if (mtrxNumber > tol || mtrxNumber < -tol) { textScale = 1 / mtrxNumber; } this.konvaLayerFind(MarkupMode.Text).forEach((text) => { if (!text) return; const position = this._viewer .visViewer() .screenToWorld(text.x() * window.devicePixelRatio, text.y() * window.devicePixelRatio); const shape = new KonvaText(null, text); texts.push({ id: shape.id(), position: this.getPoint3dFromArray(position), text: shape.getText(), text_size: textSize * textScale, angle: shape.getRotation(), color: shape.getColor(), font_size: shape.getFontSize(), }); }); return texts; } private getMarkupRectangles() { const rectangles = []; this.konvaLayerFind(MarkupMode.Rectangle).forEach((rect) => { const position = rect.position(); const worldPoint = this._viewer .visViewer() .screenToWorld(position.x * window.devicePixelRatio, position.y * window.devicePixelRatio); const shape = new KonvaRectangle(null, rect); rectangles.push({ id: shape.id(), position: this.getPoint3dFromArray(worldPoint), width: shape.getWidth(), height: shape.getHeigth(), line_width: shape.getLineWidth(), color: shape.getColor(), }); }); return rectangles; } private getMarkupEllipses() { const ellipses = []; this.konvaLayerFind(MarkupMode.Ellipse).forEach((ellipse) => { const position = ellipse.position(); const worldPoint = this._viewer .visViewer() .screenToWorld(position.x * window.devicePixelRatio, position.y * window.devicePixelRatio); const shape = new KonvaEllipse(null, ellipse); ellipses.push({ id: shape.id(), position: this.getPoint3dFromArray(worldPoint), radius: { x: ellipse.getRadiusX(), y: ellipse.getRadiusY() }, line_width: shape.getLineWidth(), color: shape.getColor(), }); }); return ellipses; } private getMarkupArrows() { const arrows = []; this.konvaLayerFind(MarkupMode.Arrow).forEach((arrow) => { // we need getAbsoluteTransform because inside Konva position starts from {0, 0} const absoluteTransform = arrow.getAbsoluteTransform(); const atStartPoint = absoluteTransform.point({ x: arrow.points()[0], y: arrow.points()[1] }); const worldStartPoint = this._viewer .visViewer() .screenToWorld(atStartPoint.x * window.devicePixelRatio, atStartPoint.y * window.devicePixelRatio); const atEndPoint = absoluteTransform.point({ x: arrow.points()[2], y: arrow.points()[3] }); const worldEndPoint = this._viewer .visViewer() .screenToWorld(atEndPoint.x * window.devicePixelRatio, atEndPoint.y * window.devicePixelRatio); const shape = new KonvaArrow(null, arrow); arrows.push({ id: shape.id(), start: this.getPoint3dFromArray(worldStartPoint), end: this.getPoint3dFromArray(worldEndPoint), color: shape.getColor(), }); }); return arrows; } private getMarkupImages() { const images = []; this.konvaLayerFind(MarkupMode.Image).forEach((image) => { const position = image.position(); const worldPoint = this._viewer .visViewer() .screenToWorld(position.x * window.devicePixelRatio, position.y * window.devicePixelRatio); const shape = new KonvaImage(null, image); images.push({ id: shape.id(), position: this.getPoint3dFromArray(worldPoint), src: shape.getSrc(), width: shape.getWidth(), height: shape.getHeight(), }); }); return images; } private getMarkupClouds() { const clouds = []; this.konvaLayerFind(MarkupMode.Cloud).forEach((cloud) => { const position = cloud.position(); const worldPoint = this._viewer .visViewer() .screenToWorld(position.x * window.devicePixelRatio, position.y * window.devicePixelRatio); const shape = new KonvaCloud(null, cloud); clouds.push({ id: shape.id(), position: this.getPoint3dFromArray(worldPoint), width: shape.getWidth(), height: shape.getHeigth(), line_width: shape.getLineWidth(), color: shape.getColor(), }); }); return clouds; } private loadMarkup(viewpoint) { viewpoint.lines?.forEach((vpLine) => { const linePoints = []; vpLine.points.forEach((point) => { const screenPoint = utils.worldToScreen( [point.x, point.y, point.z], this._viewer.visualizeJs, this._viewer.visViewer() ); linePoints.push(screenPoint.x); linePoints.push(screenPoint.y); }); this.addLine(linePoints, vpLine.color, vpLine.type, vpLine.width, vpLine.id); }); viewpoint.texts?.forEach((vpText) => { const screenPoint = utils.worldToScreen( [vpText.position.x, vpText.position.y, vpText.position.z], this._viewer.visualizeJs, this._viewer.visViewer() ); this.addText(vpText.text, screenPoint, vpText.angle, vpText.color, vpText.text_size, vpText.font_size, vpText.id); }); viewpoint.rectangles?.forEach((vpRect) => { const screenPoint = utils.worldToScreen( [vpRect.position.x, vpRect.position.y, vpRect.position.z], this._viewer.visualizeJs, this._viewer.visViewer() ); this.addRectangle( { x: screenPoint.x, y: screenPoint.y }, vpRect.width, vpRect.height, vpRect.line_width, vpRect.color, vpRect.id ); }); viewpoint.ellipses?.forEach((vpEllipse) => { const screenPoint = utils.worldToScreen( [vpEllipse.position.x, vpEllipse.position.y, vpEllipse.position.z], this._viewer.visualizeJs, this._viewer.visViewer() ); this.addEllipse( { x: screenPoint.x, y: screenPoint.y }, { x: vpEllipse.radius.x, y: vpEllipse.radius.y }, vpEllipse.line_width, vpEllipse.color, vpEllipse.id ); }); viewpoint.arrows?.forEach((vpArrow) => { const startPoint = utils.worldToScreen( [vpArrow.start.x, vpArrow.start.y, vpArrow.start.z], this._viewer.visualizeJs, this._viewer.visViewer() ); const endPoint = utils.worldToScreen( [vpArrow.end.x, vpArrow.end.y, vpArrow.end.z], this._viewer.visualizeJs, this._viewer.visViewer() ); this.addArrow({ x: startPoint.x, y: startPoint.y }, { x: endPoint.x, y: endPoint.y }, vpArrow.color, vpArrow.id); }); viewpoint.clouds?.forEach((vpCloud) => { const screenPoint = utils.worldToScreen( [vpCloud.position.x, vpCloud.position.y, vpCloud.position.z], this._viewer.visualizeJs, this._viewer.visViewer() ); this.addCloud( { x: screenPoint.x, y: screenPoint.y }, vpCloud.width, vpCloud.height, vpCloud.line_width, vpCloud.color, vpCloud.id ); }); viewpoint.images?.forEach((vpImage) => { const screenPoint = utils.worldToScreen( [vpImage.position.x, vpImage.position.y, vpImage.position.z], this._viewer.visualizeJs, this._viewer.visViewer() ); this.addImage({ x: screenPoint.x, y: screenPoint.y }, vpImage.src, vpImage.width, vpImage.height, vpImage.id); }); } private combineMarkupWithDrawing() { const trNodes = this._konvaTransformer.nodes(); if (trNodes.length > 0) { this._konvaTransformer.nodes([]); } const tempCanvas = document.createElement("canvas"); tempCanvas.height = this._canvasOriginal.height; tempCanvas.width = this._canvasOriginal.width; const ctx = tempCanvas.getContext("2d"); ctx.drawImage(this._canvasOriginal, 0, 0); ctx.drawImage(this._konvaStage.toCanvas({ pixelRatio: window.devicePixelRatio }), 0, 0); return tempCanvas.toDataURL("image/jpeg", 0.25); } private addLine( linePoints: number[], color?: string, type?: LineType, width?: number, id?: string ): Konva.Line | void { if (!linePoints || linePoints.length === 0) return; const points: { x: number; y: number }[] = []; for (let i = 0; i < linePoints.length; i += 2) { points.push({ x: linePoints[i], y: linePoints[i + 1] }); } const konvaLine = new KonvaLine({ points, color: color || this._markupColor.HexColor, type: type || LineType.Solid, width: width || this.lineWidth, id, }); const obj = konvaLine.ref(); this._konvaLayer.add(obj); return obj; } private createTextInput(pos: Konva.Vector2d, inputX: number, inputY: number, angle: number, text: string): void { if (!this._textInputRef) { this._textInputPos = pos; this._textInputAngle = angle; this._textInputRef = document.createElement("textarea"); this._textInputRef.style.zIndex = "9999"; this._textInputRef.style.position = "absolute"; this._textInputRef.style.display = "block"; this._textInputRef.style.top = inputY + "px"; this._textInputRef.style.left = inputX + "px"; this._textInputRef.onkeydown = (event) => { if (event.key === "Enter") { event.preventDefault(); this.addText(this._textInputRef.value, this._textInputPos, this._textInputAngle); } if (event.key === "Escape") { event.preventDefault(); this.removeTextInput(); } }; if (text) this._textInputRef.value = text; document.body.appendChild(this._textInputRef); setTimeout(() => { this._textInputRef.focus(); }, 50); } else { this.removeTextInput(); } } private removeTextInput(): void { this._textInputRef?.remove(); this._textInputRef = null; this._textInputPos = null; this._textInputAngle = 0; } private addText( specifiedText: string, position: Konva.Vector2d, angle?: number, color?: string, textSize?: number, fontSize?: number, id?: string ): void { if (specifiedText) { const tol = 1.0e-6; // in case we have old viewpoint without font_size if (textSize && textSize > tol && (!fontSize || fontSize < tol)) { const size = 0.02; let scale = 1.0; const projMtrx = this._viewer.visViewer().activeView.projectionMatrix; const mtrxNumber = projMtrx.get(1, 1); if (mtrxNumber > tol || mtrxNumber < -tol) { scale = 1 / mtrxNumber; } fontSize = textSize / (scale / size) / 34; } const konvaText = new KonvaText({ position: { x: position.x, y: position.y }, text: specifiedText, fontSize, color: color || this._markupColor.HexColor, rotation: angle, id, }); this._konvaLayer.add(konvaText.ref()); } const trNodes = this._konvaTransformer.nodes(); if (trNodes.length > 0) { // in case of edit - remove old Konva.Text object trNodes[0].destroy(); this._konvaTransformer.nodes([]); } this.removeTextInput(); return; } private addRectangle( position: Konva.Vector2d, width: number, height: number, lineWidth?: number, color?: string, id?: string ): Konva.Rect | void { if (!position) return; const konvaRectangle = new KonvaRectangle({ position, color: color || this._markupColor.HexColor, lineWidth: lineWidth || this.lineWidth, width, height, id, }); const obj = konvaRectangle.ref(); this._konvaLayer.add(obj); return obj; } private addEllipse( position: { x: number; y: number }, radius: { x: number; y: number }, lineWidth?: number, color?: string, id?: string ): Konva.Ellipse | void { if (!position) return; const konvaEllipse = new KonvaEllipse({ position, radius, lineWidth, color: color || this._markupColor.HexColor, id, }); const obj = konvaEllipse.ref(); this._konvaLayer.add(obj); return obj; } private addArrow( start: { x: number; y: number }, end: { x: number; y: number }, color?: string, id?: string ): Konva.Arrow | void { if (!start || !end) return; const konvaArrow = new KonvaArrow({ start, end, color: color || this._markupColor.HexColor, id, }); const obj = konvaArrow.ref(); this._konvaLayer.add(obj); return obj; } private addCloud( position: { x: number; y: number }, width: number, height: number, lineWidth?: number, color?: string, id?: string ): Konva.Shape | void { if (!position || !width || !height) return; const konvaCloud = new KonvaCloud({ position, width, height, color: color || this._markupColor.HexColor, lineWidth: lineWidth || this.lineWidth, id, }); const obj = konvaCloud.ref(); this._konvaLayer.add(obj); return obj; } private addImage( position: { x: number; y: number }, src: string, width: number, height: number, id?: string ): Konva.Image | void { if (!position || !width || !height) return; const konvaImage = new KonvaImage({ position, src, width, height, id, }); const obj = konvaImage.ref(); this._konvaLayer.add(obj); return obj; } }