From e24a0e09ab5580c8e1790d0c1b7bd2c4ddec49e6 Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Fri, 2 Jun 2023 10:34:26 -0400 Subject: [PATCH 1/2] Enable selecting multiple rectangles --- source/layer.js | 2 +- source/pixel.js | 44 ++++++------ source/selection.js | 162 ++++++++++++++++++++++++-------------------- 3 files changed, 110 insertions(+), 98 deletions(-) diff --git a/source/layer.js b/source/layer.js index 6206bee9..4c83a100 100644 --- a/source/layer.js +++ b/source/layer.js @@ -267,7 +267,7 @@ export class Layer this.actions.push(action); // Selection is temporary and only concerns this layer thus no need to add to global actions - if (!(action.object.type === "selection" && action.object.selectedShape.blendMode === "select")) + if (!(action.object.type === "selection" && action.object.selectedShapes[0].blendMode === "select")) this.pixelInstance.actions.push(action); } diff --git a/source/pixel.js b/source/pixel.js index 61496c06..29fecef2 100644 --- a/source/pixel.js +++ b/source/pixel.js @@ -51,7 +51,7 @@ export default class PixelPlugin this.lastRelCoordY = null; this.uiManager = null; this.tools = null; - this.selection = null; + this.selection = new Selection(); this.horizontalMove = false; this.layerIdCounter = 2; this.editingLayerName = false; @@ -412,12 +412,9 @@ export default class PixelPlugin } break; case "escape": - if (this.selection !== null) + if (this.selection.imageDataList.length > 0) { - if (this.selection.imageData === null) - { - this.selection.clearSelection(this.core.getSettings().maxZoomLevel); - } + this.selection.clearSelection(this.core.getSettings().maxZoomLevel); } break; case "backspace": @@ -450,24 +447,24 @@ export default class PixelPlugin this.undoAction(); break; case "c": - if (e.ctrlKey || e.metaKey) // Cmd + c + if (e.ctrlKey || e.metaKey) { // Cmd + c this.selection.copyShape(this.core.getSettings().maxZoomLevel); + } break; case "x": - if (e.ctrlKey || e.metaKey) // Cmd + x + if (e.ctrlKey || e.metaKey) { // Cmd + x this.selection.cutShape(this.core.getSettings().maxZoomLevel); + } break; case "v": if (e.ctrlKey || e.metaKey) // Cmd + v { - if (this.selection !== null) + if (this.selection.imageDataList.length > 0) { - if (this.selection.imageData !== null) - { - this.selection.pasteShapeToLayer(this.layers[this.selectedLayerIndex]); - this.redrawLayer(this.layers[this.selectedLayerIndex]); - } + this.selection.pasteShapeToLayer(this.layers[this.selectedLayerIndex]); + this.redrawLayer(this.layers[this.selectedLayerIndex]); } + this.selection = new Selection(); } break; case "shift": @@ -578,8 +575,10 @@ export default class PixelPlugin mousePos = this.getMousePos(mouseClickDiv, evt); // Clear Selection - if (this.selection !== null) + if (!evt.ctrlKey) { this.selection.clearSelection(this.core.getSettings().maxZoomLevel); + this.selection = new Selection(); + } if (evt.which === 1) this.rightMousePressed = false; @@ -608,7 +607,6 @@ export default class PixelPlugin this.initializeNewPathInCurrentLayer(mousePos); break; case this.tools.type.select: - this.selection = new Selection(); this.initializeRectanglePreview(mousePos); break; default: @@ -825,6 +823,10 @@ export default class PixelPlugin break; case "shape": action.layer.removeShapeFromLayer(action.object); + // If we are undoing a selection preview, we need to remove the shape from the selection + if (action.object.blendMode === "select" || action.object.blendMode === "subtract") { + this.selection.removeSelectedShape(action.object); + } break; case "selection": action.layer.removeSelectionFromLayer(action.object); @@ -1006,7 +1008,8 @@ export default class PixelPlugin { case this.tools.type.select: selectedLayer.addShapeToLayer(new Rectangle(new Point(relativeCoords.x,relativeCoords.y,pageIndex), 0, 0, "select", this.tools.getCurrentTool())); - this.selection.setSelectedShape(selectedLayer.getCurrentShape(), this.layers[this.selectedLayerIndex]); + this.selection.setLayer(this.layers[this.selectedLayerIndex]); + this.selection.addSelectedShape(selectedLayer.getCurrentShape()); break; case this.tools.type.rectangle: if (this.rightMousePressed) @@ -1137,12 +1140,9 @@ export default class PixelPlugin changeCurrentlySelectedLayerIndex (newIndex) { this.selectedLayerIndex = newIndex; - if (this.selection !== null) + if (this.selection.imageDataList.length > 0) { - if (this.selection.imageData === null) - { - this.selection.clearSelection(this.core.getSettings().maxZoomLevel); - } + this.selection.clearSelection(this.core.getSettings().maxZoomLevel); } } diff --git a/source/selection.js b/source/selection.js index ed5edbba..b6ffc297 100644 --- a/source/selection.js +++ b/source/selection.js @@ -1,62 +1,64 @@ /*jshint esversion: 6 */ -export class Selection -{ - constructor () - { +export class Selection { + constructor() { this.layer = null; - this.selectedShape = null; + this.selectedShapes = []; this.type = "selection"; - this.imageData = null; + this.imageDataList = []; } - copyShape (maxZoomLevel) - { - this.layer.removeShapeFromLayer(this.selectedShape); - this.layer.drawLayer(maxZoomLevel, this.layer.getCanvas()); + copyShape(maxZoomLevel) { + this.imageDataList = []; + this.selectedShapes.forEach((shape) => { + this.layer.removeShapeFromLayer(shape); + this.layer.drawLayer(maxZoomLevel, this.layer.getCanvas()); - let scaleRatio = Math.pow(2, maxZoomLevel); + let scaleRatio = Math.pow(2, maxZoomLevel); - // Get coordinates of the selection shape (a rectangle here) - let absoluteRectOriginX = this.selectedShape.origin.relativeOriginX * scaleRatio, - absoluteRectOriginY = this.selectedShape.origin.relativeOriginY * scaleRatio, - absoluteRectWidth = this.selectedShape.relativeRectWidth * scaleRatio, - absoluteRectHeight = this.selectedShape.relativeRectHeight * scaleRatio; + // Get coordinates of the selection shape (a rectangle here) + let absoluteRectOriginX = shape.origin.relativeOriginX * scaleRatio, + absoluteRectOriginY = shape.origin.relativeOriginY * scaleRatio, + absoluteRectWidth = shape.relativeRectWidth * scaleRatio, + absoluteRectHeight = shape.relativeRectHeight * scaleRatio; - let xmin = Math.min(absoluteRectOriginX, absoluteRectOriginX + absoluteRectWidth), - ymin = Math.min(absoluteRectOriginY, absoluteRectOriginY + absoluteRectHeight); + let xmin = Math.min(absoluteRectOriginX, absoluteRectOriginX + absoluteRectWidth), + ymin = Math.min(absoluteRectOriginY, absoluteRectOriginY + absoluteRectHeight); - let selectedLayerCtx = this.layer.getCanvas().getContext("2d"); - let imageData = selectedLayerCtx.getImageData(xmin, ymin, Math.abs(absoluteRectWidth), Math.abs(absoluteRectHeight)); + let selectedLayerCtx = this.layer.getCanvas().getContext("2d"); + let imageData = selectedLayerCtx.getImageData(xmin, ymin, Math.abs(absoluteRectWidth), Math.abs(absoluteRectHeight)); - this.imageData = imageData; + this.imageDataList.push(imageData); - this.selectedShape.changeBlendModeTo("add"); + shape.changeBlendModeTo("add"); + }); } - cutShape (maxZoomLevel) - { - this.layer.removeShapeFromLayer(this.selectedShape); - this.layer.drawLayer(maxZoomLevel, this.layer.getCanvas()); + cutShape(maxZoomLevel) { + this.imageDataList = []; + this.selectedShapes.forEach((shape) => { + this.layer.removeShapeFromLayer(shape); + this.layer.drawLayer(maxZoomLevel, this.layer.getCanvas()); - let scaleRatio = Math.pow(2, maxZoomLevel); + let scaleRatio = Math.pow(2, maxZoomLevel); - // Get coordinates of the selection shape (a rectangle here) - let absoluteRectOriginX = this.selectedShape.origin.relativeOriginX * scaleRatio, - absoluteRectOriginY = this.selectedShape.origin.relativeOriginY * scaleRatio, - absoluteRectWidth = this.selectedShape.relativeRectWidth * scaleRatio, - absoluteRectHeight = this.selectedShape.relativeRectHeight * scaleRatio; + // Get coordinates of the selection shape (a rectangle here) + let absoluteRectOriginX = shape.origin.relativeOriginX * scaleRatio, + absoluteRectOriginY = shape.origin.relativeOriginY * scaleRatio, + absoluteRectWidth = shape.relativeRectWidth * scaleRatio, + absoluteRectHeight = shape.relativeRectHeight * scaleRatio; - let xmin = Math.min(absoluteRectOriginX, absoluteRectOriginX + absoluteRectWidth), - ymin = Math.min(absoluteRectOriginY, absoluteRectOriginY + absoluteRectHeight); + let xmin = Math.min(absoluteRectOriginX, absoluteRectOriginX + absoluteRectWidth), + ymin = Math.min(absoluteRectOriginY, absoluteRectOriginY + absoluteRectHeight); - let selectedLayerCtx = this.layer.getCanvas().getContext("2d"); - let imageData = selectedLayerCtx.getImageData(xmin, ymin, Math.abs(absoluteRectWidth), Math.abs(absoluteRectHeight)); + let selectedLayerCtx = this.layer.getCanvas().getContext("2d"); + let imageData = selectedLayerCtx.getImageData(xmin, ymin, Math.abs(absoluteRectWidth), Math.abs(absoluteRectHeight)); - this.imageData = imageData; + this.imageDataList.push(imageData); - this.selectedShape.changeBlendModeTo("subtract"); - this.layer.addShapeToLayer(this.selectedShape); - this.layer.drawLayer(maxZoomLevel, this.layer.getCanvas()); + shape.changeBlendModeTo("subtract"); + this.layer.addShapeToLayer(shape); + this.layer.drawLayer(maxZoomLevel, this.layer.getCanvas()); + }); } /** @@ -64,55 +66,65 @@ export class Selection * @param layerToPasteTo * @param maxZoomLevel */ - pasteShapeToLayer (layerToPasteTo) - { - let data = this.imageData.data; - - // Change imageData colour to layer's colour - for (let i = 0; i < data.length; i += 4) - { - data[i] = layerToPasteTo.colour.red; // red - data[i + 1] = layerToPasteTo.colour.green; // green - data[i + 2] = layerToPasteTo.colour.blue; // blue - } + pasteShapeToLayer(layerToPasteTo) { + this.imageDataList.forEach(({ data }) => { + // Change imageData colour to layer's colour + for (let i = 0; i < data.length; i += 4) { + data[i] = layerToPasteTo.colour.red; // red + data[i + 1] = layerToPasteTo.colour.green; // green + data[i + 2] = layerToPasteTo.colour.blue; // blue + } + }); + layerToPasteTo.addToPastedRegions(this); } - setSelectedShape (selectedShape, selectedLayer) - { - this.selectedShape = selectedShape; - this.layer = selectedLayer; + setLayer(layer) { + this.layer = layer; + } + + addSelectedShape(shape) { + this.selectedShapes.push(shape); + } + + removeSelectedShape(shape) { + const index = this.selectedShapes.indexOf(shape); + if (index > -1) { + this.selectedShapes.splice(index, 1); + } } - clearSelection (maxZoomLevel) - { - if (this.layer !== null && this.selectedShape !== null) - { - if (this.selectedShape.blendMode === "select") - this.layer.removeShapeFromLayer(this.selectedShape); + clearSelection(maxZoomLevel) { + if (this.layer !== null) { + this.selectedShapes.forEach((shape) => { + if (shape.blendMode === "select") { + this.layer.removeShapeFromLayer(shape); + } + }); this.layer.drawLayer(maxZoomLevel, this.layer.getCanvas()); } } - drawOnPage (layer, pageIndex, zoomLevel, renderer, canvas) - { + drawOnPage(layer, pageIndex, zoomLevel, renderer, canvas) { let scaleRatio = Math.pow(2, zoomLevel); - // Get coordinates of the selection shape (a rectangle here) - let absoluteRectOriginX = this.selectedShape.origin.relativeOriginX * scaleRatio, - absoluteRectOriginY = this.selectedShape.origin.relativeOriginY * scaleRatio, - absoluteRectWidth = this.selectedShape.relativeRectWidth * scaleRatio, - absoluteRectHeight = this.selectedShape.relativeRectHeight * scaleRatio; + this.selectedShapes.forEach((shape, index) => { + // Get coordinates of the selection shape (a rectangle here) + let absoluteRectOriginX = shape.origin.relativeOriginX * scaleRatio, + absoluteRectOriginY = shape.origin.relativeOriginY * scaleRatio, + absoluteRectWidth = shape.relativeRectWidth * scaleRatio, + absoluteRectHeight = shape.relativeRectHeight * scaleRatio; - let xmin = Math.min(absoluteRectOriginX, absoluteRectOriginX + absoluteRectWidth); - let ymin = Math.min(absoluteRectOriginY, absoluteRectOriginY + absoluteRectHeight); + let xmin = Math.min(absoluteRectOriginX, absoluteRectOriginX + absoluteRectWidth); + let ymin = Math.min(absoluteRectOriginY, absoluteRectOriginY + absoluteRectHeight); - let pasteCanvas = document.createElement("canvas"); - pasteCanvas.width = Math.abs(absoluteRectWidth); - pasteCanvas.height = Math.abs(absoluteRectHeight); + let pasteCanvas = document.createElement("canvas"); + pasteCanvas.width = Math.abs(absoluteRectWidth); + pasteCanvas.height = Math.abs(absoluteRectHeight); - pasteCanvas.getContext("2d").putImageData(this.imageData, 0, 0); + pasteCanvas.getContext("2d").putImageData(this.imageDataList[index], 0, 0); - canvas.getContext("2d").drawImage(pasteCanvas, xmin, ymin); + canvas.getContext("2d").drawImage(pasteCanvas, xmin, ymin); + }); } } From 83383e83e4535d712d123f301155cb981550df86 Mon Sep 17 00:00:00 2001 From: Jacky Zhang Date: Mon, 5 Jun 2023 09:11:42 -0400 Subject: [PATCH 2/2] Undo multiselect cut and paste works properly --- source/compound-shape.js | 12 ++++++ source/pixel.js | 4 +- source/selection.js | 92 +++++++++++++++++----------------------- source/shape.js | 8 ---- 4 files changed, 54 insertions(+), 62 deletions(-) create mode 100644 source/compound-shape.js diff --git a/source/compound-shape.js b/source/compound-shape.js new file mode 100644 index 00000000..d463fd37 --- /dev/null +++ b/source/compound-shape.js @@ -0,0 +1,12 @@ +export class CompoundShape { + constructor(...shapes) { + this.shapes = shapes; + this.type = "shape"; + } + + drawOnPage(layer, pageIndex, zoomLevel, renderer, canvas) { + this.shapes.forEach((shape) => { + shape.drawOnPage(layer, pageIndex, zoomLevel, renderer, canvas); + }); + } +} \ No newline at end of file diff --git a/source/pixel.js b/source/pixel.js index 29fecef2..bbf9180e 100644 --- a/source/pixel.js +++ b/source/pixel.js @@ -448,12 +448,12 @@ export default class PixelPlugin break; case "c": if (e.ctrlKey || e.metaKey) { // Cmd + c - this.selection.copyShape(this.core.getSettings().maxZoomLevel); + this.selection.copySelection(this.core.getSettings().maxZoomLevel); } break; case "x": if (e.ctrlKey || e.metaKey) { // Cmd + x - this.selection.cutShape(this.core.getSettings().maxZoomLevel); + this.selection.cutSelection(this.core.getSettings().maxZoomLevel); } break; case "v": diff --git a/source/selection.js b/source/selection.js index b6ffc297..e7470b89 100644 --- a/source/selection.js +++ b/source/selection.js @@ -1,64 +1,32 @@ +import { CompoundShape } from "./compound-shape"; + /*jshint esversion: 6 */ export class Selection { constructor() { this.layer = null; this.selectedShapes = []; - this.type = "selection"; this.imageDataList = []; + this.type = "selection"; } - copyShape(maxZoomLevel) { + copySelection(maxZoomLevel) { this.imageDataList = []; this.selectedShapes.forEach((shape) => { + this._copyImageData(shape, maxZoomLevel); this.layer.removeShapeFromLayer(shape); - this.layer.drawLayer(maxZoomLevel, this.layer.getCanvas()); - - let scaleRatio = Math.pow(2, maxZoomLevel); - - // Get coordinates of the selection shape (a rectangle here) - let absoluteRectOriginX = shape.origin.relativeOriginX * scaleRatio, - absoluteRectOriginY = shape.origin.relativeOriginY * scaleRatio, - absoluteRectWidth = shape.relativeRectWidth * scaleRatio, - absoluteRectHeight = shape.relativeRectHeight * scaleRatio; - - let xmin = Math.min(absoluteRectOriginX, absoluteRectOriginX + absoluteRectWidth), - ymin = Math.min(absoluteRectOriginY, absoluteRectOriginY + absoluteRectHeight); - - let selectedLayerCtx = this.layer.getCanvas().getContext("2d"); - let imageData = selectedLayerCtx.getImageData(xmin, ymin, Math.abs(absoluteRectWidth), Math.abs(absoluteRectHeight)); - - this.imageDataList.push(imageData); - shape.changeBlendModeTo("add"); }); } - cutShape(maxZoomLevel) { + cutSelection(maxZoomLevel) { this.imageDataList = []; this.selectedShapes.forEach((shape) => { - this.layer.removeShapeFromLayer(shape); - this.layer.drawLayer(maxZoomLevel, this.layer.getCanvas()); - - let scaleRatio = Math.pow(2, maxZoomLevel); - - // Get coordinates of the selection shape (a rectangle here) - let absoluteRectOriginX = shape.origin.relativeOriginX * scaleRatio, - absoluteRectOriginY = shape.origin.relativeOriginY * scaleRatio, - absoluteRectWidth = shape.relativeRectWidth * scaleRatio, - absoluteRectHeight = shape.relativeRectHeight * scaleRatio; - - let xmin = Math.min(absoluteRectOriginX, absoluteRectOriginX + absoluteRectWidth), - ymin = Math.min(absoluteRectOriginY, absoluteRectOriginY + absoluteRectHeight); - - let selectedLayerCtx = this.layer.getCanvas().getContext("2d"); - let imageData = selectedLayerCtx.getImageData(xmin, ymin, Math.abs(absoluteRectWidth), Math.abs(absoluteRectHeight)); - - this.imageDataList.push(imageData); - + this._copyImageData(shape, maxZoomLevel); shape.changeBlendModeTo("subtract"); - this.layer.addShapeToLayer(shape); - this.layer.drawLayer(maxZoomLevel, this.layer.getCanvas()); }); + + this.layer.addShapeToLayer(new CompoundShape(...this.selectedShapes)); + this.layer.drawLayer(maxZoomLevel, this.layer.getCanvas()); } /** @@ -106,17 +74,9 @@ export class Selection { } drawOnPage(layer, pageIndex, zoomLevel, renderer, canvas) { - let scaleRatio = Math.pow(2, zoomLevel); - this.selectedShapes.forEach((shape, index) => { // Get coordinates of the selection shape (a rectangle here) - let absoluteRectOriginX = shape.origin.relativeOriginX * scaleRatio, - absoluteRectOriginY = shape.origin.relativeOriginY * scaleRatio, - absoluteRectWidth = shape.relativeRectWidth * scaleRatio, - absoluteRectHeight = shape.relativeRectHeight * scaleRatio; - - let xmin = Math.min(absoluteRectOriginX, absoluteRectOriginX + absoluteRectWidth); - let ymin = Math.min(absoluteRectOriginY, absoluteRectOriginY + absoluteRectHeight); + let { x, y, absoluteRectWidth, absoluteRectHeight } = this._getBoundingDimensions(shape, zoomLevel); let pasteCanvas = document.createElement("canvas"); pasteCanvas.width = Math.abs(absoluteRectWidth); @@ -124,7 +84,35 @@ export class Selection { pasteCanvas.getContext("2d").putImageData(this.imageDataList[index], 0, 0); - canvas.getContext("2d").drawImage(pasteCanvas, xmin, ymin); + canvas.getContext("2d").drawImage(pasteCanvas, x, y); }); } + + _copyImageData(shape, maxZoomLevel) { + this.layer.removeShapeFromLayer(shape); + this.layer.drawLayer(maxZoomLevel, this.layer.getCanvas()); + + // Get coordinates of the selection shape (a rectangle here) + let { x, y, absoluteRectWidth, absoluteRectHeight } = this._getBoundingDimensions(shape, maxZoomLevel); + + let selectedLayerCtx = this.layer.getCanvas().getContext("2d"); + let imageData = selectedLayerCtx.getImageData(x, y, Math.abs(absoluteRectWidth), Math.abs(absoluteRectHeight)); + + this.imageDataList.push(imageData); + } + + _getBoundingDimensions(shape, zoomLevel) { + let scaleRatio = Math.pow(2, zoomLevel); + + // Get coordinates of the selection shape (a rectangle here) + let absoluteRectOriginX = shape.origin.relativeOriginX * scaleRatio, + absoluteRectOriginY = shape.origin.relativeOriginY * scaleRatio, + absoluteRectWidth = shape.relativeRectWidth * scaleRatio, + absoluteRectHeight = shape.relativeRectHeight * scaleRatio; + + let x = Math.min(absoluteRectOriginX, absoluteRectOriginX + absoluteRectWidth), + y = Math.min(absoluteRectOriginY, absoluteRectOriginY + absoluteRectHeight); + + return { x, y, absoluteRectWidth, absoluteRectHeight }; + } } diff --git a/source/shape.js b/source/shape.js index f565fd63..272d83be 100644 --- a/source/shape.js +++ b/source/shape.js @@ -11,14 +11,6 @@ export class Shape this.blendMode = blendMode; } - /** - * Abstract method, to be overridden - */ - drawInViewport () - { - - } - /** * Abstract method, to be overridden */