import {getLayerDetailsFromEvent} from '../gui/layerGroup';
import {
getMousePoint,
getTouchPoints,
customUI
} from '../gui/generic';
import {Point2D} from '../math/point';
import {guid} from '../math/stats';
import {logger} from '../utils/logger';
import {replaceFlags} from '../utils/string';
import {
getShapeDisplayName,
DrawGroupCommand,
DeleteGroupCommand,
MoveGroupCommand
} from './drawCommands';
import {
isNodeNameShape
} from '../app/drawController';
import {ScrollWheel} from './scrollWheel';
import {ShapeEditor} from './editor';
// external
import Konva from 'konva';
// doc imports
/* eslint-disable no-unused-vars */
import {App} from '../app/application';
import {Style} from '../gui/style';
import {LayerGroup} from '../gui/layerGroup';
import {Scalar2D} from '../math/scalar';
import {DrawLayer} from '../gui/drawLayer';
import {DrawTrash} from './drawTrash';
/* eslint-enable no-unused-vars */
/**
* Draw Debug flag.
*/
export const DRAW_DEBUG = false;
/**
* Drawing tool.
*
* This tool is responsible for the draw of layer group structure.
*
* ```
* drawLayer
* |_ positionGroup: {name="position-group", id="#2-0#_#3-1"}
* |_ shapeGroup: {name="{shape name}-group", id="#"}
* |_ shape: {name="shape"},
* |_ label: {name="label"},
* |_ extra: line tick, protractor arc...
* ```
*
* Discussion:
* - posGroup > shapeGroup:
* (pro) slice/frame display: 1 loop -
* (cons) multi-slice shape splitted in positionGroups.
* - shapeGroup > posGroup:
* (pros) more logical -
* (cons) slice/frame display: 2 loops.
*/
export class Draw {
/**
* Associated app.
*
* @type {App}
*/
#app;
/**
* Scroll wheel handler.
*
* @type {ScrollWheel}
*/
#scrollWhell;
/**
* Shape editor.
*
* @type {ShapeEditor}
*/
#shapeEditor;
/**
* Trash draw: a cross.
*
* @type {DrawTrash}
*/
#trash;
/**
* Drawing style.
*
* @type {Style}
*/
#style;
/**
* Callback store to allow attach/detach.
*
* @type {Array}
*/
#callbackStore = [];
/**
* @param {App} app The associated application.
*/
constructor(app) {
this.#app = app;
this.#scrollWhell = new ScrollWheel(app);
this.#shapeEditor = new ShapeEditor(app);
// associate the event listeners of the editor
// with those of the draw tool
this.#shapeEditor.setDrawEventCallback(this.#fireEvent);
this.#style = app.getStyle();
this.#trash = new DrawTrash();
}
/**
* Interaction start flag.
*
* @type {boolean}
*/
#isDrawing = false;
/**
* Shape factory list.
*
* @type {object}
*/
#shapeFactoryList = null;
/**
* Current shape factory.
*
* @type {object}
*/
#currentFactory = null;
/**
* Current shape group.
*
* @type {object}
*/
#tmpShapeGroup = null;
/**
* Shape name.
*
* @type {string}
*/
#shapeName;
/**
* List of points.
*
* @type {Point2D[]}
*/
#points = [];
/**
* Last selected point.
*
* @type {Point2D}
*/
#lastPoint = null;
/**
* Active shape, ie shape with mouse over.
*
* @type {Konva.Group}
*/
#activeShapeGroup;
/**
* Original mouse cursor.
*
* @type {string}
*/
#originalCursor;
/**
* Mouse cursor.
*
* @type {string}
*/
#mouseOverCursor = 'pointer';
/**
* With scroll flag.
*
* @type {boolean}
*/
#withScroll = true;
/**
* Auto shape colour: will use defaults colours and
* vary them according to the layer.
*
* @type {boolean}
*/
#autoShapeColour = true;
/**
* Event listeners.
*/
#listeners = {};
/**
* Flag to know if the last added point was made by mouse move.
*
* @type {boolean}
*/
#lastIsMouseMovePoint = false;
/**
* Start tool interaction.
*
* @param {Point2D} point The start point.
* @param {string} divId The layer group divId.
*/
#switchEditOrCreateShapeGroup(point, divId) {
const layerGroup = this.#app.getLayerGroupByDivId(divId);
const drawLayer = layerGroup.getActiveDrawLayer();
const stage = drawLayer.getKonvaStage();
// determine if the click happened in an existing shape
const kshape = stage.getIntersection({
x: point.getX(),
y: point.getY()
});
// update scale
this.#style.setZoomScale(stage.scale());
// If shape exists, let user to edit
if (kshape) {
this.#selectShapeGroup(layerGroup, drawLayer, kshape);
return;
}
// Else, is a new shape creation
this.#startShapeGroupCreation(layerGroup, point);
}
/**
* Initializes the new shape creation:
* - Updates the started variable,
* - Gets the factory,
* - Initializes the points array.
*
* @param {LayerGroup} layerGroup The layer group where the user clicks.
* @param {Point2D} point The start point where the user clicks.
*/
#startShapeGroupCreation(layerGroup, point) {
// disable edition
this.#shapeEditor.disable();
this.#shapeEditor.reset();
this.#setToDrawingState();
// store point
const viewLayer = layerGroup.getActiveViewLayer();
this.#lastPoint = viewLayer.displayToPlanePos(point);
this.#points.push(this.#lastPoint);
}
/**
* Sets the variables to drawing state:
* - Updates is drawing variable,
* - Initializes the current factory,
* - Resets points.
*/
#setToDrawingState() {
// start storing points
this.#isDrawing = true;
// set factory
this.#currentFactory = new this.#shapeFactoryList[this.#shapeName]();
// clear array
this.#points = [];
}
/**
* Resets the variables to not drawing state:
* - Destroys tmp shape group,
* - Updates is drawing variable,
* - Resets points.
*/
#setToNotDrawingState() {
this.#isDrawing = false;
this.#points = [];
}
/**
* Selects a shape group.
*
* @param {LayerGroup} layerGroup The layer group where the user clicks.
* @param {DrawLayer} drawLayer The draw layer where to draw.
* @param {Konva.Shape} kshape The shape that has been selected.
*/
#selectShapeGroup(layerGroup, drawLayer, kshape) {
const group = kshape.getParent();
const selectedShape = group.find('.shape')[0];
// reset editor if click on other shape
// (and avoid anchors mouse down)
if (selectedShape &&
selectedShape instanceof Konva.Shape &&
selectedShape !== this.#shapeEditor.getShape()) {
this.#shapeEditor.disable();
const viewController =
layerGroup.getActiveViewLayer().getViewController();
this.#shapeEditor.setShape(selectedShape, drawLayer, viewController);
this.#shapeEditor.enable();
}
}
/**
* Update tool interaction.
*
* @param {Point2D} point The update point.
* @param {string} divId The layer group divId.
*/
#updateShapeGroupCreation(point, divId) {
const layerGroup = this.#app.getLayerGroupByDivId(divId);
const viewLayer = layerGroup.getActiveViewLayer();
const pos = viewLayer.displayToPlanePos(point);
// draw line to current pos
if (Math.abs(pos.getX() - this.#lastPoint.getX()) > 0 ||
Math.abs(pos.getY() - this.#lastPoint.getY()) > 0) {
// clear last mouse move point
if (this.#lastIsMouseMovePoint) {
this.#points.pop();
}
// current point
this.#lastPoint = pos;
// mark it as temporary
this.#lastIsMouseMovePoint = true;
// add it to the list
this.#points.push(this.#lastPoint);
// update points
this.#onNewPoints(this.#points, layerGroup);
}
}
/**
* Finish tool interaction.
*
* @param {string} divId The layer group divId.
*/
#finishShapeGroupCreation(divId) {
// exit if no points
if (this.#points.length === 0) {
logger.warn('Draw mouseup but no points...');
return;
}
// do we have all the needed points
if (this.#points.length === this.#currentFactory.getNPoints()) {
// store points
const layerGroup =
this.#app.getLayerGroupByDivId(divId);
this.#onFinalPoints(this.#points, layerGroup);
this.#setToNotDrawingState();
}
// reset mouse move point flag
this.#lastIsMouseMovePoint = false;
}
/**
* Handle mouse down event.
*
* @param {object} event The mouse down event.
*/
mousedown = (event) => {
// exit if not started draw
if (this.#isDrawing) {
return;
}
const mousePoint = getMousePoint(event);
const layerDetails = getLayerDetailsFromEvent(event);
this.#switchEditOrCreateShapeGroup(mousePoint, layerDetails.groupDivId);
};
/**
* Handle mouse move event.
*
* @param {object} event The mouse move event.
*/
mousemove = (event) => {
// exit if not started draw
if (!this.#isDrawing) {
return;
}
const mousePoint = getMousePoint(event);
const layerDetails = getLayerDetailsFromEvent(event);
this.#updateShapeGroupCreation(mousePoint, layerDetails.groupDivId);
};
/**
* Handle mouse up event.
*
* @param {object} event The mouse up event.
*/
mouseup = (event) => {
// exit if not started draw
if (!this.#isDrawing) {
return;
}
const layerDetails = getLayerDetailsFromEvent(event);
this.#finishShapeGroupCreation(layerDetails.groupDivId);
};
/**
* Handle double click event: some tools use it to finish interaction.
*
* @param {object} event The double click event.
*/
dblclick = (event) => {
// only end by double click undefined NPoints
if (typeof this.#currentFactory.getNPoints() !== 'undefined') {
return;
}
// exit if not started draw
if (!this.#isDrawing) {
return;
}
// exit if no points
if (this.#points.length === 0) {
logger.warn('Draw dblclick but no points...');
return;
}
// store points
const layerDetails = getLayerDetailsFromEvent(event);
const layerGroup = this.#app.getLayerGroupByDivId(layerDetails.groupDivId);
this.#onFinalPoints(this.#points, layerGroup);
this.#setToNotDrawingState();
};
/**
* Handle mouse out event.
*
* @param {object} event The mouse out event.
*/
mouseout = (event) => {
// exit if not started draw
if (!this.#isDrawing) {
return;
}
const layerDetails = getLayerDetailsFromEvent(event);
this.#finishShapeGroupCreation(layerDetails.groupDivId);
};
/**
* Handle touch start event.
*
* @param {object} event The touch start event.
*/
touchstart = (event) => {
// exit if not started draw
if (this.#isDrawing) {
return;
}
const touchPoints = getTouchPoints(event);
const layerDetails = getLayerDetailsFromEvent(event);
this.#switchEditOrCreateShapeGroup(touchPoints[0], layerDetails.groupDivId);
};
/**
* Handle touch move event.
*
* @param {object} event The touch move event.
*/
touchmove = (event) => {
// exit if not started draw
if (!this.#isDrawing) {
return;
}
const layerDetails = getLayerDetailsFromEvent(event);
const touchPoints = getTouchPoints(event);
const layerGroup = this.#app.getLayerGroupByDivId(layerDetails.groupDivId);
const viewLayer = layerGroup.getActiveViewLayer();
const pos = viewLayer.displayToPlanePos(touchPoints[0]);
if (Math.abs(pos.getX() - this.#lastPoint.getX()) > 0 ||
Math.abs(pos.getY() - this.#lastPoint.getY()) > 0) {
// clear last added point from the list (but not the first one)
if (this.#points.length !== 1) {
this.#points.pop();
}
// current point
this.#lastPoint = pos;
// add current one to the list
this.#points.push(this.#lastPoint);
// allow for anchor points
if (this.#points.length < this.#currentFactory.getNPoints()) {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.#points.push(this.#lastPoint);
}, this.#currentFactory.getTimeout());
}
// update points
this.#onNewPoints(this.#points, layerGroup);
}
};
/**
* Handle touch end event.
*
* @param {object} event The touch end event.
*/
touchend = (event) => {
this.dblclick(event);
};
/**
* Handle mouse wheel event.
*
* @param {WheelEvent} event The mouse wheel event.
*/
wheel = (event) => {
if (this.#withScroll) {
this.#scrollWhell.wheel(event);
}
};
/**
* Handle key down event.
*
* @param {object} event The key down event.
*/
keydown = (event) => {
// call app handler if we are not in the middle of a draw
if (!this.#isDrawing) {
event.context = 'Draw';
this.#app.onKeydown(event);
}
// press delete or backspace key
if ((event.key === 'Delete' ||
event.key === 'Backspace') &&
this.#shapeEditor.isActive()) {
// get shape
const shapeGroup = this.#shapeEditor.getShape().getParent();
if (!(shapeGroup instanceof Konva.Group)) {
return;
}
const shape = shapeGroup.getChildren(isNodeNameShape)[0];
if (!(shape instanceof Konva.Shape)) {
return;
}
// delete command
const drawLayer = this.#app.getActiveLayerGroup().getActiveDrawLayer();
this.#emitDeleteCommand(drawLayer, shapeGroup, shape);
}
// escape key: exit shape creation
if (event.key === 'Escape' && this.#tmpShapeGroup !== null) {
const konvaLayer = this.#tmpShapeGroup.getLayer();
// reset temporary shape group
this.#tmpShapeGroup.destroy();
this.#tmpShapeGroup = null;
// set state
this.#setToNotDrawingState();
// redraw
konvaLayer.draw();
}
};
/**
* Update the current draw with new points.
*
* @param {Point2D[]} tmpPoints The array of new points.
* @param {LayerGroup} layerGroup The origin layer group.
*/
#onNewPoints(tmpPoints, layerGroup) {
// remove temporary shape draw
if (this.#tmpShapeGroup) {
this.#tmpShapeGroup.destroy();
this.#tmpShapeGroup = null;
}
const drawLayer = layerGroup.getActiveDrawLayer();
const konvaLayer = drawLayer.getKonvaLayer();
const viewLayer = layerGroup.getActiveViewLayer();
// auto mode: vary shape colour with layer id
if (this.#autoShapeColour) {
const colours = [
'#ffff80', '#ff80ff', '#80ffff', '#80ff80', '8080ff', 'ff8080'
];
// warning: depends on layer id nomenclature
const viewLayerId = viewLayer.getId();
const layerId = viewLayerId.substring(viewLayerId.length - 1);
// expecting one draw layer per view layer
const layerIndex = parseInt(layerId, 10) / 2;
const colour = colours[layerIndex];
if (typeof colour !== 'undefined') {
this.#style.setLineColour(colour);
}
}
// create shape group
const viewController = viewLayer.getViewController();
this.#tmpShapeGroup = this.#currentFactory.create(
tmpPoints, this.#style, viewController);
// do not listen during creation
const shape = this.#tmpShapeGroup.getChildren(isNodeNameShape)[0];
shape.listening(false);
konvaLayer.listening(false);
// draw shape
konvaLayer.add(this.#tmpShapeGroup);
konvaLayer.draw();
}
/**
* Create the final shape from a point list.
*
* @param {Point2D[]} finalPoints The array of points.
* @param {LayerGroup} layerGroup The origin layer group.
*/
#onFinalPoints(finalPoints, layerGroup) {
// remove temporary shape draw
// (has to be done before sending drawcreate event)
if (this.#tmpShapeGroup) {
this.#tmpShapeGroup.destroy();
this.#tmpShapeGroup = null;
}
const drawLayer = layerGroup.getActiveDrawLayer();
const konvaLayer = drawLayer.getKonvaLayer();
const drawController = drawLayer.getDrawController();
const viewLayer = layerGroup.getActiveViewLayer();
const viewController = viewLayer.getViewController();
// create final shape
const finalShapeGroup = this.#currentFactory.create(
finalPoints, this.#style, viewController);
finalShapeGroup.id(guid());
// get the position group
const posGroup = drawController.getCurrentPosGroup();
// add shape group to position group
posGroup.add(finalShapeGroup);
// re-activate layer
konvaLayer.listening(true);
this.#emitDrawGroupCommand(drawLayer, finalShapeGroup);
// activate shape listeners
this.#addShapeListeners(layerGroup, finalShapeGroup);
}
/**
* Create a draw group command, execute it and add
* it to the undo stack.
*
* @param {DrawLayer} drawLayer The associated layer.
* @param {Konva.Group} shapeGroup The shape group to draw.
*/
#emitDrawGroupCommand(drawLayer, shapeGroup) {
// draw shape command
const command = new DrawGroupCommand(
shapeGroup,
this.#shapeName,
drawLayer
);
command.onExecute = this.#fireEvent;
command.onUndo = this.#fireEvent;
// execute it
command.execute();
// add it to undo stack
this.#app.addToUndoStack(command);
}
/**
* Create a delete group command, execute it and add
* it to the undo stack.
*
* @param {DrawLayer} drawLayer The associated layer.
* @param {Konva.Group} shapeGroup The shape group to delete.
* @param {Konva.Shape} shape The shape to delete.
*/
#emitDeleteCommand(drawLayer, shapeGroup, shape) {
const shapeDisplayName = getShapeDisplayName(shape);
// delete command
const delcmd = new DeleteGroupCommand(
shapeGroup,
shapeDisplayName,
drawLayer
);
delcmd.onExecute = this.#fireEvent;
delcmd.onUndo = this.#fireEvent;
// execute it
delcmd.execute();
// add it to undo stack
this.#app.addToUndoStack(delcmd);
}
/**
* Create a move group command and add
* it to the undo stack. To no execute it.
*
* @param {DrawLayer} drawLayer The associated layer.
* @param {Konva.Group} shapeGroup The shape group to move.
* @param {Konva.Shape} shape The shape to move.
* @param {object} translation The move translation as {x,y}.
*/
#storeMoveCommand(drawLayer, shapeGroup, shape, translation) {
const shapeDisplayName = getShapeDisplayName(shape);
const mvcmd = new MoveGroupCommand(
shapeGroup,
shapeDisplayName,
translation,
drawLayer
);
mvcmd.onExecute = this.#fireEvent;
mvcmd.onUndo = this.#fireEvent;
// add it to undo stack
this.#app.addToUndoStack(mvcmd);
}
/**
* Get a layerGroup position callback.
*
* TODO: check needo for store item removal.
*
* @param {LayerGroup} layerGroup The origin layer group.
* @returns {Function} The layerGroup position callback.
*/
#getPositionCallback(layerGroup) {
const divId = layerGroup.getDivId();
if (typeof this.#callbackStore[divId] === 'undefined') {
this.#callbackStore[divId] = () => {
this.#updateDrawLayer(layerGroup);
};
}
return this.#callbackStore[divId];
}
/**
* Activate the tool.
*
* @param {boolean} flag The flag to activate or not.
*/
activate(flag) {
// reset shape display properties
this.#shapeEditor.disable();
this.#shapeEditor.reset();
// get the current draw layer
const layerGroup = this.#app.getActiveLayerGroup();
if (typeof layerGroup === 'undefined') {
throw new Error('No active layerGroup to activate draw on');
}
this.#activateCurrentPositionShapes(flag, layerGroup);
// listen to app change to update the draw layer
if (flag) {
// store cursor
this.#originalCursor = document.body.style.cursor;
// TODO: merge with drawController.activateDrawLayer?
this.#app.addEventListener('positionchange',
this.#getPositionCallback(layerGroup)
);
} else {
// reset shape and cursor
this.#resetActiveShapeGroup();
// reset local var
this.#originalCursor = undefined;
// remove listeners
this.#app.removeEventListener('positionchange',
this.#getPositionCallback(layerGroup)
);
}
}
/**
* Update the draw layer.
*
* @param {LayerGroup} layerGroup The origin layer group.
*/
#updateDrawLayer(layerGroup) {
// activate the shape at current position
this.#activateCurrentPositionShapes(true, layerGroup);
}
/**
* Activate shapes at current position.
*
* @param {boolean} visible Set the draw layer visible or not.
* @param {LayerGroup} layerGroup The origin layer group.
*/
#activateCurrentPositionShapes(visible, layerGroup) {
const drawLayer = layerGroup.getActiveDrawLayer();
if (typeof drawLayer === 'undefined') {
return;
}
const drawController = drawLayer.getDrawController();
// get shape groups at the current position
const shapeGroups =
drawController.getCurrentPosGroup().getChildren();
// set shape display properties
if (visible) {
// activate shape listeners
shapeGroups.forEach((group) => {
this.#addShapeListeners(layerGroup, group);
});
} else {
// de-activate shape listeners
shapeGroups.forEach((group) => {
this.#removeShapeListeners(group);
});
}
// draw
const konvaLayer = drawLayer.getKonvaLayer();
if (shapeGroups.length !== 0) {
konvaLayer.listening(true);
}
konvaLayer.draw();
}
/**
* Remove shape group listeners.
*
* @param {Konva.Group} shapeGroup The shape group to set off.
*/
#removeShapeListeners(shapeGroup) {
// mouse over
this.#removeShapeOverListeners(shapeGroup);
// drag
shapeGroup.draggable(false);
shapeGroup.off('dragstart.draw');
shapeGroup.off('dragmove.draw');
shapeGroup.off('dragend.draw');
shapeGroup.off('dblclick');
}
/**
* Get the real position from an event.
* TODO: use layer method?
*
* @param {Scalar2D} index The input index as {x,y}.
* @param {LayerGroup} layerGroup The origin layer group.
* @returns {Scalar2D} The real position in the image as {x,y}.
*/
#getRealPosition(index, layerGroup) {
const drawLayer = layerGroup.getActiveDrawLayer();
const stage = drawLayer.getKonvaStage();
return {
x: stage.offset().x + index.x / stage.scale().x,
y: stage.offset().y + index.y / stage.scale().y
};
}
/**
* Reset the active shape group and mouse cursor to their original state.
*/
#resetActiveShapeGroup() {
if (typeof this.#originalCursor !== 'undefined') {
document.body.style.cursor = this.#originalCursor;
}
if (typeof this.#activeShapeGroup !== 'undefined') {
this.#activeShapeGroup.opacity(1);
}
}
/**
* Add shape group mouse over and out listeners: updates
* shape group opacity and cursor.
*
* @param {Konva.Group} shapeGroup The shape group.
*/
#addShapeOverListeners(shapeGroup) {
// handle mouse over
shapeGroup.on('mouseover', () => {
// store locally
this.#activeShapeGroup = shapeGroup;
// change cursor and opacity
document.body.style.cursor = this.#mouseOverCursor;
shapeGroup.opacity(0.75);
});
// handle mouse out
shapeGroup.on('mouseout', () => {
// reset cursor and opacity
this.#resetActiveShapeGroup();
// reset local var
this.#activeShapeGroup = undefined;
});
}
/**
* Remove shape group mouse over and out listeners.
*
* @param {Konva.Group} shapeGroup The shape group.
*/
#removeShapeOverListeners(shapeGroup) {
shapeGroup.off('mouseover');
shapeGroup.off('mouseout');
}
/**
* Get a groups' shape factory.
*
* @param {Konva.Group} shapeGroup The shape group to set on.
* @returns {object} The corresponding factory.
*/
#getShapeFactory(shapeGroup) {
let factory;
const keys = Object.keys(this.#shapeFactoryList);
for (let i = 0; i < keys.length; ++i) {
factory = new this.#shapeFactoryList[keys[i]];
if (factory.isFactoryGroup(shapeGroup)) {
// stop at first find
break;
}
}
if (typeof factory === 'undefined') {
throw new Error('Cannot find factory to update quantification.');
}
return factory;
}
/**
* Add shape group listeners.
*
* @param {LayerGroup} layerGroup The origin layer group.
* @param {Konva.Group} shapeGroup The shape group to set on.
*/
#addShapeListeners(layerGroup, shapeGroup) {
// shape mouse over
this.#addShapeOverListeners(shapeGroup);
const drawLayer = layerGroup.getActiveDrawLayer();
const konvaLayer = drawLayer.getKonvaLayer();
// make it draggable
shapeGroup.draggable(true);
// cache drag start position
let dragStartPos = {x: shapeGroup.x(), y: shapeGroup.y()};
// command name based on shape type
const shape = shapeGroup.getChildren(isNodeNameShape)[0];
if (!(shape instanceof Konva.Shape)) {
return;
}
let colour = null;
// drag start event handling
shapeGroup.on('dragstart.draw', (/*event*/) => {
// store colour
const shape = shapeGroup.getChildren(isNodeNameShape)[0];
if (!(shape instanceof Konva.Shape)) {
return;
}
colour = shape.stroke();
// display trash
this.#trash.activate(drawLayer);
// deactivate anchors to avoid events on null shape
this.#shapeEditor.setAnchorsActive(false);
// draw
konvaLayer.draw();
});
// drag move event handling
shapeGroup.on('dragmove.draw', (event) => {
const group = event.target;
if (!(group instanceof Konva.Group)) {
return;
}
// validate the group position
validateGroupPosition(drawLayer.getBaseSize(), group);
// get appropriate factory
const factory = this.#getShapeFactory(shapeGroup);
// update quantification if possible
if (typeof factory.updateQuantification !== 'undefined') {
const vc = layerGroup.getActiveViewLayer().getViewController();
factory.updateQuantification(group, vc);
}
// highlight trash when on it
const mousePoint = getMousePoint(event.evt);
const offset = {
x: mousePoint.getX(),
y: mousePoint.getY()
};
const eventPos = this.#getRealPosition(offset, layerGroup);
this.#trash.changeChildrenColourOnTrashHover(eventPos,
shapeGroup, colour);
// draw
konvaLayer.draw();
});
// drag end event handling
shapeGroup.on('dragend.draw', (event) => {
const group = event.target;
if (!(group instanceof Konva.Group)) {
return;
}
// remove trash
this.#trash.remove();
// activate(false) will also trigger a dragend.draw
if (typeof event === 'undefined' ||
typeof event.evt === 'undefined') {
return;
}
const pos = {x: group.x(), y: group.y()};
// delete case
const mousePoint = getMousePoint(event.evt);
const offset = {
x: mousePoint.getX(),
y: mousePoint.getY()
};
const eventPos = this.#getRealPosition(offset, layerGroup);
if (this.#trash.isOverTrash(eventPos)) {
// compensate for the drag translation
group.x(dragStartPos.x);
group.y(dragStartPos.y);
// disable editor
this.#shapeEditor.disable();
this.#shapeEditor.reset();
this.#trash.changeGroupChildrenColour(shapeGroup, colour);
this.#emitDeleteCommand(drawLayer, shapeGroup, shape);
// reset cursor
document.body.style.cursor = this.#originalCursor;
} else {
const translation = {
x: pos.x - dragStartPos.x,
y: pos.y - dragStartPos.y
};
if (translation.x !== 0 || translation.y !== 0) {
// the move is handled by Konva, create a command but
// do not execute it
this.#storeMoveCommand(drawLayer, group, shape, translation);
// manually trigger a move event
this.#fireEvent({
type: 'drawmove',
id: group.id(),
srclayerid: drawLayer.getId(),
dataid: drawLayer.getDataId()
});
}
// reset anchors
this.#shapeEditor.setAnchorsActive(true);
this.#shapeEditor.resetAnchors();
}
// draw
konvaLayer.draw();
// reset start position
dragStartPos = {x: group.x(), y: group.y()};
});
// double click handling: update label
shapeGroup.on('dblclick', (event) => {
const group = event.currentTarget;
if (!(group instanceof Konva.Group)) {
return;
}
// get the label object for this shape
const label = group.findOne('Label');
if (!(label instanceof Konva.Label)) {
return;
}
// should just be one
if (typeof label === 'undefined') {
throw new Error('Could not find the shape label.');
}
const ktext = label.getText();
// id for event
const groupId = group.id();
const onSaveCallback = (meta) => {
// store meta
// @ts-expect-error
ktext.meta = meta;
// update text expression
ktext.setText(replaceFlags(
meta.textExpr, meta.quantification));
// hide label if no text
label.visible(meta.textExpr.length !== 0);
// trigger event
this.#fireEvent({
type: 'drawchange',
id: groupId,
srclayerid: drawLayer.getId(),
dataid: drawLayer.getDataId()
});
// draw
konvaLayer.draw();
};
// call roi dialog
// @ts-expect-error
customUI.openRoiDialog(ktext.meta, onSaveCallback);
});
}
/**
* Set the tool configuration options.
*
* @param {object} options The list of shape names amd classes.
*/
setOptions(options) {
// save the options as the shape factory list
this.#shapeFactoryList = options;
// pass them to the editor
this.#shapeEditor.setFactoryList(options);
}
/**
* Get the type of tool options: here 'factory' since the shape
* list contains factories to create each possible shape.
*
* @returns {string} The type.
*/
getOptionsType() {
return 'factory';
}
/**
* Set the tool live features: shape colour and shape name.
*
* @param {object} features The list of features.
*/
setFeatures(features) {
if (typeof features.autoShapeColour !== 'undefined') {
this.#autoShapeColour = features.autoShapeColour;
}
if (typeof features.shapeColour !== 'undefined') {
this.#style.setLineColour(features.shapeColour);
this.#autoShapeColour = false;
}
if (typeof features.shapeName !== 'undefined') {
// check if we have it
if (!this.hasShape(features.shapeName)) {
throw new Error('Unknown shape: \'' + features.shapeName + '\'');
}
this.#shapeName = features.shapeName;
}
if (typeof features.mouseOverCursor !== 'undefined') {
this.#mouseOverCursor = features.mouseOverCursor;
}
if (typeof features.withScroll !== 'undefined') {
this.#withScroll = features.withScroll;
}
}
/**
* Initialise the tool.
*/
init() {
// does nothing
}
/**
* Get the list of event names that this tool can fire.
*
* @returns {string[]} The list of event names.
*/
getEventNames() {
return [
'drawcreate', 'drawchange', 'drawmove', 'drawdelete'
];
}
/**
* Add an event listener on the app.
*
* @param {string} type The event type.
* @param {Function} listener The function associated with the provided
* event type.
*/
addEventListener(type, listener) {
if (typeof this.#listeners[type] === 'undefined') {
this.#listeners[type] = [];
}
this.#listeners[type].push(listener);
}
/**
* Remove an event listener from the app.
*
* @param {string} type The event type.
* @param {Function} listener The function associated with the provided
* event type.
*/
removeEventListener(type, listener) {
if (typeof this.#listeners[type] === 'undefined') {
return;
}
for (let i = 0; i < this.#listeners[type].length; ++i) {
if (this.#listeners[type][i] === listener) {
this.#listeners[type].splice(i, 1);
}
}
}
// Private Methods -----------------------------------------------------------
/**
* Fire an event: call all associated listeners.
*
* @param {object} event The event to fire.
*/
#fireEvent = (event) => {
if (typeof this.#listeners[event.type] === 'undefined') {
return;
}
for (let i = 0; i < this.#listeners[event.type].length; ++i) {
this.#listeners[event.type][i](event);
}
};
/**
* Check if the shape is in the shape list.
*
* @param {string} name The name of the shape.
* @returns {boolean} True if there is a factory for the shape.
*/
hasShape(name) {
return typeof this.#shapeFactoryList[name] !== 'undefined';
}
} // Draw class
/**
* Get the minimum position in a groups' anchors.
*
* @param {Konva.Group} group The group that contains anchors.
* @returns {Point2D|undefined} The minimum position.
*/
function getAnchorMin(group) {
const anchors = group.find('.anchor');
if (anchors.length === 0) {
return undefined;
}
let minX = anchors[0].x();
let minY = anchors[0].y();
for (let i = 0; i < anchors.length; ++i) {
minX = Math.min(minX, anchors[i].x());
minY = Math.min(minY, anchors[i].y());
}
return new Point2D(minX, minY);
}
/**
* Bound a node position.
*
* @param {Konva.Node} node The node to bound the position.
* @param {Point2D} min The minimum position.
* @param {Point2D} max The maximum position.
* @returns {boolean} True if the position was corrected.
*/
function boundNodePosition(node, min, max) {
let changed = false;
if (node.x() < min.getX()) {
node.x(min.getX());
changed = true;
} else if (node.x() > max.getX()) {
node.x(max.getX());
changed = true;
}
if (node.y() < min.getY()) {
node.y(min.getY());
changed = true;
} else if (node.y() > max.getY()) {
node.y(max.getY());
changed = true;
}
return changed;
}
/**
* Validate a group position.
*
* @param {Scalar2D} stageSize The stage size {x,y}.
* @param {Konva.Group} group The group to evaluate.
* @returns {boolean} True if the position was corrected.
*/
function validateGroupPosition(stageSize, group) {
// if anchors get mixed, width/height can be negative
const shape = group.getChildren(isNodeNameShape)[0];
const anchorMin = getAnchorMin(group);
// handle no anchor: when dragging the label, the editor does
// not activate
if (typeof anchorMin === 'undefined') {
return null;
}
const min = new Point2D(
-anchorMin.getX(),
-anchorMin.getY()
);
const max = new Point2D(
stageSize.x - (anchorMin.getX() + Math.abs(shape.width())),
stageSize.y - (anchorMin.getY() + Math.abs(shape.height()))
);
return boundNodePosition(group, min, max);
}
/**
* Validate an anchor position.
*
* @param {Scalar2D} stageSize The stage size {x,y}.
* @param {Konva.Shape} anchor The anchor to evaluate.
* @returns {boolean} True if the position was corrected.
*/
export function validateAnchorPosition(stageSize, anchor) {
const group = anchor.getParent();
const min = new Point2D(
-group.x(),
-group.y()
);
const max = new Point2D(
stageSize.x - group.x(),
stageSize.y - group.y()
);
return boundNodePosition(anchor, min, max);
}