diff --git a/frontend/images/icons/ellipse_select.svg b/frontend/images/icons/ellipse_select.svg
new file mode 100644
index 0000000..e910b95
--- /dev/null
+++ b/frontend/images/icons/ellipse_select.svg
@@ -0,0 +1,7 @@
+
diff --git a/frontend/images/icons/lasso.svg b/frontend/images/icons/lasso.svg
new file mode 100644
index 0000000..04e99b0
--- /dev/null
+++ b/frontend/images/icons/lasso.svg
@@ -0,0 +1,6 @@
+
diff --git a/frontend/images/icons/magic_wand.svg b/frontend/images/icons/magic_wand.svg
new file mode 100644
index 0000000..a44d444
--- /dev/null
+++ b/frontend/images/icons/magic_wand.svg
@@ -0,0 +1,13 @@
+
diff --git a/frontend/src/js/config.js b/frontend/src/js/config.js
index 4ba089e..ca46a26 100644
--- a/frontend/src/js/config.js
+++ b/frontend/src/js/config.js
@@ -103,6 +103,28 @@ config.TOOLS = [
on_activate: 'on_activate',
attributes: {},
},
+ {
+ name: 'magic_wand',
+ title: 'Magic Wand (Color Select)',
+ attributes: {
+ tolerance: {
+ value: 30,
+ min: 0,
+ max: 100,
+ },
+ contiguous: true,
+ },
+ },
+ {
+ name: 'lasso',
+ title: 'Lasso (Freehand Select)',
+ attributes: {},
+ },
+ {
+ name: 'ellipse_select',
+ title: 'Ellipse Selection',
+ attributes: {},
+ },
{
name: 'brush',
attributes: {
diff --git a/frontend/src/js/tools/ellipse_select.js b/frontend/src/js/tools/ellipse_select.js
new file mode 100644
index 0000000..724c1e1
--- /dev/null
+++ b/frontend/src/js/tools/ellipse_select.js
@@ -0,0 +1,657 @@
+/**
+ * Ellipse Selection Tool - Draw elliptical/circular selections
+ * Drag to create ellipse selection, Shift+Drag for perfect circle
+ * Hold Alt to draw from center
+ */
+
+import app from './../app.js';
+import config from './../config.js';
+import Base_tools_class from './../core/base-tools.js';
+import Base_layers_class from './../core/base-layers.js';
+import Helper_class from './../libs/helpers.js';
+import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js';
+
+class Ellipse_select_class extends Base_tools_class {
+
+ constructor(ctx) {
+ super();
+ this.Base_layers = new Base_layers_class();
+ this.Helper = new Helper_class();
+ this.ctx = ctx;
+ this.name = 'ellipse_select';
+
+ // Drawing state
+ this.isDrawing = false;
+ this.startPoint = null;
+ this.currentPoint = null;
+ this.isAdditive = false;
+ this.isCircle = false;
+ this.fromCenter = false;
+
+ // Store the current mask data
+ this.currentMask = null;
+ this.maskCanvas = null;
+ this.selectionBounds = null;
+
+ // Marching ants animation
+ this.marchingAntsOffset = 0;
+
+ // Edge canvas for drawing the mask outline
+ this.edgeCanvas = null;
+ }
+
+ load() {
+ var _this = this;
+
+ // Mouse events
+ document.addEventListener('mousedown', function (e) {
+ _this.mousedown(e);
+ });
+ document.addEventListener('mousemove', function (e) {
+ _this.mousemove(e);
+ });
+ document.addEventListener('mouseup', function (e) {
+ _this.mouseup(e);
+ });
+
+ // Touch events
+ document.addEventListener('touchstart', function (e) {
+ _this.mousedown(e);
+ });
+ document.addEventListener('touchmove', function (e) {
+ _this.mousemove(e);
+ });
+ document.addEventListener('touchend', function (e) {
+ _this.mouseup(e);
+ });
+
+ // Keyboard shortcuts
+ document.addEventListener('keydown', function(e) {
+ if (config.TOOL.name != _this.name) return;
+ if (_this.Helper.is_input(e.target)) return;
+
+ var code = e.keyCode;
+
+ if (code == 46 && _this.currentMask) {
+ e.preventDefault();
+ _this.deleteSelection();
+ }
+ if (code == 27 && _this.currentMask) {
+ e.preventDefault();
+ _this.clearSelection();
+ }
+ if (code == 67 && (e.ctrlKey || e.metaKey) && _this.currentMask) {
+ e.preventDefault();
+ _this.copyToLayer();
+ }
+ if (code == 88 && (e.ctrlKey || e.metaKey) && _this.currentMask) {
+ e.preventDefault();
+ _this.cutToLayer();
+ }
+ });
+
+ this.startMarchingAnts();
+ }
+
+ startMarchingAnts() {
+ var _this = this;
+
+ setInterval(function() {
+ if (_this.currentMask || _this.isDrawing) {
+ _this.marchingAntsOffset++;
+ if (_this.marchingAntsOffset > 16) {
+ _this.marchingAntsOffset = 0;
+ }
+ config.need_render = true;
+ }
+ }, 100);
+ }
+
+ mousedown(e) {
+ var mouse = this.get_mouse_info(e);
+
+ if (config.TOOL.name != this.name) return;
+ if (mouse.click_valid == false) return;
+
+ if (config.layer.type != 'image') {
+ alertify.error('Please select an image layer first');
+ return;
+ }
+
+ this.isDrawing = true;
+ this.isAdditive = e.shiftKey;
+ this.isCircle = false;
+ this.fromCenter = e.altKey;
+
+ this.startPoint = this.getImagePoint(mouse.x, mouse.y);
+ this.currentPoint = this.startPoint;
+
+ config.need_render = true;
+ }
+
+ mousemove(e) {
+ if (!this.isDrawing) return;
+ if (config.TOOL.name != this.name) return;
+
+ var mouse = this.get_mouse_info(e);
+
+ this.currentPoint = this.getImagePoint(mouse.x, mouse.y);
+ this.isCircle = e.shiftKey;
+ this.fromCenter = e.altKey;
+
+ config.need_render = true;
+ }
+
+ mouseup(e) {
+ if (!this.isDrawing) return;
+ if (config.TOOL.name != this.name) return;
+
+ this.isDrawing = false;
+
+ var mouse = this.get_mouse_info(e);
+ this.currentPoint = this.getImagePoint(mouse.x, mouse.y);
+ this.isCircle = e.shiftKey;
+ this.fromCenter = e.altKey;
+
+ // Calculate ellipse bounds
+ var ellipse = this.calculateEllipse();
+
+ if (ellipse.radiusX < 2 || ellipse.radiusY < 2) {
+ alertify.warning('Draw a larger selection');
+ config.need_render = true;
+ return;
+ }
+
+ // Create mask from ellipse
+ this.createMaskFromEllipse(ellipse, this.isAdditive);
+
+ this.startPoint = null;
+ this.currentPoint = null;
+ }
+
+ getImagePoint(mouseX, mouseY) {
+ var x = mouseX - config.layer.x;
+ var y = mouseY - config.layer.y;
+
+ if (config.layer.width != config.layer.width_original) {
+ x = x * (config.layer.width_original / config.layer.width);
+ }
+ if (config.layer.height != config.layer.height_original) {
+ y = y * (config.layer.height_original / config.layer.height);
+ }
+
+ x = Math.max(0, Math.min(config.layer.width_original - 1, Math.round(x)));
+ y = Math.max(0, Math.min(config.layer.height_original - 1, Math.round(y)));
+
+ return { x: x, y: y };
+ }
+
+ calculateEllipse() {
+ if (!this.startPoint || !this.currentPoint) {
+ return { centerX: 0, centerY: 0, radiusX: 0, radiusY: 0 };
+ }
+
+ var x1 = this.startPoint.x;
+ var y1 = this.startPoint.y;
+ var x2 = this.currentPoint.x;
+ var y2 = this.currentPoint.y;
+
+ var width = Math.abs(x2 - x1);
+ var height = Math.abs(y2 - y1);
+
+ // If Shift is held, make it a circle (equal radii)
+ if (this.isCircle) {
+ var maxDim = Math.max(width, height);
+ width = maxDim;
+ height = maxDim;
+ }
+
+ var centerX, centerY, radiusX, radiusY;
+
+ if (this.fromCenter) {
+ // Draw from center
+ centerX = x1;
+ centerY = y1;
+ radiusX = width;
+ radiusY = height;
+ } else {
+ // Draw from corner
+ var left = Math.min(x1, x2);
+ var top = Math.min(y1, y2);
+
+ if (this.isCircle) {
+ // Adjust for circle from corner
+ if (x2 < x1) left = x1 - width;
+ if (y2 < y1) top = y1 - height;
+ }
+
+ centerX = left + width / 2;
+ centerY = top + height / 2;
+ radiusX = width / 2;
+ radiusY = height / 2;
+ }
+
+ return {
+ centerX: centerX,
+ centerY: centerY,
+ radiusX: radiusX,
+ radiusY: radiusY
+ };
+ }
+
+ createMaskFromEllipse(ellipse, isAdditive) {
+ var width = config.layer.width_original;
+ var height = config.layer.height_original;
+
+ var newMaskCanvas = document.createElement('canvas');
+ newMaskCanvas.width = width;
+ newMaskCanvas.height = height;
+ var maskCtx = newMaskCanvas.getContext('2d');
+
+ // Draw filled ellipse
+ maskCtx.fillStyle = 'white';
+ maskCtx.beginPath();
+ maskCtx.ellipse(
+ ellipse.centerX,
+ ellipse.centerY,
+ ellipse.radiusX,
+ ellipse.radiusY,
+ 0, 0, Math.PI * 2
+ );
+ maskCtx.fill();
+
+ // Combine with existing mask if additive
+ if (isAdditive && this.maskCanvas) {
+ var combinedCanvas = document.createElement('canvas');
+ combinedCanvas.width = width;
+ combinedCanvas.height = height;
+ var combinedCtx = combinedCanvas.getContext('2d');
+
+ combinedCtx.drawImage(this.maskCanvas, 0, 0);
+ combinedCtx.globalCompositeOperation = 'lighter';
+ combinedCtx.drawImage(newMaskCanvas, 0, 0);
+
+ this.maskCanvas = combinedCanvas;
+ } else {
+ this.maskCanvas = newMaskCanvas;
+ }
+
+ this.currentMask = {
+ canvas: this.maskCanvas
+ };
+
+ window.smartSelectMask = this.currentMask;
+
+ this.calculateSelectionBounds();
+ this.extractContourPath();
+
+ config.need_render = true;
+ this.Base_layers.render();
+
+ if (isAdditive) {
+ alertify.success('Added to selection!');
+ } else {
+ alertify.success('Selection complete! Hold Shift while dragging to add more.');
+ }
+ }
+
+ calculateSelectionBounds() {
+ if (!this.maskCanvas) return;
+
+ var maskCtx = this.maskCanvas.getContext('2d');
+ var imageData = maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height);
+
+ var minX = this.maskCanvas.width, minY = this.maskCanvas.height;
+ var maxX = 0, maxY = 0;
+ var hasSelection = false;
+
+ for (var y = 0; y < this.maskCanvas.height; y++) {
+ for (var x = 0; x < this.maskCanvas.width; x++) {
+ var i = (y * this.maskCanvas.width + x) * 4;
+ if (imageData.data[i] > 128) {
+ hasSelection = true;
+ minX = Math.min(minX, x);
+ minY = Math.min(minY, y);
+ maxX = Math.max(maxX, x);
+ maxY = Math.max(maxY, y);
+ }
+ }
+ }
+
+ if (hasSelection && maxX > minX && maxY > minY) {
+ var scaleX = config.layer.width / config.layer.width_original;
+ var scaleY = config.layer.height / config.layer.height_original;
+
+ this.selectionBounds = {
+ x: config.layer.x + minX * scaleX,
+ y: config.layer.y + minY * scaleY,
+ width: (maxX - minX) * scaleX,
+ height: (maxY - minY) * scaleY,
+ origMinX: minX,
+ origMinY: minY,
+ origMaxX: maxX,
+ origMaxY: maxY
+ };
+ }
+ }
+
+ extractContourPath() {
+ if (!this.maskCanvas) return;
+
+ var maskCtx = this.maskCanvas.getContext('2d');
+ var imageData = maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height);
+ var width = this.maskCanvas.width;
+ var height = this.maskCanvas.height;
+ var data = imageData.data;
+
+ this.edgeCanvas = document.createElement('canvas');
+ this.edgeCanvas.width = width;
+ this.edgeCanvas.height = height;
+ var edgeCtx = this.edgeCanvas.getContext('2d');
+ var edgeImageData = edgeCtx.createImageData(width, height);
+ var edgeData = edgeImageData.data;
+
+ for (var y = 0; y < height; y++) {
+ for (var x = 0; x < width; x++) {
+ var i = (y * width + x) * 4;
+ var isMask = data[i] > 128;
+
+ if (isMask) {
+ var isEdge = false;
+
+ if (x > 0 && data[i - 4] <= 128) isEdge = true;
+ if (x < width - 1 && data[i + 4] <= 128) isEdge = true;
+ if (y > 0 && data[i - width * 4] <= 128) isEdge = true;
+ if (y < height - 1 && data[i + width * 4] <= 128) isEdge = true;
+ if (x == 0 || x == width - 1 || y == 0 || y == height - 1) isEdge = true;
+
+ if (isEdge) {
+ edgeData[i] = 255;
+ edgeData[i + 1] = 255;
+ edgeData[i + 2] = 255;
+ edgeData[i + 3] = 255;
+ }
+ }
+ }
+ }
+
+ edgeCtx.putImageData(edgeImageData, 0, 0);
+ }
+
+ render_overlay(ctx) {
+ // Draw current ellipse being drawn
+ if (this.isDrawing && this.startPoint && this.currentPoint) {
+ ctx.save();
+
+ var ellipse = this.calculateEllipse();
+ var scaleX = config.layer.width / config.layer.width_original;
+ var scaleY = config.layer.height / config.layer.height_original;
+
+ ctx.strokeStyle = '#ffff00';
+ ctx.lineWidth = 2 / config.ZOOM;
+ ctx.setLineDash([5, 5]);
+ ctx.lineDashOffset = -this.marchingAntsOffset;
+
+ ctx.beginPath();
+ ctx.ellipse(
+ config.layer.x + ellipse.centerX * scaleX,
+ config.layer.y + ellipse.centerY * scaleY,
+ ellipse.radiusX * scaleX,
+ ellipse.radiusY * scaleY,
+ 0, 0, Math.PI * 2
+ );
+ ctx.stroke();
+
+ ctx.restore();
+ }
+
+ // Draw existing selection
+ if (!this.currentMask || !this.maskCanvas) return;
+
+ ctx.save();
+
+ // Draw semi-transparent overlay
+ var inverseCanvas = document.createElement('canvas');
+ inverseCanvas.width = this.maskCanvas.width;
+ inverseCanvas.height = this.maskCanvas.height;
+ var inverseCtx = inverseCanvas.getContext('2d');
+
+ inverseCtx.fillStyle = 'rgba(0, 0, 0, 0.4)';
+ inverseCtx.fillRect(0, 0, inverseCanvas.width, inverseCanvas.height);
+
+ inverseCtx.globalCompositeOperation = 'destination-out';
+ inverseCtx.drawImage(this.maskCanvas, 0, 0);
+
+ ctx.drawImage(
+ inverseCanvas,
+ config.layer.x, config.layer.y,
+ config.layer.width, config.layer.height
+ );
+
+ // Draw marching ants
+ if (this.edgeCanvas) {
+ var antsCanvas = document.createElement('canvas');
+ antsCanvas.width = this.maskCanvas.width;
+ antsCanvas.height = this.maskCanvas.height;
+ var antsCtx = antsCanvas.getContext('2d');
+
+ antsCtx.drawImage(this.edgeCanvas, 0, 0);
+ antsCtx.globalCompositeOperation = 'source-in';
+
+ var color = ((Math.floor(this.marchingAntsOffset / 4) % 2) === 0) ? '#ffff00' : '#ffffff';
+ antsCtx.fillStyle = color;
+ antsCtx.fillRect(0, 0, antsCanvas.width, antsCanvas.height);
+
+ ctx.drawImage(
+ antsCanvas,
+ config.layer.x, config.layer.y,
+ config.layer.width, config.layer.height
+ );
+ }
+
+ ctx.restore();
+ }
+
+ copyToLayer() {
+ if (!this.currentMask || !this.maskCanvas) {
+ alertify.error('No selection to copy');
+ return;
+ }
+
+ var layer = config.layer;
+ if (layer.type != 'image') {
+ alertify.error('Layer must be an image');
+ return;
+ }
+
+ var bounds = this.selectionBounds;
+ if (!bounds || bounds.origMinX === undefined) {
+ alertify.error('Invalid selection bounds');
+ return;
+ }
+
+ var canvas = document.createElement('canvas');
+ canvas.width = layer.width_original;
+ canvas.height = layer.height_original;
+ var ctx = canvas.getContext('2d');
+
+ ctx.drawImage(layer.link, 0, 0);
+ ctx.globalCompositeOperation = 'destination-in';
+ ctx.drawImage(this.maskCanvas, 0, 0);
+
+ var cropWidth = bounds.origMaxX - bounds.origMinX;
+ var cropHeight = bounds.origMaxY - bounds.origMinY;
+
+ if (cropWidth <= 0 || cropHeight <= 0) {
+ alertify.error('Selection is too small');
+ return;
+ }
+
+ var croppedCanvas = document.createElement('canvas');
+ croppedCanvas.width = cropWidth;
+ croppedCanvas.height = cropHeight;
+ var croppedCtx = croppedCanvas.getContext('2d');
+
+ croppedCtx.drawImage(
+ canvas,
+ bounds.origMinX, bounds.origMinY, cropWidth, cropHeight,
+ 0, 0, cropWidth, cropHeight
+ );
+
+ var scaleX = layer.width / layer.width_original;
+ var scaleY = layer.height / layer.height_original;
+
+ var params = {
+ x: Math.round(layer.x + bounds.origMinX * scaleX),
+ y: Math.round(layer.y + bounds.origMinY * scaleY),
+ width: cropWidth,
+ height: cropHeight,
+ width_original: cropWidth,
+ height_original: cropHeight,
+ type: 'image',
+ name: 'Ellipse Selection',
+ data: croppedCanvas.toDataURL('image/png')
+ };
+
+ app.State.do_action(
+ new app.Actions.Bundle_action('copy_selection_to_layer', 'Copy Selection to Layer', [
+ new app.Actions.Insert_layer_action(params)
+ ])
+ );
+
+ alertify.success('Selection copied to new layer!');
+ }
+
+ cutToLayer() {
+ if (!this.currentMask || !this.maskCanvas) {
+ alertify.error('No selection to cut');
+ return;
+ }
+
+ var layer = config.layer;
+ if (layer.type != 'image') {
+ alertify.error('Layer must be an image');
+ return;
+ }
+
+ var bounds = this.selectionBounds;
+ if (!bounds || bounds.origMinX === undefined) {
+ alertify.error('Invalid selection bounds');
+ return;
+ }
+
+ var canvas = document.createElement('canvas');
+ canvas.width = layer.width_original;
+ canvas.height = layer.height_original;
+ var ctx = canvas.getContext('2d');
+
+ ctx.drawImage(layer.link, 0, 0);
+ ctx.globalCompositeOperation = 'destination-in';
+ ctx.drawImage(this.maskCanvas, 0, 0);
+
+ var cropWidth = bounds.origMaxX - bounds.origMinX;
+ var cropHeight = bounds.origMaxY - bounds.origMinY;
+
+ if (cropWidth <= 0 || cropHeight <= 0) {
+ alertify.error('Selection is too small');
+ return;
+ }
+
+ var croppedCanvas = document.createElement('canvas');
+ croppedCanvas.width = cropWidth;
+ croppedCanvas.height = cropHeight;
+ var croppedCtx = croppedCanvas.getContext('2d');
+
+ croppedCtx.drawImage(
+ canvas,
+ bounds.origMinX, bounds.origMinY, cropWidth, cropHeight,
+ 0, 0, cropWidth, cropHeight
+ );
+
+ var scaleX = layer.width / layer.width_original;
+ var scaleY = layer.height / layer.height_original;
+
+ var params = {
+ x: Math.round(layer.x + bounds.origMinX * scaleX),
+ y: Math.round(layer.y + bounds.origMinY * scaleY),
+ width: cropWidth,
+ height: cropHeight,
+ width_original: cropWidth,
+ height_original: cropHeight,
+ type: 'image',
+ name: 'Ellipse Cut',
+ data: croppedCanvas.toDataURL('image/png')
+ };
+
+ var holeCanvas = document.createElement('canvas');
+ holeCanvas.width = layer.width_original;
+ holeCanvas.height = layer.height_original;
+ var holeCtx = holeCanvas.getContext('2d');
+
+ holeCtx.drawImage(layer.link, 0, 0);
+ holeCtx.globalCompositeOperation = 'destination-out';
+ holeCtx.drawImage(this.maskCanvas, 0, 0);
+
+ app.State.do_action(
+ new app.Actions.Bundle_action('cut_selection_to_layer', 'Cut Selection to Layer', [
+ new app.Actions.Update_layer_image_action(holeCanvas, layer.id),
+ new app.Actions.Insert_layer_action(params)
+ ])
+ );
+
+ this.clearSelection();
+ alertify.success('Selection cut to new layer!');
+ }
+
+ deleteSelection() {
+ if (!this.currentMask || !this.maskCanvas) {
+ alertify.error('No selection to delete');
+ return;
+ }
+
+ var layer = config.layer;
+ if (layer.type != 'image') {
+ alertify.error('Layer must be an image');
+ return;
+ }
+
+ var holeCanvas = document.createElement('canvas');
+ holeCanvas.width = layer.width_original;
+ holeCanvas.height = layer.height_original;
+ var holeCtx = holeCanvas.getContext('2d');
+
+ holeCtx.drawImage(layer.link, 0, 0);
+ holeCtx.globalCompositeOperation = 'destination-out';
+ holeCtx.drawImage(this.maskCanvas, 0, 0);
+
+ app.State.do_action(
+ new app.Actions.Bundle_action('delete_selection', 'Delete Selection', [
+ new app.Actions.Update_layer_image_action(holeCanvas, layer.id)
+ ])
+ );
+
+ this.clearSelection();
+ alertify.success('Selection deleted!');
+ }
+
+ clearSelection() {
+ this.currentMask = null;
+ this.maskCanvas = null;
+ this.edgeCanvas = null;
+ this.selectionBounds = null;
+ this.startPoint = null;
+ this.currentPoint = null;
+ window.smartSelectMask = null;
+ config.need_render = true;
+ this.Base_layers.render();
+ }
+
+ on_leave() {
+ this.isDrawing = false;
+ this.startPoint = null;
+ this.currentPoint = null;
+ return [];
+ }
+}
+
+export default Ellipse_select_class;
diff --git a/frontend/src/js/tools/lasso.js b/frontend/src/js/tools/lasso.js
new file mode 100644
index 0000000..3676e6a
--- /dev/null
+++ b/frontend/src/js/tools/lasso.js
@@ -0,0 +1,597 @@
+/**
+ * Lasso Selection Tool - Freehand selection by drawing
+ * Draw around an area to select it, Shift+Draw to add to selection
+ */
+
+import app from './../app.js';
+import config from './../config.js';
+import Base_tools_class from './../core/base-tools.js';
+import Base_layers_class from './../core/base-layers.js';
+import Helper_class from './../libs/helpers.js';
+import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js';
+
+class Lasso_class extends Base_tools_class {
+
+ constructor(ctx) {
+ super();
+ this.Base_layers = new Base_layers_class();
+ this.Helper = new Helper_class();
+ this.ctx = ctx;
+ this.name = 'lasso';
+
+ // Drawing state
+ this.isDrawing = false;
+ this.points = [];
+
+ // Store the current mask data
+ this.currentMask = null;
+ this.maskCanvas = null;
+ this.selectionBounds = null;
+
+ // Marching ants animation
+ this.marchingAntsOffset = 0;
+
+ // Edge canvas for drawing the mask outline
+ this.edgeCanvas = null;
+ }
+
+ load() {
+ var _this = this;
+
+ // Mouse events
+ document.addEventListener('mousedown', function (e) {
+ _this.mousedown(e);
+ });
+ document.addEventListener('mousemove', function (e) {
+ _this.mousemove(e);
+ });
+ document.addEventListener('mouseup', function (e) {
+ _this.mouseup(e);
+ });
+
+ // Touch events
+ document.addEventListener('touchstart', function (e) {
+ _this.mousedown(e);
+ });
+ document.addEventListener('touchmove', function (e) {
+ _this.mousemove(e);
+ });
+ document.addEventListener('touchend', function (e) {
+ _this.mouseup(e);
+ });
+
+ // Keyboard shortcuts
+ document.addEventListener('keydown', function(e) {
+ if (config.TOOL.name != _this.name) return;
+ if (_this.Helper.is_input(e.target)) return;
+
+ var code = e.keyCode;
+
+ if (code == 46 && _this.currentMask) {
+ e.preventDefault();
+ _this.deleteSelection();
+ }
+ if (code == 27 && _this.currentMask) {
+ e.preventDefault();
+ _this.clearSelection();
+ }
+ if (code == 67 && (e.ctrlKey || e.metaKey) && _this.currentMask) {
+ e.preventDefault();
+ _this.copyToLayer();
+ }
+ if (code == 88 && (e.ctrlKey || e.metaKey) && _this.currentMask) {
+ e.preventDefault();
+ _this.cutToLayer();
+ }
+ });
+
+ this.startMarchingAnts();
+ }
+
+ startMarchingAnts() {
+ var _this = this;
+
+ setInterval(function() {
+ if (_this.currentMask || _this.isDrawing) {
+ _this.marchingAntsOffset++;
+ if (_this.marchingAntsOffset > 16) {
+ _this.marchingAntsOffset = 0;
+ }
+ config.need_render = true;
+ }
+ }, 100);
+ }
+
+ mousedown(e) {
+ var mouse = this.get_mouse_info(e);
+
+ if (config.TOOL.name != this.name) return;
+ if (mouse.click_valid == false) return;
+
+ if (config.layer.type != 'image') {
+ alertify.error('Please select an image layer first');
+ return;
+ }
+
+ this.isDrawing = true;
+ this.points = [];
+
+ // Get point relative to image
+ var point = this.getImagePoint(mouse.x, mouse.y);
+ if (point) {
+ this.points.push(point);
+ }
+
+ config.need_render = true;
+ }
+
+ mousemove(e) {
+ if (!this.isDrawing) return;
+ if (config.TOOL.name != this.name) return;
+
+ var mouse = this.get_mouse_info(e);
+
+ var point = this.getImagePoint(mouse.x, mouse.y);
+ if (point) {
+ this.points.push(point);
+ }
+
+ config.need_render = true;
+ }
+
+ mouseup(e) {
+ if (!this.isDrawing) return;
+ if (config.TOOL.name != this.name) return;
+
+ this.isDrawing = false;
+
+ if (this.points.length < 3) {
+ alertify.warning('Draw a larger selection');
+ this.points = [];
+ config.need_render = true;
+ return;
+ }
+
+ // Check for Shift key - additive selection
+ var isAdditive = e.shiftKey;
+
+ // Create mask from points
+ this.createMaskFromPoints(isAdditive);
+
+ this.points = [];
+ }
+
+ getImagePoint(mouseX, mouseY) {
+ var x = mouseX - config.layer.x;
+ var y = mouseY - config.layer.y;
+
+ // Adjust for layer scaling
+ if (config.layer.width != config.layer.width_original) {
+ x = x * (config.layer.width_original / config.layer.width);
+ }
+ if (config.layer.height != config.layer.height_original) {
+ y = y * (config.layer.height_original / config.layer.height);
+ }
+
+ // Clamp to image bounds
+ x = Math.max(0, Math.min(config.layer.width_original - 1, Math.round(x)));
+ y = Math.max(0, Math.min(config.layer.height_original - 1, Math.round(y)));
+
+ return { x: x, y: y };
+ }
+
+ createMaskFromPoints(isAdditive) {
+ var width = config.layer.width_original;
+ var height = config.layer.height_original;
+
+ // Create new mask canvas
+ var newMaskCanvas = document.createElement('canvas');
+ newMaskCanvas.width = width;
+ newMaskCanvas.height = height;
+ var maskCtx = newMaskCanvas.getContext('2d');
+
+ // Draw filled polygon
+ maskCtx.fillStyle = 'white';
+ maskCtx.beginPath();
+ maskCtx.moveTo(this.points[0].x, this.points[0].y);
+ for (var i = 1; i < this.points.length; i++) {
+ maskCtx.lineTo(this.points[i].x, this.points[i].y);
+ }
+ maskCtx.closePath();
+ maskCtx.fill();
+
+ // If additive and we have an existing mask, combine them
+ if (isAdditive && this.maskCanvas) {
+ var combinedCanvas = document.createElement('canvas');
+ combinedCanvas.width = width;
+ combinedCanvas.height = height;
+ var combinedCtx = combinedCanvas.getContext('2d');
+
+ combinedCtx.drawImage(this.maskCanvas, 0, 0);
+ combinedCtx.globalCompositeOperation = 'lighter';
+ combinedCtx.drawImage(newMaskCanvas, 0, 0);
+
+ this.maskCanvas = combinedCanvas;
+ } else {
+ this.maskCanvas = newMaskCanvas;
+ }
+
+ this.currentMask = {
+ canvas: this.maskCanvas
+ };
+
+ window.smartSelectMask = this.currentMask;
+
+ this.calculateSelectionBounds();
+ this.extractContourPath();
+
+ config.need_render = true;
+ this.Base_layers.render();
+
+ if (isAdditive) {
+ alertify.success('Added to selection!');
+ } else {
+ alertify.success('Selection complete! Shift+Draw to add more.');
+ }
+ }
+
+ calculateSelectionBounds() {
+ if (!this.maskCanvas) return;
+
+ var maskCtx = this.maskCanvas.getContext('2d');
+ var imageData = maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height);
+
+ var minX = this.maskCanvas.width, minY = this.maskCanvas.height;
+ var maxX = 0, maxY = 0;
+ var hasSelection = false;
+
+ for (var y = 0; y < this.maskCanvas.height; y++) {
+ for (var x = 0; x < this.maskCanvas.width; x++) {
+ var i = (y * this.maskCanvas.width + x) * 4;
+ if (imageData.data[i] > 128) {
+ hasSelection = true;
+ minX = Math.min(minX, x);
+ minY = Math.min(minY, y);
+ maxX = Math.max(maxX, x);
+ maxY = Math.max(maxY, y);
+ }
+ }
+ }
+
+ if (hasSelection && maxX > minX && maxY > minY) {
+ var scaleX = config.layer.width / config.layer.width_original;
+ var scaleY = config.layer.height / config.layer.height_original;
+
+ this.selectionBounds = {
+ x: config.layer.x + minX * scaleX,
+ y: config.layer.y + minY * scaleY,
+ width: (maxX - minX) * scaleX,
+ height: (maxY - minY) * scaleY,
+ origMinX: minX,
+ origMinY: minY,
+ origMaxX: maxX,
+ origMaxY: maxY
+ };
+ }
+ }
+
+ extractContourPath() {
+ if (!this.maskCanvas) return;
+
+ var maskCtx = this.maskCanvas.getContext('2d');
+ var imageData = maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height);
+ var width = this.maskCanvas.width;
+ var height = this.maskCanvas.height;
+ var data = imageData.data;
+
+ this.edgeCanvas = document.createElement('canvas');
+ this.edgeCanvas.width = width;
+ this.edgeCanvas.height = height;
+ var edgeCtx = this.edgeCanvas.getContext('2d');
+ var edgeImageData = edgeCtx.createImageData(width, height);
+ var edgeData = edgeImageData.data;
+
+ for (var y = 0; y < height; y++) {
+ for (var x = 0; x < width; x++) {
+ var i = (y * width + x) * 4;
+ var isMask = data[i] > 128;
+
+ if (isMask) {
+ var isEdge = false;
+
+ if (x > 0 && data[i - 4] <= 128) isEdge = true;
+ if (x < width - 1 && data[i + 4] <= 128) isEdge = true;
+ if (y > 0 && data[i - width * 4] <= 128) isEdge = true;
+ if (y < height - 1 && data[i + width * 4] <= 128) isEdge = true;
+ if (x == 0 || x == width - 1 || y == 0 || y == height - 1) isEdge = true;
+
+ if (isEdge) {
+ edgeData[i] = 255;
+ edgeData[i + 1] = 255;
+ edgeData[i + 2] = 255;
+ edgeData[i + 3] = 255;
+ }
+ }
+ }
+ }
+
+ edgeCtx.putImageData(edgeImageData, 0, 0);
+ }
+
+ render_overlay(ctx) {
+ // Draw current drawing path
+ if (this.isDrawing && this.points.length > 1) {
+ ctx.save();
+
+ var scaleX = config.layer.width / config.layer.width_original;
+ var scaleY = config.layer.height / config.layer.height_original;
+
+ ctx.strokeStyle = '#00ffff';
+ ctx.lineWidth = 2 / config.ZOOM;
+ ctx.setLineDash([5, 5]);
+ ctx.lineDashOffset = -this.marchingAntsOffset;
+
+ ctx.beginPath();
+ ctx.moveTo(
+ config.layer.x + this.points[0].x * scaleX,
+ config.layer.y + this.points[0].y * scaleY
+ );
+ for (var i = 1; i < this.points.length; i++) {
+ ctx.lineTo(
+ config.layer.x + this.points[i].x * scaleX,
+ config.layer.y + this.points[i].y * scaleY
+ );
+ }
+ ctx.stroke();
+
+ ctx.restore();
+ }
+
+ // Draw existing selection
+ if (!this.currentMask || !this.maskCanvas) return;
+
+ ctx.save();
+
+ // Draw semi-transparent overlay
+ var inverseCanvas = document.createElement('canvas');
+ inverseCanvas.width = this.maskCanvas.width;
+ inverseCanvas.height = this.maskCanvas.height;
+ var inverseCtx = inverseCanvas.getContext('2d');
+
+ inverseCtx.fillStyle = 'rgba(0, 0, 0, 0.4)';
+ inverseCtx.fillRect(0, 0, inverseCanvas.width, inverseCanvas.height);
+
+ inverseCtx.globalCompositeOperation = 'destination-out';
+ inverseCtx.drawImage(this.maskCanvas, 0, 0);
+
+ ctx.drawImage(
+ inverseCanvas,
+ config.layer.x, config.layer.y,
+ config.layer.width, config.layer.height
+ );
+
+ // Draw marching ants
+ if (this.edgeCanvas) {
+ var antsCanvas = document.createElement('canvas');
+ antsCanvas.width = this.maskCanvas.width;
+ antsCanvas.height = this.maskCanvas.height;
+ var antsCtx = antsCanvas.getContext('2d');
+
+ antsCtx.drawImage(this.edgeCanvas, 0, 0);
+ antsCtx.globalCompositeOperation = 'source-in';
+
+ var color = ((Math.floor(this.marchingAntsOffset / 4) % 2) === 0) ? '#00ffff' : '#ffffff';
+ antsCtx.fillStyle = color;
+ antsCtx.fillRect(0, 0, antsCanvas.width, antsCanvas.height);
+
+ ctx.drawImage(
+ antsCanvas,
+ config.layer.x, config.layer.y,
+ config.layer.width, config.layer.height
+ );
+ }
+
+ ctx.restore();
+ }
+
+ copyToLayer() {
+ if (!this.currentMask || !this.maskCanvas) {
+ alertify.error('No selection to copy');
+ return;
+ }
+
+ var layer = config.layer;
+ if (layer.type != 'image') {
+ alertify.error('Layer must be an image');
+ return;
+ }
+
+ var bounds = this.selectionBounds;
+ if (!bounds || bounds.origMinX === undefined) {
+ alertify.error('Invalid selection bounds');
+ return;
+ }
+
+ var canvas = document.createElement('canvas');
+ canvas.width = layer.width_original;
+ canvas.height = layer.height_original;
+ var ctx = canvas.getContext('2d');
+
+ ctx.drawImage(layer.link, 0, 0);
+ ctx.globalCompositeOperation = 'destination-in';
+ ctx.drawImage(this.maskCanvas, 0, 0);
+
+ var cropWidth = bounds.origMaxX - bounds.origMinX;
+ var cropHeight = bounds.origMaxY - bounds.origMinY;
+
+ if (cropWidth <= 0 || cropHeight <= 0) {
+ alertify.error('Selection is too small');
+ return;
+ }
+
+ var croppedCanvas = document.createElement('canvas');
+ croppedCanvas.width = cropWidth;
+ croppedCanvas.height = cropHeight;
+ var croppedCtx = croppedCanvas.getContext('2d');
+
+ croppedCtx.drawImage(
+ canvas,
+ bounds.origMinX, bounds.origMinY, cropWidth, cropHeight,
+ 0, 0, cropWidth, cropHeight
+ );
+
+ var scaleX = layer.width / layer.width_original;
+ var scaleY = layer.height / layer.height_original;
+
+ var params = {
+ x: Math.round(layer.x + bounds.origMinX * scaleX),
+ y: Math.round(layer.y + bounds.origMinY * scaleY),
+ width: cropWidth,
+ height: cropHeight,
+ width_original: cropWidth,
+ height_original: cropHeight,
+ type: 'image',
+ name: 'Lasso Selection',
+ data: croppedCanvas.toDataURL('image/png')
+ };
+
+ app.State.do_action(
+ new app.Actions.Bundle_action('copy_selection_to_layer', 'Copy Selection to Layer', [
+ new app.Actions.Insert_layer_action(params)
+ ])
+ );
+
+ alertify.success('Selection copied to new layer!');
+ }
+
+ cutToLayer() {
+ if (!this.currentMask || !this.maskCanvas) {
+ alertify.error('No selection to cut');
+ return;
+ }
+
+ var layer = config.layer;
+ if (layer.type != 'image') {
+ alertify.error('Layer must be an image');
+ return;
+ }
+
+ var bounds = this.selectionBounds;
+ if (!bounds || bounds.origMinX === undefined) {
+ alertify.error('Invalid selection bounds');
+ return;
+ }
+
+ var canvas = document.createElement('canvas');
+ canvas.width = layer.width_original;
+ canvas.height = layer.height_original;
+ var ctx = canvas.getContext('2d');
+
+ ctx.drawImage(layer.link, 0, 0);
+ ctx.globalCompositeOperation = 'destination-in';
+ ctx.drawImage(this.maskCanvas, 0, 0);
+
+ var cropWidth = bounds.origMaxX - bounds.origMinX;
+ var cropHeight = bounds.origMaxY - bounds.origMinY;
+
+ if (cropWidth <= 0 || cropHeight <= 0) {
+ alertify.error('Selection is too small');
+ return;
+ }
+
+ var croppedCanvas = document.createElement('canvas');
+ croppedCanvas.width = cropWidth;
+ croppedCanvas.height = cropHeight;
+ var croppedCtx = croppedCanvas.getContext('2d');
+
+ croppedCtx.drawImage(
+ canvas,
+ bounds.origMinX, bounds.origMinY, cropWidth, cropHeight,
+ 0, 0, cropWidth, cropHeight
+ );
+
+ var scaleX = layer.width / layer.width_original;
+ var scaleY = layer.height / layer.height_original;
+
+ var params = {
+ x: Math.round(layer.x + bounds.origMinX * scaleX),
+ y: Math.round(layer.y + bounds.origMinY * scaleY),
+ width: cropWidth,
+ height: cropHeight,
+ width_original: cropWidth,
+ height_original: cropHeight,
+ type: 'image',
+ name: 'Lasso Cut',
+ data: croppedCanvas.toDataURL('image/png')
+ };
+
+ var holeCanvas = document.createElement('canvas');
+ holeCanvas.width = layer.width_original;
+ holeCanvas.height = layer.height_original;
+ var holeCtx = holeCanvas.getContext('2d');
+
+ holeCtx.drawImage(layer.link, 0, 0);
+ holeCtx.globalCompositeOperation = 'destination-out';
+ holeCtx.drawImage(this.maskCanvas, 0, 0);
+
+ app.State.do_action(
+ new app.Actions.Bundle_action('cut_selection_to_layer', 'Cut Selection to Layer', [
+ new app.Actions.Update_layer_image_action(holeCanvas, layer.id),
+ new app.Actions.Insert_layer_action(params)
+ ])
+ );
+
+ this.clearSelection();
+ alertify.success('Selection cut to new layer!');
+ }
+
+ deleteSelection() {
+ if (!this.currentMask || !this.maskCanvas) {
+ alertify.error('No selection to delete');
+ return;
+ }
+
+ var layer = config.layer;
+ if (layer.type != 'image') {
+ alertify.error('Layer must be an image');
+ return;
+ }
+
+ var holeCanvas = document.createElement('canvas');
+ holeCanvas.width = layer.width_original;
+ holeCanvas.height = layer.height_original;
+ var holeCtx = holeCanvas.getContext('2d');
+
+ holeCtx.drawImage(layer.link, 0, 0);
+ holeCtx.globalCompositeOperation = 'destination-out';
+ holeCtx.drawImage(this.maskCanvas, 0, 0);
+
+ app.State.do_action(
+ new app.Actions.Bundle_action('delete_selection', 'Delete Selection', [
+ new app.Actions.Update_layer_image_action(holeCanvas, layer.id)
+ ])
+ );
+
+ this.clearSelection();
+ alertify.success('Selection deleted!');
+ }
+
+ clearSelection() {
+ this.currentMask = null;
+ this.maskCanvas = null;
+ this.edgeCanvas = null;
+ this.selectionBounds = null;
+ this.points = [];
+ window.smartSelectMask = null;
+ config.need_render = true;
+ this.Base_layers.render();
+ }
+
+ on_leave() {
+ this.points = [];
+ this.isDrawing = false;
+ return [];
+ }
+}
+
+export default Lasso_class;
diff --git a/frontend/src/js/tools/magic_wand.js b/frontend/src/js/tools/magic_wand.js
new file mode 100644
index 0000000..24021bb
--- /dev/null
+++ b/frontend/src/js/tools/magic_wand.js
@@ -0,0 +1,600 @@
+/**
+ * Magic Wand Selection Tool - Selects areas by color similarity
+ * Click to select similar colors, Shift+Click to add to selection
+ * Like GIMP's magic wand / fuzzy select tool
+ */
+
+import app from './../app.js';
+import config from './../config.js';
+import Base_tools_class from './../core/base-tools.js';
+import Base_layers_class from './../core/base-layers.js';
+import Helper_class from './../libs/helpers.js';
+import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js';
+
+class Magic_wand_class extends Base_tools_class {
+
+ constructor(ctx) {
+ super();
+ this.Base_layers = new Base_layers_class();
+ this.Helper = new Helper_class();
+ this.ctx = ctx;
+ this.name = 'magic_wand';
+
+ // Store the current mask data
+ this.currentMask = null;
+ this.maskCanvas = null;
+ this.selectionBounds = null;
+
+ // Marching ants animation
+ this.marchingAntsOffset = 0;
+
+ // Edge canvas for drawing the mask outline
+ this.edgeCanvas = null;
+ }
+
+ load() {
+ var _this = this;
+
+ // Mouse click event for selection
+ document.addEventListener('mousedown', function (e) {
+ _this.mousedown(e);
+ });
+
+ // Keyboard shortcuts
+ document.addEventListener('keydown', function(e) {
+ if (config.TOOL.name != _this.name) return;
+ if (_this.Helper.is_input(e.target)) return;
+
+ var code = e.keyCode;
+
+ // Delete - delete selected area
+ if (code == 46 && _this.currentMask) {
+ e.preventDefault();
+ _this.deleteSelection();
+ }
+ // Escape - clear selection
+ if (code == 27 && _this.currentMask) {
+ e.preventDefault();
+ _this.clearSelection();
+ }
+ // Ctrl+C - copy to new layer
+ if (code == 67 && (e.ctrlKey || e.metaKey) && _this.currentMask) {
+ e.preventDefault();
+ _this.copyToLayer();
+ }
+ // Ctrl+X - cut to new layer
+ if (code == 88 && (e.ctrlKey || e.metaKey) && _this.currentMask) {
+ e.preventDefault();
+ _this.cutToLayer();
+ }
+ });
+
+ // Start marching ants animation
+ this.startMarchingAnts();
+ }
+
+ startMarchingAnts() {
+ var _this = this;
+
+ setInterval(function() {
+ if (_this.currentMask) {
+ _this.marchingAntsOffset++;
+ if (_this.marchingAntsOffset > 16) {
+ _this.marchingAntsOffset = 0;
+ }
+ config.need_render = true;
+ }
+ }, 100);
+ }
+
+ mousedown(e) {
+ var mouse = this.get_mouse_info(e);
+
+ if (config.TOOL.name != this.name) return;
+ if (mouse.click_valid == false) return;
+
+ // Check if we have an image layer
+ if (config.layer.type != 'image') {
+ alertify.error('Please select an image layer first');
+ return;
+ }
+
+ // Get click coordinates relative to the image
+ var x = mouse.x - config.layer.x;
+ var y = mouse.y - config.layer.y;
+
+ // Adjust for layer scaling
+ if (config.layer.width != config.layer.width_original) {
+ x = x * (config.layer.width_original / config.layer.width);
+ }
+ if (config.layer.height != config.layer.height_original) {
+ y = y * (config.layer.height_original / config.layer.height);
+ }
+
+ x = Math.round(x);
+ y = Math.round(y);
+
+ // Make sure click is within image bounds
+ if (x < 0 || y < 0 || x >= config.layer.width_original || y >= config.layer.height_original) {
+ alertify.error('Click inside the image');
+ return;
+ }
+
+ // Check for Shift key - additive selection
+ var isAdditive = e.shiftKey;
+
+ // Get tool parameters
+ var params = this.getParams();
+ var tolerance = params.tolerance || 30;
+ var contiguous = params.contiguous !== false;
+
+ // Perform the selection
+ this.selectByColor(x, y, tolerance, contiguous, isAdditive);
+ }
+
+ /**
+ * Select pixels by color similarity using flood fill algorithm
+ */
+ selectByColor(startX, startY, tolerance, contiguous, isAdditive) {
+ var layer = config.layer;
+
+ // Get the layer's image data
+ var srcCanvas = document.createElement('canvas');
+ srcCanvas.width = layer.width_original;
+ srcCanvas.height = layer.height_original;
+ var srcCtx = srcCanvas.getContext('2d');
+ srcCtx.drawImage(layer.link, 0, 0);
+
+ var imageData = srcCtx.getImageData(0, 0, srcCanvas.width, srcCanvas.height);
+ var data = imageData.data;
+ var width = srcCanvas.width;
+ var height = srcCanvas.height;
+
+ // Create mask canvas
+ var newMaskCanvas = document.createElement('canvas');
+ newMaskCanvas.width = width;
+ newMaskCanvas.height = height;
+ var maskCtx = newMaskCanvas.getContext('2d');
+ var maskImageData = maskCtx.createImageData(width, height);
+ var maskData = maskImageData.data;
+
+ // Get the color at the clicked point
+ var startIdx = (startY * width + startX) * 4;
+ var targetColor = {
+ r: data[startIdx],
+ g: data[startIdx + 1],
+ b: data[startIdx + 2],
+ a: data[startIdx + 3]
+ };
+
+ // Convert tolerance to 0-255 range
+ var sens = tolerance * 255 / 100;
+
+ if (contiguous) {
+ // Flood fill - only connected pixels
+ var visited = new Uint8Array(width * height);
+ var stack = [[startX, startY]];
+ var dx = [0, -1, +1, 0];
+ var dy = [-1, 0, 0, +1];
+
+ while (stack.length > 0) {
+ var point = stack.pop();
+ var px = point[0];
+ var py = point[1];
+
+ if (px < 0 || py < 0 || px >= width || py >= height) continue;
+
+ var idx = py * width + px;
+ if (visited[idx]) continue;
+ visited[idx] = 1;
+
+ var i = idx * 4;
+
+ // Check color similarity
+ if (Math.abs(data[i] - targetColor.r) <= sens &&
+ Math.abs(data[i + 1] - targetColor.g) <= sens &&
+ Math.abs(data[i + 2] - targetColor.b) <= sens &&
+ Math.abs(data[i + 3] - targetColor.a) <= sens) {
+
+ // Add to mask (white = selected)
+ maskData[i] = 255;
+ maskData[i + 1] = 255;
+ maskData[i + 2] = 255;
+ maskData[i + 3] = 255;
+
+ // Add neighbors to stack
+ for (var d = 0; d < 4; d++) {
+ stack.push([px + dx[d], py + dy[d]]);
+ }
+ }
+ }
+ } else {
+ // Global - all matching pixels regardless of connection
+ for (var y = 0; y < height; y++) {
+ for (var x = 0; x < width; x++) {
+ var i = (y * width + x) * 4;
+
+ if (Math.abs(data[i] - targetColor.r) <= sens &&
+ Math.abs(data[i + 1] - targetColor.g) <= sens &&
+ Math.abs(data[i + 2] - targetColor.b) <= sens &&
+ Math.abs(data[i + 3] - targetColor.a) <= sens) {
+
+ maskData[i] = 255;
+ maskData[i + 1] = 255;
+ maskData[i + 2] = 255;
+ maskData[i + 3] = 255;
+ }
+ }
+ }
+ }
+
+ maskCtx.putImageData(maskImageData, 0, 0);
+
+ // If additive and we have an existing mask, combine them
+ if (isAdditive && this.maskCanvas) {
+ var combinedCanvas = document.createElement('canvas');
+ combinedCanvas.width = width;
+ combinedCanvas.height = height;
+ var combinedCtx = combinedCanvas.getContext('2d');
+
+ // Draw existing mask
+ combinedCtx.drawImage(this.maskCanvas, 0, 0);
+
+ // Add new mask
+ combinedCtx.globalCompositeOperation = 'lighter';
+ combinedCtx.drawImage(newMaskCanvas, 0, 0);
+
+ this.maskCanvas = combinedCanvas;
+ } else {
+ this.maskCanvas = newMaskCanvas;
+ }
+
+ this.currentMask = {
+ canvas: this.maskCanvas
+ };
+
+ // Store globally for other tools
+ window.smartSelectMask = this.currentMask;
+
+ // Calculate bounds and extract edge
+ this.calculateSelectionBounds();
+ this.extractContourPath();
+
+ config.need_render = true;
+ this.Base_layers.render();
+
+ if (isAdditive) {
+ alertify.success('Added to selection!');
+ } else {
+ alertify.success('Selection complete! Shift+Click to add more.');
+ }
+ }
+
+ calculateSelectionBounds() {
+ if (!this.maskCanvas) return;
+
+ var maskCtx = this.maskCanvas.getContext('2d');
+ var imageData = maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height);
+
+ var minX = this.maskCanvas.width, minY = this.maskCanvas.height;
+ var maxX = 0, maxY = 0;
+ var hasSelection = false;
+
+ for (var y = 0; y < this.maskCanvas.height; y++) {
+ for (var x = 0; x < this.maskCanvas.width; x++) {
+ var i = (y * this.maskCanvas.width + x) * 4;
+ if (imageData.data[i] > 128) {
+ hasSelection = true;
+ minX = Math.min(minX, x);
+ minY = Math.min(minY, y);
+ maxX = Math.max(maxX, x);
+ maxY = Math.max(maxY, y);
+ }
+ }
+ }
+
+ if (hasSelection && maxX > minX && maxY > minY) {
+ var scaleX = config.layer.width / config.layer.width_original;
+ var scaleY = config.layer.height / config.layer.height_original;
+
+ this.selectionBounds = {
+ x: config.layer.x + minX * scaleX,
+ y: config.layer.y + minY * scaleY,
+ width: (maxX - minX) * scaleX,
+ height: (maxY - minY) * scaleY,
+ origMinX: minX,
+ origMinY: minY,
+ origMaxX: maxX,
+ origMaxY: maxY
+ };
+ }
+ }
+
+ extractContourPath() {
+ if (!this.maskCanvas) return;
+
+ var maskCtx = this.maskCanvas.getContext('2d');
+ var imageData = maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height);
+ var width = this.maskCanvas.width;
+ var height = this.maskCanvas.height;
+ var data = imageData.data;
+
+ this.edgeCanvas = document.createElement('canvas');
+ this.edgeCanvas.width = width;
+ this.edgeCanvas.height = height;
+ var edgeCtx = this.edgeCanvas.getContext('2d');
+ var edgeImageData = edgeCtx.createImageData(width, height);
+ var edgeData = edgeImageData.data;
+
+ for (var y = 0; y < height; y++) {
+ for (var x = 0; x < width; x++) {
+ var i = (y * width + x) * 4;
+ var isMask = data[i] > 128;
+
+ if (isMask) {
+ var isEdge = false;
+
+ if (x > 0 && data[i - 4] <= 128) isEdge = true;
+ if (x < width - 1 && data[i + 4] <= 128) isEdge = true;
+ if (y > 0 && data[i - width * 4] <= 128) isEdge = true;
+ if (y < height - 1 && data[i + width * 4] <= 128) isEdge = true;
+ if (x == 0 || x == width - 1 || y == 0 || y == height - 1) isEdge = true;
+
+ if (isEdge) {
+ edgeData[i] = 255;
+ edgeData[i + 1] = 255;
+ edgeData[i + 2] = 255;
+ edgeData[i + 3] = 255;
+ }
+ }
+ }
+ }
+
+ edgeCtx.putImageData(edgeImageData, 0, 0);
+ }
+
+ render_overlay(ctx) {
+ if (!this.currentMask || !this.maskCanvas) return;
+
+ ctx.save();
+
+ // Draw semi-transparent overlay on non-selected areas
+ var inverseCanvas = document.createElement('canvas');
+ inverseCanvas.width = this.maskCanvas.width;
+ inverseCanvas.height = this.maskCanvas.height;
+ var inverseCtx = inverseCanvas.getContext('2d');
+
+ inverseCtx.fillStyle = 'rgba(0, 0, 0, 0.4)';
+ inverseCtx.fillRect(0, 0, inverseCanvas.width, inverseCanvas.height);
+
+ inverseCtx.globalCompositeOperation = 'destination-out';
+ inverseCtx.drawImage(this.maskCanvas, 0, 0);
+
+ ctx.drawImage(
+ inverseCanvas,
+ config.layer.x, config.layer.y,
+ config.layer.width, config.layer.height
+ );
+
+ // Draw marching ants
+ if (this.edgeCanvas) {
+ var antsCanvas = document.createElement('canvas');
+ antsCanvas.width = this.maskCanvas.width;
+ antsCanvas.height = this.maskCanvas.height;
+ var antsCtx = antsCanvas.getContext('2d');
+
+ antsCtx.drawImage(this.edgeCanvas, 0, 0);
+ antsCtx.globalCompositeOperation = 'source-in';
+
+ var color = ((Math.floor(this.marchingAntsOffset / 4) % 2) === 0) ? '#ff00ff' : '#ffffff';
+ antsCtx.fillStyle = color;
+ antsCtx.fillRect(0, 0, antsCanvas.width, antsCanvas.height);
+
+ ctx.drawImage(
+ antsCanvas,
+ config.layer.x, config.layer.y,
+ config.layer.width, config.layer.height
+ );
+ }
+
+ ctx.restore();
+ }
+
+ copyToLayer() {
+ if (!this.currentMask || !this.maskCanvas) {
+ alertify.error('No selection to copy');
+ return;
+ }
+
+ var layer = config.layer;
+ if (layer.type != 'image') {
+ alertify.error('Layer must be an image');
+ return;
+ }
+
+ var bounds = this.selectionBounds;
+ if (!bounds || bounds.origMinX === undefined) {
+ alertify.error('Invalid selection bounds');
+ return;
+ }
+
+ var canvas = document.createElement('canvas');
+ canvas.width = layer.width_original;
+ canvas.height = layer.height_original;
+ var ctx = canvas.getContext('2d');
+
+ ctx.drawImage(layer.link, 0, 0);
+ ctx.globalCompositeOperation = 'destination-in';
+ ctx.drawImage(this.maskCanvas, 0, 0);
+
+ var cropWidth = bounds.origMaxX - bounds.origMinX;
+ var cropHeight = bounds.origMaxY - bounds.origMinY;
+
+ if (cropWidth <= 0 || cropHeight <= 0) {
+ alertify.error('Selection is too small');
+ return;
+ }
+
+ var croppedCanvas = document.createElement('canvas');
+ croppedCanvas.width = cropWidth;
+ croppedCanvas.height = cropHeight;
+ var croppedCtx = croppedCanvas.getContext('2d');
+
+ croppedCtx.drawImage(
+ canvas,
+ bounds.origMinX, bounds.origMinY, cropWidth, cropHeight,
+ 0, 0, cropWidth, cropHeight
+ );
+
+ var scaleX = layer.width / layer.width_original;
+ var scaleY = layer.height / layer.height_original;
+
+ var params = {
+ x: Math.round(layer.x + bounds.origMinX * scaleX),
+ y: Math.round(layer.y + bounds.origMinY * scaleY),
+ width: cropWidth,
+ height: cropHeight,
+ width_original: cropWidth,
+ height_original: cropHeight,
+ type: 'image',
+ name: 'Magic Wand Selection',
+ data: croppedCanvas.toDataURL('image/png')
+ };
+
+ app.State.do_action(
+ new app.Actions.Bundle_action('copy_selection_to_layer', 'Copy Selection to Layer', [
+ new app.Actions.Insert_layer_action(params)
+ ])
+ );
+
+ alertify.success('Selection copied to new layer!');
+ }
+
+ cutToLayer() {
+ if (!this.currentMask || !this.maskCanvas) {
+ alertify.error('No selection to cut');
+ return;
+ }
+
+ var layer = config.layer;
+ if (layer.type != 'image') {
+ alertify.error('Layer must be an image');
+ return;
+ }
+
+ var bounds = this.selectionBounds;
+ if (!bounds || bounds.origMinX === undefined) {
+ alertify.error('Invalid selection bounds');
+ return;
+ }
+
+ var canvas = document.createElement('canvas');
+ canvas.width = layer.width_original;
+ canvas.height = layer.height_original;
+ var ctx = canvas.getContext('2d');
+
+ ctx.drawImage(layer.link, 0, 0);
+ ctx.globalCompositeOperation = 'destination-in';
+ ctx.drawImage(this.maskCanvas, 0, 0);
+
+ var cropWidth = bounds.origMaxX - bounds.origMinX;
+ var cropHeight = bounds.origMaxY - bounds.origMinY;
+
+ if (cropWidth <= 0 || cropHeight <= 0) {
+ alertify.error('Selection is too small');
+ return;
+ }
+
+ var croppedCanvas = document.createElement('canvas');
+ croppedCanvas.width = cropWidth;
+ croppedCanvas.height = cropHeight;
+ var croppedCtx = croppedCanvas.getContext('2d');
+
+ croppedCtx.drawImage(
+ canvas,
+ bounds.origMinX, bounds.origMinY, cropWidth, cropHeight,
+ 0, 0, cropWidth, cropHeight
+ );
+
+ var scaleX = layer.width / layer.width_original;
+ var scaleY = layer.height / layer.height_original;
+
+ var params = {
+ x: Math.round(layer.x + bounds.origMinX * scaleX),
+ y: Math.round(layer.y + bounds.origMinY * scaleY),
+ width: cropWidth,
+ height: cropHeight,
+ width_original: cropWidth,
+ height_original: cropHeight,
+ type: 'image',
+ name: 'Magic Wand Cut',
+ data: croppedCanvas.toDataURL('image/png')
+ };
+
+ var holeCanvas = document.createElement('canvas');
+ holeCanvas.width = layer.width_original;
+ holeCanvas.height = layer.height_original;
+ var holeCtx = holeCanvas.getContext('2d');
+
+ holeCtx.drawImage(layer.link, 0, 0);
+ holeCtx.globalCompositeOperation = 'destination-out';
+ holeCtx.drawImage(this.maskCanvas, 0, 0);
+
+ app.State.do_action(
+ new app.Actions.Bundle_action('cut_selection_to_layer', 'Cut Selection to Layer', [
+ new app.Actions.Update_layer_image_action(holeCanvas, layer.id),
+ new app.Actions.Insert_layer_action(params)
+ ])
+ );
+
+ this.clearSelection();
+ alertify.success('Selection cut to new layer!');
+ }
+
+ deleteSelection() {
+ if (!this.currentMask || !this.maskCanvas) {
+ alertify.error('No selection to delete');
+ return;
+ }
+
+ var layer = config.layer;
+ if (layer.type != 'image') {
+ alertify.error('Layer must be an image');
+ return;
+ }
+
+ var holeCanvas = document.createElement('canvas');
+ holeCanvas.width = layer.width_original;
+ holeCanvas.height = layer.height_original;
+ var holeCtx = holeCanvas.getContext('2d');
+
+ holeCtx.drawImage(layer.link, 0, 0);
+ holeCtx.globalCompositeOperation = 'destination-out';
+ holeCtx.drawImage(this.maskCanvas, 0, 0);
+
+ app.State.do_action(
+ new app.Actions.Bundle_action('delete_selection', 'Delete Selection', [
+ new app.Actions.Update_layer_image_action(holeCanvas, layer.id)
+ ])
+ );
+
+ this.clearSelection();
+ alertify.success('Selection deleted!');
+ }
+
+ clearSelection() {
+ this.currentMask = null;
+ this.maskCanvas = null;
+ this.edgeCanvas = null;
+ this.selectionBounds = null;
+ window.smartSelectMask = null;
+ config.need_render = true;
+ this.Base_layers.render();
+ }
+
+ on_leave() {
+ return [];
+ }
+}
+
+export default Magic_wand_class;
diff --git a/frontend/src/js/tools/smart_select.js b/frontend/src/js/tools/smart_select.js
index 0e80c3e..f25c2ab 100644
--- a/frontend/src/js/tools/smart_select.js
+++ b/frontend/src/js/tools/smart_select.js
@@ -1,6 +1,7 @@
/**
* Smart Select Tool - Uses SAM (Segment Anything Model) for AI-powered selection
* Click on any object to automatically select it
+ * Shift+Click to add to existing selection (multi-select)
* Supports: Copy to layer, Cut to layer, Delete selection, AI Inpaint
*/
@@ -31,10 +32,9 @@ class Smart_select_class extends Base_tools_class {
// Marching ants animation
this.marchingAntsOffset = 0;
- this.animationFrame = null;
- // Contour path for drawing the mask outline
- this.contourPath = null;
+ // Edge canvas for drawing the mask outline
+ this.edgeCanvas = null;
}
load() {
@@ -81,18 +81,7 @@ class Smart_select_class extends Base_tools_class {
startMarchingAnts() {
var _this = this;
- var animate = function() {
- _this.marchingAntsOffset++;
- if (_this.marchingAntsOffset > 16) {
- _this.marchingAntsOffset = 0;
- }
- if (_this.currentMask) {
- config.need_render = true;
- }
- _this.animationFrame = requestAnimationFrame(animate);
- };
-
- // Slower animation - every 100ms
+ // Animate every 100ms for smooth marching ants
setInterval(function() {
if (_this.currentMask) {
_this.marchingAntsOffset++;
@@ -138,6 +127,9 @@ class Smart_select_class extends Base_tools_class {
return;
}
+ // Check for Shift key - additive selection
+ var isAdditive = e.shiftKey;
+
this.isProcessing = true;
alertify.message('AI is analyzing the image...');
@@ -148,10 +140,14 @@ class Smart_select_class extends Base_tools_class {
// Call SAM API
var result = await apiService.smartSelect(imageData, Math.round(x), Math.round(y));
- // Apply the mask as selection
- this.applyMask(result.mask, result.bbox);
+ // Apply the mask as selection (additive if Shift is held)
+ this.applyMask(result.mask, result.bbox, isAdditive);
- alertify.success('Selection complete! Use Ctrl+C to copy, Ctrl+X to cut, Delete to remove, or AI Inpaint to edit.');
+ if (isAdditive && this.currentMask) {
+ alertify.success('Added to selection! Shift+Click to add more.');
+ } else {
+ alertify.success('Selection complete! Shift+Click to add more, Ctrl+C to copy, Ctrl+X to cut.');
+ }
} catch (error) {
console.error('Smart select error:', error);
@@ -182,19 +178,39 @@ class Smart_select_class extends Base_tools_class {
* Apply the SAM mask as a selection
* @param {string} maskBase64 - Base64 encoded mask image
* @param {Object} bbox - Bounding box {x, y, width, height}
+ * @param {boolean} isAdditive - If true, add to existing selection
*/
- applyMask(maskBase64, bbox) {
+ applyMask(maskBase64, bbox, isAdditive) {
var _this = this;
// Create mask image
var maskImage = new Image();
maskImage.onload = function() {
- // Store mask for later use by inpaint tool
- _this.maskCanvas = document.createElement('canvas');
- _this.maskCanvas.width = config.layer.width_original;
- _this.maskCanvas.height = config.layer.height_original;
- var maskCtx = _this.maskCanvas.getContext('2d');
- maskCtx.drawImage(maskImage, 0, 0);
+ // Create new mask canvas
+ var newMaskCanvas = document.createElement('canvas');
+ newMaskCanvas.width = config.layer.width_original;
+ newMaskCanvas.height = config.layer.height_original;
+ var newMaskCtx = newMaskCanvas.getContext('2d');
+ newMaskCtx.drawImage(maskImage, 0, 0);
+
+ // If additive and we have an existing mask, combine them
+ if (isAdditive && _this.maskCanvas) {
+ var combinedCanvas = document.createElement('canvas');
+ combinedCanvas.width = config.layer.width_original;
+ combinedCanvas.height = config.layer.height_original;
+ var combinedCtx = combinedCanvas.getContext('2d');
+
+ // Draw existing mask
+ combinedCtx.drawImage(_this.maskCanvas, 0, 0);
+
+ // Add new mask using 'lighter' composite to combine white areas
+ combinedCtx.globalCompositeOperation = 'lighter';
+ combinedCtx.drawImage(newMaskCanvas, 0, 0);
+
+ _this.maskCanvas = combinedCanvas;
+ } else {
+ _this.maskCanvas = newMaskCanvas;
+ }
_this.currentMask = {
canvas: _this.maskCanvas,
@@ -325,10 +341,6 @@ class Smart_select_class extends Base_tools_class {
ctx.save();
- // Scale to match layer
- var scaleX = config.layer.width / config.layer.width_original;
- var scaleY = config.layer.height / config.layer.height_original;
-
// Draw semi-transparent overlay on non-selected areas
var inverseCanvas = document.createElement('canvas');
inverseCanvas.width = this.maskCanvas.width;
@@ -358,28 +370,15 @@ class Smart_select_class extends Base_tools_class {
antsCanvas.height = this.maskCanvas.height;
var antsCtx = antsCanvas.getContext('2d');
- // Draw the edge in white (visible part of marching ants)
+ // Draw the edge
antsCtx.drawImage(this.edgeCanvas, 0, 0);
- // Apply marching ants pattern using composite
+ // Apply marching ants color using composite
antsCtx.globalCompositeOperation = 'source-in';
- // Create marching ants pattern (alternating black and white)
- var pattern = antsCtx.createLinearGradient(0, 0, 16, 16);
- var offset = this.marchingAntsOffset / 16;
-
- // Create dashed pattern
- for (var i = 0; i < 2; i++) {
- var pos1 = ((i * 0.5) + offset) % 1;
- var pos2 = ((i * 0.5 + 0.25) + offset) % 1;
-
- if (pos1 < pos2) {
- pattern.addColorStop(pos1, '#00ff00');
- pattern.addColorStop(pos2, '#00ff00');
- }
- }
-
- antsCtx.fillStyle = '#00ff00';
+ // Alternate color based on animation offset
+ var color = ((Math.floor(this.marchingAntsOffset / 4) % 2) === 0) ? '#00ff00' : '#ffffff';
+ antsCtx.fillStyle = color;
antsCtx.fillRect(0, 0, antsCanvas.width, antsCanvas.height);
// Draw the marching ants outline
@@ -388,20 +387,6 @@ class Smart_select_class extends Base_tools_class {
config.layer.x, config.layer.y,
config.layer.width, config.layer.height
);
-
- // Also draw a second pass with offset for alternating colors
- var antsCanvas2 = document.createElement('canvas');
- antsCanvas2.width = this.maskCanvas.width;
- antsCanvas2.height = this.maskCanvas.height;
- var antsCtx2 = antsCanvas2.getContext('2d');
-
- // Dilate the edge slightly for the black outline
- antsCtx2.drawImage(this.edgeCanvas, 0, 0);
- antsCtx2.globalCompositeOperation = 'source-in';
-
- // Alternate pattern
- antsCtx2.fillStyle = ((Math.floor(this.marchingAntsOffset / 4) % 2) === 0) ? '#ffffff' : '#000000';
- antsCtx2.fillRect(0, 0, antsCanvas2.width, antsCanvas2.height);
}
ctx.restore();
@@ -422,6 +407,13 @@ class Smart_select_class extends Base_tools_class {
return;
}
+ // Get the bounds of the selection
+ var bounds = this.selectionBounds;
+ if (!bounds || !bounds.origMinX === undefined) {
+ alertify.error('Invalid selection bounds');
+ return;
+ }
+
// Create canvas with just the selected pixels
var canvas = document.createElement('canvas');
canvas.width = layer.width_original;
@@ -435,17 +427,16 @@ class Smart_select_class extends Base_tools_class {
ctx.globalCompositeOperation = 'destination-in';
ctx.drawImage(this.maskCanvas, 0, 0);
- // Get the bounds of the selection to crop the canvas
- var bounds = this.selectionBounds;
- if (!bounds) {
- alertify.error('Invalid selection bounds');
+ // Crop to selection bounds
+ var cropWidth = bounds.origMaxX - bounds.origMinX;
+ var cropHeight = bounds.origMaxY - bounds.origMinY;
+
+ if (cropWidth <= 0 || cropHeight <= 0) {
+ alertify.error('Selection is too small');
return;
}
- // Crop to selection bounds
var croppedCanvas = document.createElement('canvas');
- var cropWidth = bounds.origMaxX - bounds.origMinX;
- var cropHeight = bounds.origMaxY - bounds.origMinY;
croppedCanvas.width = cropWidth;
croppedCanvas.height = cropHeight;
var croppedCtx = croppedCanvas.getContext('2d');
@@ -460,12 +451,12 @@ class Smart_select_class extends Base_tools_class {
var scaleX = layer.width / layer.width_original;
var scaleY = layer.height / layer.height_original;
- // Create new layer with the selection
+ // Create new layer with the selection - use data as dataURL string
var params = {
x: Math.round(layer.x + bounds.origMinX * scaleX),
y: Math.round(layer.y + bounds.origMinY * scaleY),
- width: Math.round(cropWidth * scaleX),
- height: Math.round(cropHeight * scaleY),
+ width: cropWidth,
+ height: cropHeight,
width_original: cropWidth,
height_original: cropHeight,
type: 'image',
@@ -475,7 +466,7 @@ class Smart_select_class extends Base_tools_class {
app.State.do_action(
new app.Actions.Bundle_action('copy_selection_to_layer', 'Copy Selection to Layer', [
- new app.Actions.Insert_layer_action(params, false)
+ new app.Actions.Insert_layer_action(params)
])
);
@@ -497,7 +488,14 @@ class Smart_select_class extends Base_tools_class {
return;
}
- // First copy to new layer
+ // Get the bounds of the selection
+ var bounds = this.selectionBounds;
+ if (!bounds || bounds.origMinX === undefined) {
+ alertify.error('Invalid selection bounds');
+ return;
+ }
+
+ // Create canvas with just the selected pixels
var canvas = document.createElement('canvas');
canvas.width = layer.width_original;
canvas.height = layer.height_original;
@@ -510,17 +508,16 @@ class Smart_select_class extends Base_tools_class {
ctx.globalCompositeOperation = 'destination-in';
ctx.drawImage(this.maskCanvas, 0, 0);
- // Get the bounds of the selection
- var bounds = this.selectionBounds;
- if (!bounds) {
- alertify.error('Invalid selection bounds');
+ // Crop to selection bounds
+ var cropWidth = bounds.origMaxX - bounds.origMinX;
+ var cropHeight = bounds.origMaxY - bounds.origMinY;
+
+ if (cropWidth <= 0 || cropHeight <= 0) {
+ alertify.error('Selection is too small');
return;
}
- // Crop to selection bounds
var croppedCanvas = document.createElement('canvas');
- var cropWidth = bounds.origMaxX - bounds.origMinX;
- var cropHeight = bounds.origMaxY - bounds.origMinY;
croppedCanvas.width = cropWidth;
croppedCanvas.height = cropHeight;
var croppedCtx = croppedCanvas.getContext('2d');
@@ -539,8 +536,8 @@ class Smart_select_class extends Base_tools_class {
var params = {
x: Math.round(layer.x + bounds.origMinX * scaleX),
y: Math.round(layer.y + bounds.origMinY * scaleY),
- width: Math.round(cropWidth * scaleX),
- height: Math.round(cropHeight * scaleY),
+ width: cropWidth,
+ height: cropHeight,
width_original: cropWidth,
height_original: cropHeight,
type: 'image',
@@ -548,7 +545,7 @@ class Smart_select_class extends Base_tools_class {
data: croppedCanvas.toDataURL('image/png')
};
- // Now delete from original - create canvas with hole
+ // Create canvas with hole where selection was
var holeCanvas = document.createElement('canvas');
holeCanvas.width = layer.width_original;
holeCanvas.height = layer.height_original;
@@ -561,11 +558,11 @@ class Smart_select_class extends Base_tools_class {
holeCtx.globalCompositeOperation = 'destination-out';
holeCtx.drawImage(this.maskCanvas, 0, 0);
- // Execute both actions
+ // Execute both actions - update original layer, then insert new layer
app.State.do_action(
new app.Actions.Bundle_action('cut_selection_to_layer', 'Cut Selection to Layer', [
- new app.Actions.Update_layer_image_action(holeCanvas),
- new app.Actions.Insert_layer_action(params, false)
+ new app.Actions.Update_layer_image_action(holeCanvas, layer.id),
+ new app.Actions.Insert_layer_action(params)
])
);
@@ -605,7 +602,7 @@ class Smart_select_class extends Base_tools_class {
app.State.do_action(
new app.Actions.Bundle_action('delete_selection', 'Delete Selection', [
- new app.Actions.Update_layer_image_action(holeCanvas)
+ new app.Actions.Update_layer_image_action(holeCanvas, layer.id)
])
);
@@ -623,7 +620,6 @@ class Smart_select_class extends Base_tools_class {
this.maskCanvas = null;
this.edgeCanvas = null;
this.selectionBounds = null;
- this.contourPath = null;
window.smartSelectMask = null;
config.need_render = true;
this.Base_layers.render();