import {DrawGroupCommand} from '../tools/drawCommands';
import {RoiFactory} from '../tools/roi';
import {guid} from '../math/stats';
import {Point2D} from '../math/point';
import {Style} from '../gui/style';
import {
getMousePoint,
getTouchPoints
} from '../gui/generic';
import {getLayerDetailsFromEvent} from '../gui/layerGroup';
import {ListenerHandler} from '../utils/listen';
import {logger} from '../utils/logger';
// doc imports
/* eslint-disable no-unused-vars */
import {App} from '../app/application';
import {LayerGroup} from '../gui/layerGroup';
import {Scalar2D} from '../math/scalar';
/* eslint-enable no-unused-vars */
/**
* The magic wand namespace.
*
* Ref: {@link https://github.com/Tamersoul/magic-wand-js}.
*
* @external MagicWand
*/
import MagicWand from 'magic-wand-tool';
/**
* Floodfill painting tool.
*/
export class Floodfill {
/**
* Associated app.
*
* @type {App}
*/
#app;
/**
* @param {App} app The associated application.
*/
constructor(app) {
this.#app = app;
}
/**
* Original variables from external library. Used as in the lib example.
*
* @type {number}
*/
#blurRadius = 5;
/**
* Original variables from external library. Used as in the lib example.
*
* @type {number}
*/
#simplifyTolerant = 0;
/**
* Original variables from external library. Used as in the lib example.
*
* @type {number}
*/
#simplifyCount = 2000;
/**
* Canvas info.
*
* @type {object}
*/
#imageInfo = null;
/**
* Object created by MagicWand lib containing border points.
*
* @type {object}
*/
#mask = null;
/**
* Threshold default tolerance of the tool border.
*
* @type {number}
*/
#initialthreshold = 10;
/**
* Threshold tolerance of the tool border.
*
* @type {number}
*/
#currentthreshold = null;
/**
* Interaction start flag.
*
* @type {boolean}
*/
#started = false;
/**
* Draw command.
*
* @type {object}
*/
#command = null;
/**
* Current shape group.
*
* @type {object}
*/
#shapeGroup = null;
/**
* Coordinates of the fist mousedown event.
*
* @type {object}
*/
#initialpoint;
/**
* Floodfill border.
*
* @type {object}
*/
#border = null;
/**
* List of parent points.
*
* @type {Array}
*/
#parentPoints = [];
/**
* Assistant variable to paint border on all slices.
*
* @type {boolean}
*/
#extender = false;
/**
* Timeout for painting on mousemove.
*
*/
#painterTimeout;
/**
* Drawing style.
*
* @type {Style}
*/
#style = new Style();
/**
* Listener handler.
*
* @type {ListenerHandler}
*/
#listenerHandler = new ListenerHandler();
/**
* Set extend option for painting border on all slices.
*
* @param {boolean} bool The option to set.
*/
setExtend(bool) {
this.#extender = bool;
}
/**
* Get extend option for painting border on all slices.
*
* @returns {boolean} The actual value of of the variable to use Floodfill
* on museup.
*/
getExtend() {
return this.#extender;
}
/**
* Get (x, y) coordinates referenced to the canvas.
*
* @param {Point2D} point The start point.
* @param {string} divId The layer group divId.
* @returns {Scalar2D} The coordinates as a {x,y}.
*/
#getIndex = (point, divId) => {
const layerGroup = this.#app.getLayerGroupByDivId(divId);
const viewLayer = layerGroup.getActiveViewLayer();
const index = viewLayer.displayToPlaneIndex(point);
return {
x: index.get(0),
y: index.get(1)
};
};
/**
* Calculate border.
*
* @param {object} points The input points.
* @param {number} threshold The threshold of the floodfill.
* @param {boolean} simple Return first points or a list.
* @returns {Array} The parent points.
*/
#calcBorder(points, threshold, simple) {
this.#parentPoints = [];
const image = {
data: this.#imageInfo.data,
width: this.#imageInfo.width,
height: this.#imageInfo.height,
bytes: 4
};
this.#mask = MagicWand.floodFill(image, points.x, points.y, threshold);
this.#mask = MagicWand.gaussBlurOnlyBorder(this.#mask, this.#blurRadius);
let cs = MagicWand.traceContours(this.#mask);
cs = MagicWand.simplifyContours(
cs, this.#simplifyTolerant, this.#simplifyCount);
if (cs.length > 0 && cs[0].points[0].x) {
if (simple) {
return cs[0].points;
}
for (let j = 0, icsl = cs[0].points.length; j < icsl; j++) {
this.#parentPoints.push(new Point2D(
cs[0].points[j].x,
cs[0].points[j].y
));
}
return this.#parentPoints;
} else {
return [];
}
}
/**
* Paint Floodfill.
*
* @param {object} point The start point.
* @param {number} threshold The border threshold.
* @param {LayerGroup} layerGroup The origin layer group.
* @returns {boolean} False if no border.
*/
#paintBorder(point, threshold, layerGroup) {
// Calculate the border
this.#border = this.#calcBorder(point, threshold, false);
// Paint the border
if (this.#border) {
const factory = new RoiFactory();
this.#shapeGroup = factory.create(this.#border, this.#style);
this.#shapeGroup.id(guid());
const drawLayer = layerGroup.getActiveDrawLayer();
const drawController = drawLayer.getDrawController();
// get the position group
const posGroup = drawController.getCurrentPosGroup();
// add shape group to position group
posGroup.add(this.#shapeGroup);
// draw shape command
this.#command = new DrawGroupCommand(
this.#shapeGroup,
'floodfill',
drawLayer
);
this.#command.onExecute = this.#fireEvent;
this.#command.onUndo = this.#fireEvent;
// // draw
this.#command.execute();
// save it in undo stack
this.#app.addToUndoStack(this.#command);
return true;
} else {
return false;
}
}
/**
* Create Floodfill in all the prev and next slices while border is found.
*
* @param {number} ini The first slice to extend to.
* @param {number} end The last slice to extend to.
* @param {object} layerGroup The origin layer group.
*/
extend(ini, end, layerGroup) {
//avoid errors
if (!this.#initialpoint) {
throw '\'initialpoint\' not found. User must click before use extend!';
}
// remove previous draw
if (this.#shapeGroup) {
this.#shapeGroup.destroy();
}
const viewController =
layerGroup.getActiveViewLayer().getViewController();
const pos = viewController.getCurrentIndex();
const imageSize = viewController.getImageSize();
const threshold = this.#currentthreshold || this.#initialthreshold;
// Iterate over the next images and paint border on each slice.
for (let i = pos.get(2),
len = end
? end : imageSize.get(2);
i < len; i++) {
if (!this.#paintBorder(this.#initialpoint, threshold, layerGroup)) {
break;
}
viewController.incrementIndex(2);
}
viewController.setCurrentPosition(pos);
// Iterate over the prev images and paint border on each slice.
for (let j = pos.get(2), jl = ini ? ini : 0; j > jl; j--) {
if (!this.#paintBorder(this.#initialpoint, threshold, layerGroup)) {
break;
}
viewController.decrementIndex(2);
}
viewController.setCurrentPosition(pos);
}
/**
* Modify tolerance threshold and redraw ROI.
*
* @param {number} modifyThreshold The new threshold.
* @param {object} shape The shape to update.
*/
modifyThreshold(modifyThreshold, shape) {
if (!shape && this.#shapeGroup) {
shape = this.#shapeGroup.getChildren(function (node) {
return node.name() === 'shape';
})[0];
} else {
throw 'No shape found';
}
clearTimeout(this.#painterTimeout);
this.#painterTimeout = setTimeout(() => {
this.#border = this.#calcBorder(
this.#initialpoint, modifyThreshold, true);
if (!this.#border) {
return false;
}
const arr = [];
for (let i = 0, bl = this.#border.length; i < bl; ++i) {
arr.push(this.#border[i].x);
arr.push(this.#border[i].y);
}
shape.setPoints(arr);
const shapeLayer = shape.getLayer();
shapeLayer.draw();
this.onThresholdChange(modifyThreshold);
}, 100);
}
/**
* Event fired when threshold change.
*
* @param {number} _value Current threshold.
*/
onThresholdChange(_value) {
// Defaults do nothing
}
/**
* Start tool interaction.
*
* @param {Point2D} point The start point.
* @param {string} divId The layer group divId.
*/
#start(point, divId) {
const layerGroup = this.#app.getLayerGroupByDivId(divId);
const viewLayer = layerGroup.getActiveViewLayer();
const drawLayer = layerGroup.getActiveDrawLayer();
this.#imageInfo = viewLayer.getImageData();
if (!this.#imageInfo) {
logger.error('No image found');
return;
}
// update zoom scale
this.#style.setZoomScale(
drawLayer.getKonvaLayer().getAbsoluteScale());
this.#started = true;
this.#initialpoint = this.#getIndex(point, divId);
this.#paintBorder(this.#initialpoint, this.#initialthreshold, layerGroup);
this.onThresholdChange(this.#initialthreshold);
}
/**
* Update tool interaction.
*
* @param {Point2D} point The update point.
* @param {string} divId The layer group divId.
*/
#update(point, divId) {
if (!this.#started) {
return;
}
const movedpoint = this.#getIndex(point, divId);
this.#currentthreshold = Math.round(Math.sqrt(
Math.pow((this.#initialpoint.x - movedpoint.x), 2) +
Math.pow((this.#initialpoint.y - movedpoint.y), 2)) / 2);
this.#currentthreshold = this.#currentthreshold < this.#initialthreshold
? this.#initialthreshold
: this.#currentthreshold - this.#initialthreshold;
this.modifyThreshold(this.#currentthreshold);
}
/**
* Finish tool interaction.
*/
#finish() {
if (this.#started) {
this.#started = false;
}
}
/**
* Handle mouse down event.
*
* @param {object} event The mouse down event.
*/
mousedown = (event) => {
const mousePoint = getMousePoint(event);
const layerDetails = getLayerDetailsFromEvent(event);
this.#start(mousePoint, layerDetails.groupDivId);
};
/**
* Handle mouse move event.
*
* @param {object} event The mouse move event.
*/
mousemove = (event) => {
const mousePoint = getMousePoint(event);
const layerDetails = getLayerDetailsFromEvent(event);
this.#update(mousePoint, layerDetails.groupDivId);
};
/**
* Handle mouse up event.
*
* @param {object} _event The mouse up event.
*/
mouseup = (_event) => {
this.#finish();
// TODO: re-activate
// if (this.#extender) {
// const layerDetails = getLayerDetailsFromEvent(event);
// const layerGroup =
// this.#app.getLayerGroupByDivId(layerDetails.groupDivId);
// this.extend(layerGroup);
// }
};
/**
* Handle mouse out event.
*
* @param {object} _event The mouse out event.
*/
mouseout = (_event) => {
this.#finish();
};
/**
* Handle touch start event.
*
* @param {object} event The touch start event.
*/
touchstart = (event) => {
const touchPoints = getTouchPoints(event);
const layerDetails = getLayerDetailsFromEvent(event);
this.#start(touchPoints[0], layerDetails.groupDivId);
};
/**
* Handle touch move event.
*
* @param {object} event The touch move event.
*/
touchmove = (event) => {
const touchPoints = getTouchPoints(event);
const layerDetails = getLayerDetailsFromEvent(event);
this.#update(touchPoints[0], layerDetails.groupDivId);
};
/**
* Handle touch end event.
*
* @param {object} _event The touch end event.
*/
touchend = (_event) => {
this.#finish();
};
/**
* Handle key down event.
*
* @param {object} event The key down event.
*/
keydown = (event) => {
event.context = 'Floodfill';
this.#app.onKeydown(event);
};
/**
* Activate the tool.
*
* @param {boolean} bool The flag to activate or not.
*/
activate(bool) {
if (bool) {
// init with the app window scale
this.#style.setBaseScale(this.#app.getBaseScale());
// set the default to the first in the list
this.setFeatures({shapeColour: this.#style.getLineColour()});
}
}
/**
* Initialise the tool.
*/
init() {
// does nothing
}
/**
* Get the list of event names that this tool can fire.
*
* @returns {Array} The list of event names.
*/
getEventNames() {
return ['drawcreate', 'drawchange', 'drawmove', 'drawdelete'];
}
/**
* Add an event listener to this class.
*
* @param {string} type The event type.
* @param {Function} callback The function associated with the provided
* event type, will be called with the fired event.
*/
addEventListener(type, callback) {
this.#listenerHandler.add(type, callback);
}
/**
* Remove an event listener from this class.
*
* @param {string} type The event type.
* @param {Function} callback The function associated with the provided
* event type.
*/
removeEventListener(type, callback) {
this.#listenerHandler.remove(type, callback);
}
/**
* Fire an event: call all associated listeners with the input event object.
*
* @param {object} event The event to fire.
*/
#fireEvent = (event) => {
this.#listenerHandler.fireEvent(event);
};
/**
* Set the tool live features: shape colour.
*
* @param {object} features The list of features.
*/
setFeatures(features) {
if (typeof features.shapeColour !== 'undefined') {
this.#style.setLineColour(features.shapeColour);
}
}
} // Floodfill class