From a8f208eda48bba7618510e16f93662d3a7330775 Mon Sep 17 00:00:00 2001 From: Metachs <123@example.org> Date: Sun, 25 Aug 2024 16:59:35 -0400 Subject: [PATCH 1/9] Add onundo/redo events, add undo/redo handling to select tool to avoid history desync when undoing with an active selection. --- js/lib/commands.js | 20 ++++++++++++++++++++ js/ui/tool/select.js | 17 +++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/js/lib/commands.js b/js/lib/commands.js index 0dc492f..3b9a3ca 100644 --- a/js/lib/commands.js +++ b/js/lib/commands.js @@ -21,6 +21,14 @@ const commands = makeReadOnly( _history: [], /** The types of commands we can run (private) */ _types: {}, + + /** @type {Observer<{n: int, cancel: function}>} */ + get onundo() { return this._onundo; }, + _onundo: new Observer(), + + /** @type {Observer<{n: int, cancel: function}>} */ + get onredo() { return this._onredo; }, + _onredo: new Observer(), /** * Undoes the last commands in the history @@ -28,6 +36,12 @@ const commands = makeReadOnly( * @param {number} [n] Number of actions to undo */ async undo(n = 1) { + var cancelled = false; + await this._onundo.emit({ + n:n, + cancel: ()=>{cancelled=true;}, + }); + if (cancelled) return; for (var i = 0; i < n && this.current > -1; i++) { try { await this._history[this._current--].undo(); @@ -45,6 +59,12 @@ const commands = makeReadOnly( * @param {number} [n] Number of actions to redo */ async redo(n = 1) { + let cancelled = false; + await this._onredo.emit({ + n:n, + cancel: ()=>{cancelled=true;}, + }); + if (cancelled) return; for (var i = 0; i < n && this.current + 1 < this._history.length; i++) { try { await this._history[++this._current].redo(); diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index f89da91..855c2e1 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -23,6 +23,9 @@ const selectTransformTool = () => // Layer system handlers uil.onactive.on(state.uilayeractivecb); + + commands.onundo.on(state.undocb); + commands.onredo.on(state.redocb); // Registers keyboard shortcuts keyboard.onShortcut({ctrl: true, key: "KeyA"}, state.ctrlacb); @@ -65,6 +68,9 @@ const selectTransformTool = () => uil.onactive.clear(state.uilayeractivecb); + commands.onundo.clear(state.undocb); + commands.onredo.clear(state.redocb); + // Clear any selections state.reset(); @@ -179,6 +185,17 @@ const selectTransformTool = () => ); } }; + + // Undo/Redo Handling, reset state before Undo/Redo + state.undocb= (undo)=>{ + if (state.selected){ + if (undo.n<=1) undo.cancel(); + state.reset(false); + } + } + state.redocb= (redo)=>{ + if (state.selected){ state.reset(false); } + } // Mirroring state.togglemirror = () => { From 78fee4fa8435f751a2ee679f99a3bc3b6f0c7178 Mon Sep 17 00:00:00 2001 From: Metachs <123@example.org> Date: Sun, 25 Aug 2024 17:09:35 -0400 Subject: [PATCH 2/9] Reset state before Ctrl-A shortcuts to avoid deleted currently selected content --- js/ui/tool/select.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 855c2e1..8a95e6a 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -557,6 +557,7 @@ const selectTransformTool = () => // Register Ctrl-A Shortcut state.ctrlacb = () => { + state.reset(false); // Reset to preserve selected content try { const {bb} = cropCanvas(uil.canvas); select(bb); @@ -566,6 +567,8 @@ const selectTransformTool = () => }; state.ctrlsacb = () => { + state.reset(false); // Reset to preserve selected content + // Shift Key selects based on all visible layer information const tl = {x: Infinity, y: Infinity}; const br = {x: -Infinity, y: -Infinity}; From 073c1b2b783b6a985e833a4f71a2a3a95ab8d523 Mon Sep 17 00:00:00 2001 From: Metachs <123@example.org> Date: Sun, 25 Aug 2024 18:24:28 -0400 Subject: [PATCH 3/9] Overhaul Select Tool --- css/icons.css | 12 ++ js/ui/tool/select.js | 330 ++++++++++++++++++++++++++++++------------- 2 files changed, 246 insertions(+), 96 deletions(-) diff --git a/css/icons.css b/css/icons.css index 214460d..2f2dc6a 100644 --- a/css/icons.css +++ b/css/icons.css @@ -202,3 +202,15 @@ -webkit-mask-image: url("../res/icons/squircle.svg"); mask-image: url("../res/icons/squircle.svg"); } + +.ui.inline-icon.icon-file-plus::after, +.ui.icon > .icon-file-plus { + -webkit-mask-image: url("../res/icons/file-plus.svg"); + mask-image: url("../res/icons/file-plus.svg"); +} + +.ui.inline-icon.icon-lock::after, +.ui.icon > .icon-lock { + -webkit-mask-image: url("../res/icons/lock.svg"); + mask-image: url("../res/icons/lock.svg"); +} \ No newline at end of file diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 8a95e6a..e8db523 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -37,6 +37,14 @@ const selectTransformTool = () => keyboard.onShortcut({ctrl: true, key: "KeyV"}, state.ctrlvcb); keyboard.onShortcut({ctrl: true, key: "KeyX"}, state.ctrlxcb); keyboard.onShortcut({key: "Equal"}, state.togglemirror); + + keyboard.onShortcut({key: "Enter"}, state.entercb); + keyboard.onShortcut({shift: true, key: "Enter"}, state.sentercb); + keyboard.onShortcut({ctrl: true, key: "Enter"}, state.ctentercb); + keyboard.onShortcut({key: "Delete"}, state.delcb); + keyboard.onShortcut({shift: true, key: "Delete"}, state.sdelcb); + + keyboard.onShortcut({key: "Escape"}, state.escapecb); state.ctxmenu.mirrorSelectionCheckbox.disabled = true; state.selected = null; @@ -65,7 +73,13 @@ const selectTransformTool = () => keyboard.deleteShortcut(state.ctrlvcb, "KeyV"); keyboard.deleteShortcut(state.ctrlxcb, "KeyX"); keyboard.deleteShortcut(state.togglemirror, "Equal"); - + keyboard.deleteShortcut(state.entercb,"Enter"); + keyboard.deleteShortcut(state.sentercb,"Enter"); + keyboard.deleteShortcut(state.ctentercb,"Enter"); + keyboard.deleteShortcut(state.delcb,"Delete"); + keyboard.deleteShortcut(state.sdelcb,"Delete"); + keyboard.deleteShortcut(state.escapecb,"Escape"); + uil.onactive.clear(state.uilayeractivecb); commands.onundo.clear(state.undocb); @@ -91,6 +105,10 @@ const selectTransformTool = () => state.snapToGrid = true; state.keepAspectRatio = true; state.block_res_change = true; + + state.toNewLayer = false; + state.preserveOriginal = false; + state.useClipboard = !!( navigator.clipboard && navigator.clipboard.write ); // Use it by default if supported @@ -278,80 +296,9 @@ const selectTransformTool = () => state.selected.position.y === state.original.sy && !state.mirrorSelection && state.original.layer === uil.layer - ) && - !isCanvasBlank( - 0, - 0, - state.selected.canvas.width, - state.selected.canvas.height, - state.selected.canvas ) ) { - // Put original image back - state.original.layer.ctx.drawImage( - state.selected.canvas, - state.original.x, - state.original.y - ); - - // Erase Original Selection Area - commands.runCommand( - "eraseImage", - "Transform Tool Erase", - { - layer: state.original.layer, - x: state.original.x, - y: state.original.y, - w: state.selected.canvas.width, - h: state.selected.canvas.height, - }, - { - extra: { - log: `Erased original selection area at x: ${state.original.x}, y: ${state.original.y}, width: ${state.selected.canvas.width}, height: ${state.selected.canvas.height} from layer ${state.original.layer.id}`, - }, - } - ); - - // Draw Image - const {canvas, bb} = cropCanvas(state.originalDisplayLayer.canvas, { - border: 10, - }); - - let commandLog = ""; - const addline = (v, newline = true) => { - commandLog += v; - if (newline) commandLog += "\n"; - }; - - addline( - `Draw selected area to x: ${bb.x}, y: ${bb.y}, width: ${bb.w}, height: ${bb.h} to layer ${state.original.layer.id}` - ); - addline( - ` - translation: (x: ${state.selected.position.x}, y: ${state.selected.position.y})` - ); - addline( - ` - rotation : ${ - Math.round(1000 * ((180 * state.selected.rotation) / Math.PI)) / - 1000 - } degrees`, - false - ); - - commands.runCommand( - "drawImage", - "Transform Tool Apply", - { - image: canvas, - ...bb, - }, - { - extra: { - log: commandLog, - }, - } - ); - - state.reset(true); + state.applyTransform(); } else { state.reset(); } @@ -530,30 +477,30 @@ const selectTransformTool = () => state.cancelcb = (evn) => { state.reset(); }; - + + state.keydowncb = (evn) => { }; + // Keyboard callbacks (For now, they just handle the "delete" key) - state.keydowncb = (evn) => {}; - - state.keyclickcb = (evn) => { - switch (evn.code) { - case "Delete": - // Deletes selected area - state.selected && - commands.runCommand( - "eraseImage", - "Erase Area", - state.selected, - { - extra: { - log: `[Placeholder] Delete selected area. TODO it's also broken`, - }, - } - ); - state.ctxmenu.mirrorSelectionCheckbox.disabled = true; - state.selected = null; - state.redraw(); - } - }; + state.keyclickcb = async(evn) => { }; + + // Register Delete Shortcut + state.delcb = async(evn) => { state.applyTransform(true,false,false,false); }; + + // Register Escape Shortcut + state.escapecb = async(evn) => { state.reset(false); }; + + // Register Shift-Delete Shortcut + state.sdelcb = async(evn) => { state.applyTransform(false,true,false,false); }; + + // Register Enter Shortcut (Delegates to clickcb) + state.entercb = async(evn) => { clickcb(evn); }; + + // Register Ctrl-Enter Shortcut + state.ctentercb = async(evn) => { state.applyTransform(false,false,true,true); }; + + // Register Shift-Enter Shortcut + state.sentercb = async(evn) => { state.applyTransform(false,false,true,false); }; + // Register Ctrl-A Shortcut state.ctrlacb = () => { @@ -687,6 +634,117 @@ const selectTransformTool = () => state.ctrlxcb = (evn) => { state.ctrlccb(evn, true); }; + + // Apply Transform and Reset State, optionally erase Selection or Clear Original Layer + // newLayer and keepOriginal default to null, overriding the forced variants if explicitly set to false + // Only checks if Selection exists and content has been selected + // Does not check if content has been transformed, eg for deletion/applying to new layer + state.applyTransform = (eraseSelected = false, clearLayer = false, newLayer = null, keepOriginal = null) => { + // Just reset state if nothing is selected, unless Clearing layer + if (!state.selected || state.original.layer.hidden || + !clearLayer && + isCanvasBlank( + 0, + 0, + state.selected.canvas.width, + state.selected.canvas.height, + state.selected.canvas + ) + ){ + state.reset(false); + return; + } + + // Put original image back + state.original.layer.ctx.drawImage( + state.selected.canvas, + state.original.x, + state.original.y + ); + + // Erase Entire Layer + if (clearLayer) commands.runCommand( + "eraseImage", + "Transform Tool Erase", + { + ...state.original.layer.bb, + layer: state.original.layer, + }, + { + extra: { + log: `Erased layer ${state.original.layer.id}`, + }, + } + ); + // Erase Original Selection Area + else if (eraseSelected || !state.preserveOriginal && (keepOriginal==null || !keepOriginal) ) commands.runCommand( + "eraseImage", + "Transform Tool Erase", + { + layer: state.original.layer, + x: state.original.x, + y: state.original.y, + w: state.selected.canvas.width, + h: state.selected.canvas.height, + }, + { + extra: { + log: `Erased original selection area at x: ${state.original.x}, y: ${state.original.y}, width: ${state.selected.canvas.width}, height: ${state.selected.canvas.height} from layer ${state.original.layer.id}`, + }, + } + ); + + // Selection erased, no need to draw anything + if (eraseSelected){ + state.reset(true); + return; + } + + // Draw Image + const {canvas, bb} = cropCanvas(state.originalDisplayLayer.canvas, { + border: 10, + }); + + if ( (newLayer || state.toNewLayer && newLayer==null) && !clearLayer) + commands.runCommand("addLayer", "Added Layer", {name: "Copy-"+state.original.layer.name}); + + let commandLog = ""; + const addline = (v, newline = true) => { + commandLog += v; + if (newline) commandLog += "\n"; + }; + + addline( + `Draw selected area to x: ${bb.x}, y: ${bb.y}, width: ${bb.w}, height: ${bb.h} to layer ${state.original.layer.id}` + ); + addline( + ` - translation: (x: ${state.selected.position.x}, y: ${state.selected.position.y})` + ); + addline( + ` - rotation : ${ + Math.round(1000 * ((180 * state.selected.rotation) / Math.PI)) / + 1000 + } degrees`, + false + ); + + commands.runCommand( + "drawImage", + "Transform Tool Apply", + { + image: canvas, + ...bb, + }, + { + extra: { + log: commandLog, + }, + } + ); + + state.reset(true); + } + }, populateContextMenu: (menu, state) => { if (!state.ctxmenu) { @@ -752,6 +810,24 @@ const selectTransformTool = () => state.ctxmenu.useClipboardLabel = clipboardCheckbox.checkbox; if (!(navigator.clipboard && navigator.clipboard.write)) clipboardCheckbox.checkbox.disabled = true; // Disable if not available + + // toNewLayer + state.ctxmenu.toNewLayerLabel = _toolbar_input.checkbox( + state, + "openoutpaint/select-toNewLayer", + "toNewLayer", + "Always Create New Layer", + "icon-file-plus" + ).checkbox; + + // preserveOriginal + state.ctxmenu.preserveOriginalLabel = _toolbar_input.checkbox( + state, + "openoutpaint/select-preserveOriginal", + "preserveOriginal", + "Preserve Original Image - Restore original content after transforming selection", + "icon-lock" + ).checkbox; // Selection Peek Opacity state.ctxmenu.selectionPeekOpacitySlider = _toolbar_input.slider( @@ -853,12 +929,59 @@ const selectTransformTool = () => visibleActionArray.appendChild(saveVisibleSelectionButton); visibleActionArray.appendChild(createVisibleResourceButton); + + // Some useful actions to do with selection + const actionArrayRow3 = document.createElement("div"); + actionArrayRow3.classList.add("button-array"); + + // Apply To New Layer button + const applyNewLayerButton = document.createElement("button"); + applyNewLayerButton.classList.add("button", "tool"); + applyNewLayerButton.textContent = "Move to Layer"; + applyNewLayerButton.title = "Moves Selection to a New Layer (Shift+Enter)"; + applyNewLayerButton.onclick = () => { state.applyTransform(false,false,true,false); }; + + // Copy To Layer Buttons + const copyNewLayerButton = document.createElement("button"); + copyNewLayerButton.classList.add("button", "tool"); + copyNewLayerButton.textContent = "Copy to Layer"; + copyNewLayerButton.title = "Copies selection to a new Layer (Ctrl+Enter)"; + copyNewLayerButton.onclick = () => { state.applyTransform(false,false,true,true); }; + + actionArrayRow3.appendChild(applyNewLayerButton); + actionArrayRow3.appendChild(copyNewLayerButton); + + const actionArrayRow4 = document.createElement("div"); + actionArrayRow4.classList.add("button-array"); + + // Clear Button + const applyClearButton = document.createElement("button"); + applyClearButton.classList.add("button", "tool"); + applyClearButton.textContent = "Clear and Apply"; + applyClearButton.title = "Erases everything in the current layer other than the selection (Shift+Delete)"; + applyClearButton.onclick = () => { state.applyTransform(false,true,false,false); }; + + // Erase Button + const eraseSelectionButton = document.createElement("button"); + eraseSelectionButton.classList.add("button", "tool"); + eraseSelectionButton.textContent = "Erase"; + eraseSelectionButton.title = "Erases current selection (Delete)"; + eraseSelectionButton.onclick = () => { state.applyTransform(true,false,false,false); }; + + actionArrayRow4.appendChild(applyClearButton); + actionArrayRow4.appendChild(eraseSelectionButton); + + // Disable buttons (if nothing is selected) state.ctxmenu.disableButtons = () => { saveSelectionButton.disabled = true; createResourceButton.disabled = true; saveVisibleSelectionButton.disabled = true; createVisibleResourceButton.disabled = true; + applyNewLayerButton.disabled = true; + copyNewLayerButton.disabled = true; + applyClearButton.disabled = true; + eraseSelectionButton.disabled = true; }; // Enable buttons (if something is selected) @@ -867,9 +990,17 @@ const selectTransformTool = () => createResourceButton.disabled = ""; saveVisibleSelectionButton.disabled = ""; createVisibleResourceButton.disabled = ""; + applyNewLayerButton.disabled = ""; + copyNewLayerButton.disabled = ""; + applyClearButton.disabled = ""; + eraseSelectionButton.disabled = ""; }; + state.ctxmenu.actionArray = actionArray; state.ctxmenu.visibleActionArray = visibleActionArray; + + state.ctxmenu.actionArrayRow3 = actionArrayRow3; + state.ctxmenu.actionArrayRow4 = actionArrayRow4; // Send Selection to Destination state.ctxmenu.sendSelected = document.createElement("select"); @@ -892,10 +1023,17 @@ const selectTransformTool = () => array.appendChild(state.ctxmenu.keepAspectRatioLabel); array.appendChild(state.ctxmenu.mirrorSelectionCheckbox); array.appendChild(state.ctxmenu.useClipboardLabel); + + array.appendChild(state.ctxmenu.toNewLayerLabel); + array.appendChild(state.ctxmenu.preserveOriginalLabel); + menu.appendChild(array); menu.appendChild(state.ctxmenu.selectionPeekOpacitySlider); menu.appendChild(state.ctxmenu.actionArray); menu.appendChild(state.ctxmenu.visibleActionArray); + menu.appendChild(state.ctxmenu.actionArrayRow3); + menu.appendChild(state.ctxmenu.actionArrayRow4); + if (global.webui && global.webui.destinations) { while (state.ctxmenu.sendSelected.lastChild.value !== "None") { state.ctxmenu.sendSelected.removeChild( From b4c84a83519bcd27e420c31e5ab15a543d665c81 Mon Sep 17 00:00:00 2001 From: Metachs <123@example.org> Date: Sun, 25 Aug 2024 18:38:07 -0400 Subject: [PATCH 4/9] Adjust Text --- js/ui/tool/select.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index e8db523..59a0193 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -957,8 +957,8 @@ const selectTransformTool = () => // Clear Button const applyClearButton = document.createElement("button"); applyClearButton.classList.add("button", "tool"); - applyClearButton.textContent = "Clear and Apply"; - applyClearButton.title = "Erases everything in the current layer other than the selection (Shift+Delete)"; + applyClearButton.textContent = "Erase Outside"; + applyClearButton.title = "Erases everything in the current layer outside the selection (Shift+Delete)"; applyClearButton.onclick = () => { state.applyTransform(false,true,false,false); }; // Erase Button From d9efd82f27a21bbc7df8314fa442bc2e272d7334 Mon Sep 17 00:00:00 2001 From: Metachs <123@example.org> Date: Sun, 25 Aug 2024 22:28:36 -0400 Subject: [PATCH 5/9] Additional Controls, adjust UI. --- js/ui/tool/select.js | 186 +++++++++++++++++++++++++++---------------- 1 file changed, 119 insertions(+), 67 deletions(-) diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 59a0193..26be822 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -13,6 +13,8 @@ const selectTransformTool = () => mouse.listen.world.btn.left.ondragstart.on(state.dragstartcb); mouse.listen.world.btn.left.ondrag.on(state.dragcb); mouse.listen.world.btn.left.ondragend.on(state.dragendcb); + + mouse.listen.world.btn.left.ondclick.on(state.dclickcb); // Canvas right mouse handler mouse.listen.world.btn.right.onclick.on(state.cancelcb); @@ -24,6 +26,7 @@ const selectTransformTool = () => // Layer system handlers uil.onactive.on(state.uilayeractivecb); + // Undo commands.onundo.on(state.undocb); commands.onredo.on(state.redocb); @@ -41,6 +44,7 @@ const selectTransformTool = () => keyboard.onShortcut({key: "Enter"}, state.entercb); keyboard.onShortcut({shift: true, key: "Enter"}, state.sentercb); keyboard.onShortcut({ctrl: true, key: "Enter"}, state.ctentercb); + keyboard.onShortcut({ctrl: true, shift: true, key: "Enter"}, state.sctentercb); keyboard.onShortcut({key: "Delete"}, state.delcb); keyboard.onShortcut({shift: true, key: "Delete"}, state.sdelcb); @@ -62,11 +66,14 @@ const selectTransformTool = () => mouse.listen.world.btn.left.ondragstart.clear(state.dragstartcb); mouse.listen.world.btn.left.ondrag.clear(state.dragcb); mouse.listen.world.btn.left.ondragend.clear(state.dragendcb); + + mouse.listen.world.btn.left.ondclick.clear(state.dclickcb); mouse.listen.world.btn.right.onclick.clear(state.cancelcb); keyboard.listen.onkeyclick.clear(state.keyclickcb); keyboard.listen.onkeydown.clear(state.keydowncb); + keyboard.deleteShortcut(state.ctrlacb, "KeyA"); keyboard.deleteShortcut(state.ctrlsacb, "KeyA"); keyboard.deleteShortcut(state.ctrlccb, "KeyC"); @@ -76,6 +83,7 @@ const selectTransformTool = () => keyboard.deleteShortcut(state.entercb,"Enter"); keyboard.deleteShortcut(state.sentercb,"Enter"); keyboard.deleteShortcut(state.ctentercb,"Enter"); + keyboard.deleteShortcut(state.sctentercb,"Enter"); keyboard.deleteShortcut(state.delcb,"Delete"); keyboard.deleteShortcut(state.sdelcb,"Delete"); keyboard.deleteShortcut(state.escapecb,"Escape"); @@ -304,6 +312,19 @@ const selectTransformTool = () => } }; + // Handles left mouse double clicks - Select All + state.dclickcb = (evn) => { + if (state.selected) return; + // Wait so clickcb doesn't immediately deselect. + state.dclickcb_timeout = state.dclickcb_timeout ?? window.setTimeout(async ()=>{ + state.dclickcb_timeout = null; + if (!state.selected && !selection.exists) { + try { select(cropCanvas(uil.canvas)?.bb); } + catch (e) { }// Ignore errors + } + },300); + }; + // Handles left mouse drag start events state.dragstartcb = (evn) => { const { @@ -481,30 +502,50 @@ const selectTransformTool = () => state.keydowncb = (evn) => { }; // Keyboard callbacks (For now, they just handle the "delete" key) - state.keyclickcb = async(evn) => { }; + state.keyclickcb = (evn) => { }; // Register Delete Shortcut - state.delcb = async(evn) => { state.applyTransform(true,false,false,false); }; + state.delcb = (evn) => { state.applyTransform(true,false,false,false); }; // Register Escape Shortcut - state.escapecb = async(evn) => { state.reset(false); }; + state.escapecb = (evn) => { state.reset(false); }; // Register Shift-Delete Shortcut - state.sdelcb = async(evn) => { state.applyTransform(false,true,false,false); }; - + state.sdelcb = (evn) => { state.applyTransform(false,true,false,false); }; + // Register Enter Shortcut (Delegates to clickcb) - state.entercb = async(evn) => { clickcb(evn); }; - + state.entercb = (evn) => { state.clickcb(evn); }; + // Register Ctrl-Enter Shortcut - state.ctentercb = async(evn) => { state.applyTransform(false,false,true,true); }; - - // Register Shift-Enter Shortcut - state.sentercb = async(evn) => { state.applyTransform(false,false,true,false); }; - + state.ctentercb = (evn) => { state.applyTransform(false,false,true,true); }; + + // Register Shift-Enter Shortcut + state.sentercb = (evn) => { state.applyTransform(false,false,true,false); }; + + // Register Ctrl-Shift-Enter Shortcut + state.sctentercb = async (evn) => { + var selectBB = + state.selected.bb != undefined + ? state.selected.bb + : state.backupBB; + const canvas = uil.getVisible(selectBB, { + categories: ["image", "user", "select-display"], + }); + await commands.runCommand("addLayer", "Added Layer"); + + await commands.runCommand("drawImage", "Transform Tool Apply", + { + image: canvas, + ...selectBB, + } + ); + state.reset(false); + }; + // Register Ctrl-A Shortcut state.ctrlacb = () => { - state.reset(false); // Reset to preserve selected content + // state.reset(false); // Reset to preserve selected content try { const {bb} = cropCanvas(uil.canvas); select(bb); @@ -639,18 +680,12 @@ const selectTransformTool = () => // newLayer and keepOriginal default to null, overriding the forced variants if explicitly set to false // Only checks if Selection exists and content has been selected // Does not check if content has been transformed, eg for deletion/applying to new layer - state.applyTransform = (eraseSelected = false, clearLayer = false, newLayer = null, keepOriginal = null) => { + state.applyTransform = async (eraseSelected = false, clearLayer = false, newLayer = null, keepOriginal = null) => { + const isBlank = + isCanvasBlank( 0, 0, state.selected.canvas.width, state.selected.canvas.height, state.selected.canvas); + // Just reset state if nothing is selected, unless Clearing layer - if (!state.selected || state.original.layer.hidden || - !clearLayer && - isCanvasBlank( - 0, - 0, - state.selected.canvas.width, - state.selected.canvas.height, - state.selected.canvas - ) - ){ + if (!state.selected || !clearLayer && isBlank ){ state.reset(false); return; } @@ -663,7 +698,7 @@ const selectTransformTool = () => ); // Erase Entire Layer - if (clearLayer) commands.runCommand( + if (clearLayer) await commands.runCommand( "eraseImage", "Transform Tool Erase", { @@ -675,9 +710,9 @@ const selectTransformTool = () => log: `Erased layer ${state.original.layer.id}`, }, } - ); + ); // Erase Original Selection Area - else if (eraseSelected || !state.preserveOriginal && (keepOriginal==null || !keepOriginal) ) commands.runCommand( + else if (eraseSelected || !(keepOriginal ?? state.preserveOriginal)) await commands.runCommand( "eraseImage", "Transform Tool Erase", { @@ -694,10 +729,10 @@ const selectTransformTool = () => } ); - // Selection erased, no need to draw anything - if (eraseSelected){ - state.reset(true); - return; + // Selection erased or was blank, no need to draw anything + if (eraseSelected || isBlank){ + state.reset(true); + return; } // Draw Image @@ -705,8 +740,8 @@ const selectTransformTool = () => border: 10, }); - if ( (newLayer || state.toNewLayer && newLayer==null) && !clearLayer) - commands.runCommand("addLayer", "Added Layer", {name: "Copy-"+state.original.layer.name}); + if ( (newLayer ?? state.toNewLayer) && !clearLayer) + await commands.runCommand("addLayer", "Added Layer"); let commandLog = ""; const addline = (v, newline = true) => { @@ -728,7 +763,7 @@ const selectTransformTool = () => false ); - commands.runCommand( + await commands.runCommand( "drawImage", "Transform Tool Apply", { @@ -853,7 +888,7 @@ const selectTransformTool = () => // Save button const saveSelectionButton = document.createElement("button"); saveSelectionButton.classList.add("button", "tool"); - saveSelectionButton.textContent = "Save Sel."; + saveSelectionButton.innerHTML = "Save Sel."; // nbsp as a quick hack for unwanted text wrapping saveSelectionButton.title = "Saves Selection"; saveSelectionButton.onclick = () => { downloadCanvas({ @@ -875,9 +910,25 @@ const selectTransformTool = () => tools.stamp.enable(); }; }; - + + const copyNewLayerButton = document.createElement("button"); + copyNewLayerButton.classList.add("button", "tool"); + copyNewLayerButton.textContent = "Layer"; + copyNewLayerButton.title = "Copies selection to a new Layer (Ctrl+Enter)"; + copyNewLayerButton.onclick = () => { state.applyTransform(false,false,true,true); }; + + // Dummy button for saving active selection + const ActiveSelectionButton = document.createElement("button"); + ActiveSelectionButton.classList.add("button", "tool"); + ActiveSelectionButton.textContent = "📄"; + ActiveSelectionButton.title = "Commands Applied to the visible Selection"; + ActiveSelectionButton.disabled = true; + + actionArray.appendChild(saveSelectionButton); actionArray.appendChild(createResourceButton); + actionArray.appendChild(copyNewLayerButton); + actionArray.appendChild(ActiveSelectionButton); // Some useful actions to do with selection const visibleActionArray = document.createElement("div"); @@ -886,8 +937,8 @@ const selectTransformTool = () => // Save Visible button const saveVisibleSelectionButton = document.createElement("button"); saveVisibleSelectionButton.classList.add("button", "tool"); - saveVisibleSelectionButton.textContent = "Save Vis."; - saveVisibleSelectionButton.title = "Saves Visible Selection"; + saveVisibleSelectionButton.innerHTML = "Save Vis."; // nbsp as a quick hack for unwanted text wrapping + saveVisibleSelectionButton.title = "Saves Visible Selection And Download"; saveVisibleSelectionButton.onclick = () => { console.debug(state.selected); console.debug(state.selected.bb); @@ -907,7 +958,7 @@ const selectTransformTool = () => // Save Visible as Resource Button const createVisibleResourceButton = document.createElement("button"); createVisibleResourceButton.classList.add("button", "tool"); - createVisibleResourceButton.textContent = "Vis. to Res."; + createVisibleResourceButton.textContent = "Resource"; createVisibleResourceButton.title = "Saves Visible Selection as a Resource"; createVisibleResourceButton.onclick = () => { @@ -926,38 +977,32 @@ const selectTransformTool = () => }; }; + // Copy To Layer Buttons + const copyVisNewLayerButton = document.createElement("button"); + copyVisNewLayerButton.classList.add("button", "tool"); + copyVisNewLayerButton.textContent = "Layer"; + copyVisNewLayerButton.title = "Copies Visible Selection to a new Layer (Ctrl+Shift+Enter)"; + copyVisNewLayerButton.onclick = (e) => { state.sctentercb(e); }; + + // Dummy button for saving visible Selection + const VisibleSelectionButton = document.createElement("button"); + VisibleSelectionButton.classList.add("button", "tool"); + VisibleSelectionButton.textContent = "👁"; + VisibleSelectionButton.title = "Commands Applied to the visible Selection"; + VisibleSelectionButton.disabled = true; + visibleActionArray.appendChild(saveVisibleSelectionButton); visibleActionArray.appendChild(createVisibleResourceButton); + visibleActionArray.appendChild(copyVisNewLayerButton); + visibleActionArray.appendChild(VisibleSelectionButton); - - // Some useful actions to do with selection const actionArrayRow3 = document.createElement("div"); actionArrayRow3.classList.add("button-array"); - - // Apply To New Layer button - const applyNewLayerButton = document.createElement("button"); - applyNewLayerButton.classList.add("button", "tool"); - applyNewLayerButton.textContent = "Move to Layer"; - applyNewLayerButton.title = "Moves Selection to a New Layer (Shift+Enter)"; - applyNewLayerButton.onclick = () => { state.applyTransform(false,false,true,false); }; - - // Copy To Layer Buttons - const copyNewLayerButton = document.createElement("button"); - copyNewLayerButton.classList.add("button", "tool"); - copyNewLayerButton.textContent = "Copy to Layer"; - copyNewLayerButton.title = "Copies selection to a new Layer (Ctrl+Enter)"; - copyNewLayerButton.onclick = () => { state.applyTransform(false,false,true,true); }; - - actionArrayRow3.appendChild(applyNewLayerButton); - actionArrayRow3.appendChild(copyNewLayerButton); - - const actionArrayRow4 = document.createElement("div"); - actionArrayRow4.classList.add("button-array"); // Clear Button const applyClearButton = document.createElement("button"); applyClearButton.classList.add("button", "tool"); - applyClearButton.textContent = "Erase Outside"; + applyClearButton.textContent = "Isolate"; applyClearButton.title = "Erases everything in the current layer outside the selection (Shift+Delete)"; applyClearButton.onclick = () => { state.applyTransform(false,true,false,false); }; @@ -967,10 +1012,17 @@ const selectTransformTool = () => eraseSelectionButton.textContent = "Erase"; eraseSelectionButton.title = "Erases current selection (Delete)"; eraseSelectionButton.onclick = () => { state.applyTransform(true,false,false,false); }; - - actionArrayRow4.appendChild(applyClearButton); - actionArrayRow4.appendChild(eraseSelectionButton); + // Apply To New Layer button + const applyNewLayerButton = document.createElement("button"); + applyNewLayerButton.classList.add("button", "tool"); + applyNewLayerButton.textContent = "Extract"; + applyNewLayerButton.title = "Moves Selection to a New Layer (Shift+Enter)"; + applyNewLayerButton.onclick = () => { state.applyTransform(false,false,true,false); }; + + actionArrayRow3.appendChild(applyClearButton); + actionArrayRow3.appendChild(eraseSelectionButton); + actionArrayRow3.appendChild(applyNewLayerButton); // Disable buttons (if nothing is selected) state.ctxmenu.disableButtons = () => { @@ -982,6 +1034,7 @@ const selectTransformTool = () => copyNewLayerButton.disabled = true; applyClearButton.disabled = true; eraseSelectionButton.disabled = true; + copyVisNewLayerButton.disabled = true; }; // Enable buttons (if something is selected) @@ -994,13 +1047,13 @@ const selectTransformTool = () => copyNewLayerButton.disabled = ""; applyClearButton.disabled = ""; eraseSelectionButton.disabled = ""; + copyVisNewLayerButton.disabled = ""; }; state.ctxmenu.actionArray = actionArray; state.ctxmenu.visibleActionArray = visibleActionArray; state.ctxmenu.actionArrayRow3 = actionArrayRow3; - state.ctxmenu.actionArrayRow4 = actionArrayRow4; // Send Selection to Destination state.ctxmenu.sendSelected = document.createElement("select"); @@ -1032,7 +1085,6 @@ const selectTransformTool = () => menu.appendChild(state.ctxmenu.actionArray); menu.appendChild(state.ctxmenu.visibleActionArray); menu.appendChild(state.ctxmenu.actionArrayRow3); - menu.appendChild(state.ctxmenu.actionArrayRow4); if (global.webui && global.webui.destinations) { while (state.ctxmenu.sendSelected.lastChild.value !== "None") { From 27896897b297497cfa1b8f884f7cfad9a93a52b3 Mon Sep 17 00:00:00 2001 From: Metachs <123@example.org> Date: Mon, 26 Aug 2024 09:25:22 -0400 Subject: [PATCH 6/9] Layer Selection Controls --- js/ui/tool/select.js | 68 ++++++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 26be822..37a4935 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -14,7 +14,8 @@ const selectTransformTool = () => mouse.listen.world.btn.left.ondrag.on(state.dragcb); mouse.listen.world.btn.left.ondragend.on(state.dragendcb); - mouse.listen.world.btn.left.ondclick.on(state.dclickcb); + mouse.listen.world.btn.left.ondclick.on(state.dclickcb); + mouse.listen.world.btn.right.ondclick.on(state.drclickcb); // Canvas right mouse handler mouse.listen.world.btn.right.onclick.on(state.cancelcb); @@ -68,6 +69,7 @@ const selectTransformTool = () => mouse.listen.world.btn.left.ondragend.clear(state.dragendcb); mouse.listen.world.btn.left.ondclick.clear(state.dclickcb); + mouse.listen.world.btn.right.ondclick.clear(state.drclickcb); mouse.listen.world.btn.right.onclick.clear(state.cancelcb); @@ -215,10 +217,12 @@ const selectTransformTool = () => // Undo/Redo Handling, reset state before Undo/Redo state.undocb= (undo)=>{ if (state.selected){ - if (undo.n<=1) undo.cancel(); + // Cancel so undo shortcut effectively undoes the current transform, unless requesting multiple steps + if (state.selectionTransformed() && undo.n<=1) + undo.cancel(); state.reset(false); } - } + } state.redocb= (redo)=>{ if (state.selected){ state.reset(false); } } @@ -294,37 +298,59 @@ const selectTransformTool = () => // Handles left mouse clicks state.clickcb = (evn) => { - if ( - state.selected && - !( - state.selected.rotation === 0 && - state.selected.scale.x === 1 && - state.selected.scale.y === 1 && - state.selected.position.x === state.original.sx && - state.selected.position.y === state.original.sy && - !state.mirrorSelection && - state.original.layer === uil.layer - ) - ) { + if (state.selectionTransformed()) { state.applyTransform(); } else { state.reset(); } }; + + // Check if selection has been transformed in any way. + state.selectionTransformed = ()=>{ + return state.selected && + !( + state.selected.rotation === 0 && + state.selected.scale.x === 1 && + state.selected.scale.y === 1 && + state.selected.position.x === state.original.sx && + state.selected.position.y === state.original.sy && + !state.mirrorSelection && + state.original.layer === uil.layer + ); + } - // Handles left mouse double clicks - Select All + // Handles left mouse double clicks - Select All Ctrl-A + // Holding shift key - Ctrl-Shift-A state.dclickcb = (evn) => { - if (state.selected) return; + // Do nothing if Ctrl Key is held for panning + if (state.selected || evn.evn.ctrlKey) return; + let shift = evn.evn.shiftKey; // Wait so clickcb doesn't immediately deselect. state.dclickcb_timeout = state.dclickcb_timeout ?? window.setTimeout(async ()=>{ state.dclickcb_timeout = null; if (!state.selected && !selection.exists) { - try { select(cropCanvas(uil.canvas)?.bb); } - catch (e) { }// Ignore errors - } + if (shift) state.ctrlsacb(evn); + else state.ctrlacb(evn); + } },300); }; + // Handles right mouse double clicks - Select topmost layer with content under pointer + // Holding shift key selects bottommost layer + state.drclickcb = (evn) => { + if (state.selected) return; + for (let l of (evn.evn.shiftKey ? uil.layers : uil.layers.toReversed()) ) { + if (!l.hidden && !isCanvasBlank(evn.x,evn.y,2,2,l.canvas)) { + uil.active=l; + state.dclickcb_timeout = state.dclickcb_timeout ?? window.setTimeout(async ()=>{ + state.dclickcb_timeout = null; + if (!state.selected && !selection.exists) { state.ctrlacb(evn); } + },300); + return; + } + } + }; + // Handles left mouse drag start events state.dragstartcb = (evn) => { const { @@ -545,7 +571,7 @@ const selectTransformTool = () => // Register Ctrl-A Shortcut state.ctrlacb = () => { - // state.reset(false); // Reset to preserve selected content + state.reset(false); // Reset to preserve selected content try { const {bb} = cropCanvas(uil.canvas); select(bb); From 954a2ac0eccb7fd0d1e03f61aad45a5a44cc02f4 Mon Sep 17 00:00:00 2001 From: Metachs <123@example.org> Date: Mon, 26 Aug 2024 10:44:37 -0400 Subject: [PATCH 7/9] Cleanup, removed a new control that no longer made sense. --- css/icons.css | 6 ----- js/ui/tool/select.js | 63 ++++++++++++++++++++++---------------------- 2 files changed, 32 insertions(+), 37 deletions(-) diff --git a/css/icons.css b/css/icons.css index 2f2dc6a..45ab99a 100644 --- a/css/icons.css +++ b/css/icons.css @@ -208,9 +208,3 @@ -webkit-mask-image: url("../res/icons/file-plus.svg"); mask-image: url("../res/icons/file-plus.svg"); } - -.ui.inline-icon.icon-lock::after, -.ui.icon > .icon-lock { - -webkit-mask-image: url("../res/icons/lock.svg"); - mask-image: url("../res/icons/lock.svg"); -} \ No newline at end of file diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index 37a4935..c9d7f33 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -117,7 +117,6 @@ const selectTransformTool = () => state.block_res_change = true; state.toNewLayer = false; - state.preserveOriginal = false; state.useClipboard = !!( navigator.clipboard && navigator.clipboard.write @@ -336,18 +335,30 @@ const selectTransformTool = () => }; // Handles right mouse double clicks - Select topmost layer with content under pointer - // Holding shift key selects bottommost layer + // Holding shift key Selects the next topmost if current layer has visible content under pointer. state.drclickcb = (evn) => { if (state.selected) return; - for (let l of (evn.evn.shiftKey ? uil.layers : uil.layers.toReversed()) ) { - if (!l.hidden && !isCanvasBlank(evn.x,evn.y,2,2,l.canvas)) { - uil.active=l; - state.dclickcb_timeout = state.dclickcb_timeout ?? window.setTimeout(async ()=>{ - state.dclickcb_timeout = null; - if (!state.selected && !selection.exists) { state.ctrlacb(evn); } - },300); - return; + // If shift key is held, and current layer is has visible pixels under pointer + // select topmost visible layer beneath the active layer + let shift = evn.evn.shiftKey + && !uil.active.hidden + && !isCanvasBlank(evn.x,evn.y,2,2,uil.active.canvas); + let layer = shift ? uil.active : null; + for (let l of uil.layers.toReversed()) { + if (shift) { + if (layer==l) shift = false; } + else if (!l.hidden && !isCanvasBlank(evn.x,evn.y,2,2,l.canvas)){ + layer = l; + break; + } + } + if (layer) { + uil.active=layer; + state.dclickcb_timeout = state.dclickcb_timeout ?? window.setTimeout(async ()=>{ + state.dclickcb_timeout = null; + if (!state.selected && !selection.exists) { state.ctrlacb(evn); } + },300); } }; @@ -527,7 +538,7 @@ const selectTransformTool = () => state.keydowncb = (evn) => { }; - // Keyboard callbacks (For now, they just handle the "delete" key) + // Keyboard callbacks state.keyclickcb = (evn) => { }; // Register Delete Shortcut @@ -536,19 +547,19 @@ const selectTransformTool = () => // Register Escape Shortcut state.escapecb = (evn) => { state.reset(false); }; - // Register Shift-Delete Shortcut + // Register Shift-Delete Shortcut - Delete Outside Selection and Apply state.sdelcb = (evn) => { state.applyTransform(false,true,false,false); }; - // Register Enter Shortcut (Delegates to clickcb) + // Register Enter Shortcut - Apply Transform (Delegates to clickcb) state.entercb = (evn) => { state.clickcb(evn); }; - // Register Ctrl-Enter Shortcut + // Register Ctrl-Enter Shortcut - Copy Selection to new layer, restore original state.ctentercb = (evn) => { state.applyTransform(false,false,true,true); }; - // Register Shift-Enter Shortcut + // Register Shift-Enter Shortcut - Move Selection to new layer state.sentercb = (evn) => { state.applyTransform(false,false,true,false); }; - // Register Ctrl-Shift-Enter Shortcut + // Register Ctrl-Shift-Enter Shortcut - Copy Visible Selection to new layer state.sctentercb = async (evn) => { var selectBB = state.selected.bb != undefined @@ -703,10 +714,10 @@ const selectTransformTool = () => }; // Apply Transform and Reset State, optionally erase Selection or Clear Original Layer - // newLayer and keepOriginal default to null, overriding the forced variants if explicitly set to false + // newLayer defaults to null, overriding the forced variants if explicitly set to false // Only checks if Selection exists and content has been selected // Does not check if content has been transformed, eg for deletion/applying to new layer - state.applyTransform = async (eraseSelected = false, clearLayer = false, newLayer = null, keepOriginal = null) => { + state.applyTransform = async (eraseSelected = false, clearLayer = false, newLayer = null, keepOriginal = false) => { const isBlank = isCanvasBlank( 0, 0, state.selected.canvas.width, state.selected.canvas.height, state.selected.canvas); @@ -738,7 +749,7 @@ const selectTransformTool = () => } ); // Erase Original Selection Area - else if (eraseSelected || !(keepOriginal ?? state.preserveOriginal)) await commands.runCommand( + else if (eraseSelected || !keepOriginal) await commands.runCommand( "eraseImage", "Transform Tool Erase", { @@ -880,15 +891,6 @@ const selectTransformTool = () => "Always Create New Layer", "icon-file-plus" ).checkbox; - - // preserveOriginal - state.ctxmenu.preserveOriginalLabel = _toolbar_input.checkbox( - state, - "openoutpaint/select-preserveOriginal", - "preserveOriginal", - "Preserve Original Image - Restore original content after transforming selection", - "icon-lock" - ).checkbox; // Selection Peek Opacity state.ctxmenu.selectionPeekOpacitySlider = _toolbar_input.slider( @@ -947,7 +949,7 @@ const selectTransformTool = () => const ActiveSelectionButton = document.createElement("button"); ActiveSelectionButton.classList.add("button", "tool"); ActiveSelectionButton.textContent = "📄"; - ActiveSelectionButton.title = "Commands Applied to the visible Selection"; + ActiveSelectionButton.title = "Commands Applied to the Current Selection"; ActiveSelectionButton.disabled = true; @@ -1014,7 +1016,7 @@ const selectTransformTool = () => const VisibleSelectionButton = document.createElement("button"); VisibleSelectionButton.classList.add("button", "tool"); VisibleSelectionButton.textContent = "👁"; - VisibleSelectionButton.title = "Commands Applied to the visible Selection"; + VisibleSelectionButton.title = "Commands Applied to All Visible Content In the Selected Area"; VisibleSelectionButton.disabled = true; visibleActionArray.appendChild(saveVisibleSelectionButton); @@ -1104,7 +1106,6 @@ const selectTransformTool = () => array.appendChild(state.ctxmenu.useClipboardLabel); array.appendChild(state.ctxmenu.toNewLayerLabel); - array.appendChild(state.ctxmenu.preserveOriginalLabel); menu.appendChild(array); menu.appendChild(state.ctxmenu.selectionPeekOpacitySlider); From a24bc7eb60444097d6188cfd560dbc0dad833002 Mon Sep 17 00:00:00 2001 From: Metachs <123@example.org> Date: Mon, 26 Aug 2024 15:55:35 -0400 Subject: [PATCH 8/9] Fix error when nothing is selected --- js/ui/tool/select.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index c9d7f33..a1e69e7 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -718,7 +718,7 @@ const selectTransformTool = () => // Only checks if Selection exists and content has been selected // Does not check if content has been transformed, eg for deletion/applying to new layer state.applyTransform = async (eraseSelected = false, clearLayer = false, newLayer = null, keepOriginal = false) => { - const isBlank = + const isBlank = !state.selected || isCanvasBlank( 0, 0, state.selected.canvas.width, state.selected.canvas.height, state.selected.canvas); // Just reset state if nothing is selected, unless Clearing layer From 12c93a1a8709193062fb51eed24191aa7f23b4c1 Mon Sep 17 00:00:00 2001 From: zero01101 Date: Sat, 31 Aug 2024 15:38:31 +0000 Subject: [PATCH 9/9] Fixed resource hashes --- index.html | 8 ++++---- pages/configuration.html | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/index.html b/index.html index d4a3d9a..fbf2dc6 100644 --- a/index.html +++ b/index.html @@ -5,7 +5,7 @@ openOutpaint 🐠 - + @@ -547,7 +547,7 @@ - + @@ -562,7 +562,7 @@ - +