From a97834fda3a96ae5deb9b37fbaa0354e900c06c6 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 13 Jun 2026 16:10:32 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20real-world=20selection=20actions=20?= =?UTF-8?q?=E2=80=94=20scale=20%,=20AI=20edit,=20clipboard=20paste?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend (3 new endpoints under /api/image/): - POST /api/image/scale-selection — scale selected object by any % in-place; LaMa/OpenCV fills the exposed gap so the scene looks natural - POST /api/image/ai-edit-region — AI redraws the masked region via the configured inpaint provider (local_gpu / InvokeAI / ComfyUI / OpenAI) - POST /api/image/paste-into-selection — scales clipboard image to fit the selection bounding box, masks it to the selection shape, composites result Frontend (selection_actions.js + tool integration): - New SelectionActions panel: fixed bottom-center HUD that appears automatically after every SAM selection (click or paint) - Panel actions: Scale by % (default 3%), Make less symmetrical (AI), custom AI Edit prompt, Replace with clipboard, Copy/Cut to layer, Erase - Both smart_select.js and brush_select.js updated to show the panel, add updateLayerWithResult(), and hide panel on clearSelection/on_leave - brush_select: offerFloatSelection() replaced with richer action panel Real-world workflows now supported in one click after painting over object: "Make this 3% bigger" → scale-selection (LaMa fills gap) "Make this less symmetrical" → ai-edit-region with asymmetry prompt "Replace this with what I copied" → paste-into-selection https://claude.ai/code/session_01WVDg7amsy1TTtxvpku7bcM --- backend/app/routers/ai_tools.py | 158 ++++++++++ frontend/src/js/tools/brush_select.js | 44 ++- frontend/src/js/tools/selection_actions.js | 346 +++++++++++++++++++++ frontend/src/js/tools/smart_select.js | 29 +- 4 files changed, 552 insertions(+), 25 deletions(-) create mode 100644 frontend/src/js/tools/selection_actions.js diff --git a/backend/app/routers/ai_tools.py b/backend/app/routers/ai_tools.py index 096903c..be39254 100644 --- a/backend/app/routers/ai_tools.py +++ b/backend/app/routers/ai_tools.py @@ -367,6 +367,164 @@ async def get_config(): } +# ─── Selection image operations ───────────────────────────────────────────── + +class ScaleSelectionRequest(BaseModel): + image: str # base64 full canvas + mask: str # base64 selection mask (white = object) + scale_pct: float = 103.0 # 103 = 3% bigger, 95 = 5% smaller + + +class AiEditRegionRequest(BaseModel): + image: str + mask: str + instruction: str + negative_prompt: str = "" + steps: int = 30 + cfg_scale: float = 7.5 + + +class PasteIntoSelectionRequest(BaseModel): + image: str # base64 target canvas + mask: str # base64 selection mask + paste_image: str # base64 image to paste + + +@router.post("/image/scale-selection") +async def scale_selection(req: ScaleSelectionRequest): + """ + Scale the object selected by mask by scale_pct%, AI-fill the exposed gap. + Works purely with local tools (LaMa/OpenCV) — no remote provider needed. + """ + try: + import numpy as np + from PIL import Image, ImageFilter + except ImportError: + raise HTTPException(status_code=500, detail="PIL/numpy not available") + + img = Image.open(BytesIO(_decode(req.image))).convert("RGB") + mask = Image.open(BytesIO(_decode(req.mask))).convert("L") + if img.size != mask.size: + mask = mask.resize(img.size, Image.LANCZOS) + + mask_arr = np.array(mask) + ys, xs = np.where(mask_arr > 128) + if len(xs) == 0: + raise HTTPException(status_code=400, detail="Empty mask — nothing to scale") + + minx, maxx = int(xs.min()), int(xs.max()) + miny, maxy = int(ys.min()), int(ys.max()) + cx, cy = (minx + maxx) / 2.0, (miny + maxy) / 2.0 + obj_w, obj_h = maxx - minx + 1, maxy - miny + 1 + + scale = req.scale_pct / 100.0 + new_w = max(1, round(obj_w * scale)) + new_h = max(1, round(obj_h * scale)) + + # Extract masked object crop (RGBA with mask as alpha) + img_rgba = img.convert("RGBA") + obj_crop = img_rgba.crop((minx, miny, maxx + 1, maxy + 1)) + mask_crop = mask.crop((minx, miny, maxx + 1, maxy + 1)) + r, g, b, _ = obj_crop.split() + obj_masked = Image.merge("RGBA", (r, g, b, mask_crop)) + scaled_obj = obj_masked.resize((new_w, new_h), Image.LANCZOS) + + # AI-fill the original mask area (gap) with LaMa/OpenCV + gap_mask = mask.filter(ImageFilter.MaxFilter(9)) # expand ~4px for clean seam + gap_bytes = BytesIO() + img.save(gap_bytes, format="PNG") + gap_mask_bytes = BytesIO() + gap_mask.save(gap_mask_bytes, format="PNG") + + try: + if lama_available(): + filled_bytes = await asyncio.get_event_loop().run_in_executor( + None, lama_inpaint, gap_bytes.getvalue(), gap_mask_bytes.getvalue() + ) + else: + filled_bytes = await asyncio.get_event_loop().run_in_executor( + None, opencv_inpaint, gap_bytes.getvalue(), gap_mask_bytes.getvalue() + ) + filled = Image.open(BytesIO(filled_bytes)).convert("RGBA") + except Exception as exc: + print(f"[scale-selection] fill fallback: {exc}") + filled = img.convert("RGBA") + + # Paste scaled object centered on original centroid + px = round(cx - new_w / 2) + py = round(cy - new_h / 2) + result = filled.copy() + result.paste(scaled_obj, (px, py), scaled_obj.split()[3]) + + out = BytesIO() + result.convert("RGB").save(out, format="PNG") + return {"result": _encode(out.getvalue())} + + +@router.post("/image/ai-edit-region") +async def ai_edit_region(req: AiEditRegionRequest): + """ + AI-edit the selected region using the configured inpaint provider. + Works with local_gpu, InvokeAI, ComfyUI, or OpenAI. + """ + provider = _require_remote("inpaint") + result_bytes = await provider.inpaint( + _decode(req.image), + _decode(req.mask), + req.instruction, + {"negative_prompt": req.negative_prompt, "steps": req.steps, "cfg_scale": req.cfg_scale}, + ) + return {"result": _encode(result_bytes)} + + +@router.post("/image/paste-into-selection") +async def paste_into_selection(req: PasteIntoSelectionRequest): + """ + Scale a clipboard image to the selection bounding box, mask it to the + selection shape, and composite it over the original canvas. + """ + try: + import numpy as np + from PIL import Image + except ImportError: + raise HTTPException(status_code=500, detail="PIL/numpy not available") + + img = Image.open(BytesIO(_decode(req.image))).convert("RGBA") + mask = Image.open(BytesIO(_decode(req.mask))).convert("L") + paste_img = Image.open(BytesIO(_decode(req.paste_image))).convert("RGBA") + + if img.size != mask.size: + mask = mask.resize(img.size, Image.LANCZOS) + + mask_arr = np.array(mask) + ys, xs = np.where(mask_arr > 128) + if len(xs) == 0: + raise HTTPException(status_code=400, detail="Empty mask") + + minx, maxx = int(xs.min()), int(xs.max()) + miny, maxy = int(ys.min()), int(ys.max()) + target_w = maxx - minx + 1 + target_h = maxy - miny + 1 + + # Scale clipboard image to fit the selection bounding box + paste_scaled = paste_img.resize((target_w, target_h), Image.LANCZOS) + + # Clip paste to selection shape using mask + mask_crop = mask.crop((minx, miny, maxx + 1, maxy + 1)) + r, g, b, a = paste_scaled.split() + mask_np = np.array(mask_crop) + alpha_np = np.array(a) + combined = (alpha_np.astype(np.uint16) * mask_np.astype(np.uint16) // 255).astype(np.uint8) + paste_final = Image.merge("RGBA", (r, g, b, Image.fromarray(combined))) + + result = img.copy() + result.paste(paste_final, (minx, miny), paste_final.split()[3]) + + out = BytesIO() + result.convert("RGB").save(out, format="PNG") + return {"result": _encode(out.getvalue())} + + # ─── SAM (Segment Anything) ────────────────────────────────────────────────── class SegmentPointRequest(BaseModel): diff --git a/frontend/src/js/tools/brush_select.js b/frontend/src/js/tools/brush_select.js index 6d6d4b9..9302cd3 100644 --- a/frontend/src/js/tools/brush_select.js +++ b/frontend/src/js/tools/brush_select.js @@ -11,6 +11,7 @@ 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'; import apiService from './../services/api.js'; +import { SelectionActions, updateLayerWithResult } from './selection_actions.js'; class Brush_select_class extends Base_tools_class { @@ -39,6 +40,9 @@ class Brush_select_class extends Base_tools_class { // Processing state this.isProcessing = false; + + // Quick-action panel shown after selection + this.selectionActions = new SelectionActions(this); } load() { @@ -300,31 +304,23 @@ class Brush_select_class extends Base_tools_class { } /** - * Offer to float the selection to a new layer for manipulation (Canva-like workflow) + * Show quick-action panel after selection (AI operations, scale, clipboard paste, etc.) */ offerFloatSelection() { - var _this = this; + var imageData = this.getLayerImageData(); + var maskData = this.maskCanvas + ? this.maskCanvas.toDataURL('image/png').split(',')[1] + : null; + if (maskData) { + this.selectionActions.show(imageData, maskData); + } + } - alertify.confirm( - 'Selection Complete', - 'Would you like to move/scale this selection? This will copy it to a new layer.', - function() { - // Yes - copy to layer and switch to Select tool - _this.copyToLayer(); - - // Switch to Select tool - setTimeout(function() { - var selectTool = document.querySelector('.sidebar_left .item[data-tool="select"]'); - if (selectTool) { - selectTool.click(); - } - }, 100); - }, - function() { - // No - just keep the selection - alertify.message('Tip: Use Ctrl+C to copy or Ctrl+X to cut the selection.'); - } - ).set('labels', {ok: 'Yes, Move/Scale', cancel: 'Keep Selection'}); + /** + * Update the current layer canvas with a base64 result from a backend operation. + */ + updateLayerWithResult(base64) { + updateLayerWithResult(base64, this); } /** @@ -781,6 +777,7 @@ class Brush_select_class extends Base_tools_class { } clearSelection() { + this.selectionActions.hide(); this.currentMask = null; this.maskCanvas = null; this.edgeCanvas = null; @@ -793,8 +790,9 @@ class Brush_select_class extends Base_tools_class { } on_leave() { + this.selectionActions.hide(); this.isDrawing = false; - this.isProcessing = false; // Reset processing state when leaving tool + this.isProcessing = false; this.brushPath = []; return []; } diff --git a/frontend/src/js/tools/selection_actions.js b/frontend/src/js/tools/selection_actions.js new file mode 100644 index 0000000..af6f9b7 --- /dev/null +++ b/frontend/src/js/tools/selection_actions.js @@ -0,0 +1,346 @@ +/** + * SelectionActions — floating quick-action panel that appears after a SAM selection. + * + * Surfaces high-value real-world workflows directly in the UI: + * • Scale by % — make object 3% (or any %) bigger/smaller, gap AI-filled + * • Make less symmetrical — AI redraws the region with organic variation + * • Replace with clipboard — paste clipboard image into the selection shape + * • Copy / Cut to layer — classic Photoshop workflow + * • AI Edit (custom prompt) — full inpaint with user text + * + * Usage: + * this.selectionActions = new SelectionActions(this); + * // after successful selection: + * this.selectionActions.show(imageBase64, maskBase64); + */ + +import app from './../app.js'; +import config from './../config.js'; +import Base_layers_class from './../core/base-layers.js'; +import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js'; + +const BASE = window.API_BASE_URL || ''; + +export class SelectionActions { + constructor(tool) { + this.tool = tool; + this.Base_layers = new Base_layers_class(); + this._panel = null; + this._imageData = null; + this._maskData = null; + this._escHandler = null; + } + + show(imageBase64, maskBase64) { + this.hide(); + this._imageData = imageBase64; + this._maskData = maskBase64; + + var panel = document.createElement('div'); + panel.id = 'sel-actions-panel'; + panel.style.cssText = [ + 'position:fixed', + 'bottom:80px', + 'left:50%', + 'transform:translateX(-50%)', + 'background:#1a1a2e', + 'border:1px solid #3a3a6a', + 'border-radius:12px', + 'padding:14px 16px', + 'z-index:10000', + 'font-family:sans-serif', + 'font-size:12px', + 'color:#d0d0e0', + 'min-width:340px', + 'box-shadow:0 8px 32px rgba(0,0,0,0.7)', + 'display:flex', + 'flex-direction:column', + 'gap:6px', + ].join(';'); + + // ── Title row ──────────────────────────────────────────────────────── + var titleRow = document.createElement('div'); + titleRow.style.cssText = 'display:flex;align-items:center;justify-content:space-between;margin-bottom:4px'; + var title = document.createElement('span'); + title.textContent = 'Selection Actions'; + title.style.cssText = 'font-size:13px;font-weight:bold;color:#aaaaff'; + var closeX = document.createElement('button'); + closeX.textContent = '✕'; + closeX.style.cssText = 'background:none;border:none;color:#666;cursor:pointer;font-size:14px;padding:0;line-height:1'; + closeX.title = 'Close panel (keep selection)'; + closeX.onclick = () => this.hide(); + titleRow.appendChild(title); + titleRow.appendChild(closeX); + panel.appendChild(titleRow); + + // ── Scale by % ─────────────────────────────────────────────────────── + var scaleRow = document.createElement('div'); + scaleRow.style.cssText = 'display:flex;align-items:center;gap:6px;background:#16213e;border-radius:7px;padding:7px 10px'; + var scaleLabel = document.createElement('span'); + scaleLabel.textContent = 'Scale by'; + scaleLabel.style.color = '#aaa'; + var scaleInput = document.createElement('input'); + scaleInput.type = 'number'; + scaleInput.value = '103'; + scaleInput.min = '1'; + scaleInput.max = '500'; + scaleInput.title = '103 = 3% bigger · 95 = 5% smaller'; + scaleInput.style.cssText = 'width:52px;background:#0f0f1a;color:#fff;border:1px solid #4a4a8a;border-radius:4px;padding:2px 5px;font-size:12px'; + var scaleUnit = document.createElement('span'); + scaleUnit.textContent = '%'; + scaleUnit.style.color = '#888'; + var scaleBtn = _btn('Apply', '#1a2a4a', '#8aacff'); + scaleBtn.style.marginLeft = 'auto'; + scaleBtn.onclick = () => { + var pct = parseFloat(scaleInput.value) || 103; + this._scaleSelection(pct); + }; + scaleRow.appendChild(scaleLabel); + scaleRow.appendChild(scaleInput); + scaleRow.appendChild(scaleUnit); + scaleRow.appendChild(scaleBtn); + panel.appendChild(scaleRow); + + // ── AI actions ─────────────────────────────────────────────────────── + panel.appendChild( + _actionBtn('Make less symmetrical', '#1c1a2e', '#cc99ff', + '⟳ AI redraws the region with natural, organic asymmetry', + () => this._makeAsymmetric()) + ); + panel.appendChild( + _actionBtn('Replace with clipboard', '#1a2a1a', '#88dd88', + '📋 Scales your clipboard image into the selection shape', + () => this._pasteFromClipboard()) + ); + + // ── Custom AI edit prompt ───────────────────────────────────────────── + var aiRow = document.createElement('div'); + aiRow.style.cssText = 'display:flex;align-items:center;gap:6px;background:#16213e;border-radius:7px;padding:7px 10px'; + var aiInput = document.createElement('input'); + aiInput.type = 'text'; + aiInput.placeholder = 'AI edit: "add a scar", "make it look aged", …'; + aiInput.style.cssText = 'flex:1;background:#0f0f1a;color:#fff;border:1px solid #4a4a8a;border-radius:4px;padding:3px 7px;font-size:11px'; + var aiBtn = _btn('Edit', '#1a2a4a', '#8aacff'); + aiBtn.onclick = () => { + var instruction = aiInput.value.trim(); + if (!instruction) { alertify.warning('Enter an AI edit instruction first.'); return; } + this._aiEditRegion(instruction); + }; + aiRow.appendChild(aiInput); + aiRow.appendChild(aiBtn); + panel.appendChild(aiRow); + + // ── Divider ────────────────────────────────────────────────────────── + var hr = document.createElement('div'); + hr.style.cssText = 'border-top:1px solid #2a2a4a;margin:2px 0'; + panel.appendChild(hr); + + // ── Classic selection ops ───────────────────────────────────────────── + var classicRow = document.createElement('div'); + classicRow.style.cssText = 'display:flex;gap:6px'; + var copyBtn = _btn('Copy to layer', '#1a2a1a', '#88cc88'); + copyBtn.style.flex = '1'; + copyBtn.title = 'Ctrl+C'; + copyBtn.onclick = () => { this.tool.copyToLayer(); this.hide(); }; + var cutBtn = _btn('Cut to layer', '#2a1a1a', '#cc8888'); + cutBtn.style.flex = '1'; + cutBtn.title = 'Ctrl+X'; + cutBtn.onclick = () => { this.tool.cutToLayer(); this.hide(); }; + var delBtn = _btn('Erase', '#2a1a1a', '#ff7766'); + delBtn.style.flex = '0 0 auto'; + delBtn.title = 'Delete key'; + delBtn.onclick = () => { this.tool.deleteSelection(); this.hide(); }; + classicRow.appendChild(copyBtn); + classicRow.appendChild(cutBtn); + classicRow.appendChild(delBtn); + panel.appendChild(classicRow); + + document.body.appendChild(panel); + this._panel = panel; + + this._escHandler = (e) => { if (e.key === 'Escape') this.hide(); }; + document.addEventListener('keydown', this._escHandler); + } + + hide() { + if (this._panel) { this._panel.remove(); this._panel = null; } + if (this._escHandler) { + document.removeEventListener('keydown', this._escHandler); + this._escHandler = null; + } + } + + // ── Actions ───────────────────────────────────────────────────────────── + + async _scaleSelection(scalePct) { + if (!this._check()) return; + this.hide(); + alertify.message('Scaling object and filling gap…'); + try { + var res = await _post('/api/image/scale-selection', { + image: this._imageData, + mask: this._maskData, + scale_pct: scalePct, + }); + this.tool.updateLayerWithResult(res.result); + this.tool.clearSelection(); + alertify.success('Scaled by ' + scalePct + '%!'); + } catch (e) { + alertify.error('Scale failed: ' + e.message); + } + } + + async _makeAsymmetric() { + if (!this._check()) return; + this.hide(); + alertify.message('AI is adding natural asymmetry…'); + try { + var res = await _post('/api/image/ai-edit-region', { + image: this._imageData, + mask: this._maskData, + instruction: 'natural asymmetry, slight organic variation, realistic, subtle imperfection', + negative_prompt:'perfectly symmetric, mirror image, artificial, identical halves', + steps: 30, + cfg_scale: 7.5, + }); + this.tool.updateLayerWithResult(res.result); + this.tool.clearSelection(); + alertify.success('Made less symmetrical!'); + } catch (e) { + alertify.error('AI edit failed: ' + e.message); + } + } + + async _aiEditRegion(instruction) { + if (!this._check()) return; + this.hide(); + alertify.message('AI is editing the region…'); + try { + var res = await _post('/api/image/ai-edit-region', { + image: this._imageData, + mask: this._maskData, + instruction: instruction, + steps: 30, + cfg_scale: 7.5, + }); + this.tool.updateLayerWithResult(res.result); + this.tool.clearSelection(); + alertify.success('Done!'); + } catch (e) { + alertify.error('AI edit failed: ' + e.message); + } + } + + async _pasteFromClipboard() { + if (!this._check()) return; + + if (!navigator.clipboard || !navigator.clipboard.read) { + alertify.error('Clipboard API not available. Use HTTPS or enable clipboard permissions.'); + return; + } + try { + var items = await navigator.clipboard.read(); + var clipBlob = null; + for (var item of items) { + for (var type of item.types) { + if (type.startsWith('image/')) { + clipBlob = await item.getType(type); + break; + } + } + if (clipBlob) break; + } + if (!clipBlob) { + alertify.error('No image in clipboard. Copy an image first (e.g., right-click → Copy image).'); + return; + } + + var clipBase64 = await _blobToBase64(clipBlob); + this.hide(); + alertify.message('Pasting clipboard into selection…'); + + var res = await _post('/api/image/paste-into-selection', { + image: this._imageData, + mask: this._maskData, + paste_image: clipBase64, + }); + this.tool.updateLayerWithResult(res.result); + this.tool.clearSelection(); + alertify.success('Clipboard pasted into selection!'); + } catch (e) { + alertify.error('Paste failed: ' + e.message); + } + } + + _check() { + if (!this._imageData || !this._maskData) { + alertify.error('No selection data. Make a new selection first.'); + return false; + } + return true; + } +} + +// ── Shared method: patch into both smart_select and brush_select instances ─── + +/** + * Update the active layer canvas with a base64 result image from the backend. + * Call as `this.updateLayerWithResult(base64)` on any tool that extends Base_tools_class. + */ +export function updateLayerWithResult(base64, tool) { + var img = new Image(); + img.onload = function () { + var canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + canvas.getContext('2d').drawImage(img, 0, 0); + + app.State.do_action( + new app.Actions.Bundle_action('ai_transform', 'AI Transform', [ + new app.Actions.Update_layer_image_action(canvas, config.layer.id) + ]) + ); + // Trigger re-render + config.need_render = true; + }; + img.src = 'data:image/png;base64,' + base64; +} + +// ── Private helpers ────────────────────────────────────────────────────────── + +function _btn(text, bg, color) { + var b = document.createElement('button'); + b.textContent = text; + b.style.cssText = 'background:' + bg + ';color:' + color + ';border:1px solid #3a3a6a;padding:4px 10px;border-radius:5px;cursor:pointer;font-size:11px;white-space:nowrap'; + return b; +} + +function _actionBtn(text, bg, color, tooltip, handler) { + var b = _btn(text, bg, color); + b.style.cssText += ';display:block;width:100%;text-align:left;padding:7px 10px;border-radius:7px;font-size:12px'; + if (tooltip) b.title = tooltip; + b.onclick = handler; + return b; +} + +async function _post(path, body) { + var r = await fetch(BASE + path, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + if (!r.ok) { + var err = await r.json().catch(() => ({ detail: r.statusText })); + throw new Error(err.detail || 'Request failed'); + } + return r.json(); +} + +function _blobToBase64(blob) { + return new Promise((resolve, reject) => { + var reader = new FileReader(); + reader.onload = (e) => resolve(e.target.result.split(',')[1]); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); +} diff --git a/frontend/src/js/tools/smart_select.js b/frontend/src/js/tools/smart_select.js index 643447c..ce0829d 100644 --- a/frontend/src/js/tools/smart_select.js +++ b/frontend/src/js/tools/smart_select.js @@ -13,6 +13,7 @@ import Helper_class from './../libs/helpers.js'; import Dialog_class from './../libs/popup.js'; import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js'; import apiService from './../services/api.js'; +import { SelectionActions, updateLayerWithResult } from './selection_actions.js'; class Smart_select_class extends Base_tools_class { @@ -35,6 +36,9 @@ class Smart_select_class extends Base_tools_class { // Edge canvas for drawing the mask outline this.edgeCanvas = null; + + // Quick-action panel shown after selection + this.selectionActions = new SelectionActions(this); } load() { @@ -146,7 +150,7 @@ class Smart_select_class extends Base_tools_class { 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.'); + this._showActionPanel(); } } catch (error) { @@ -633,10 +637,31 @@ class Smart_select_class extends Base_tools_class { alertify.success('Selection deleted!'); } + /** + * Show the quick-action panel for the current selection. + */ + _showActionPanel() { + var imageData = this.getLayerImageData(); + var maskData = this.maskCanvas + ? this.maskCanvas.toDataURL('image/png').split(',')[1] + : null; + if (maskData) { + this.selectionActions.show(imageData, maskData); + } + } + + /** + * Update the current layer canvas with a base64 result from a backend operation. + */ + updateLayerWithResult(base64) { + updateLayerWithResult(base64, this); + } + /** * Clear the current selection */ clearSelection() { + this.selectionActions.hide(); this.currentMask = null; this.maskCanvas = null; this.edgeCanvas = null; @@ -647,7 +672,7 @@ class Smart_select_class extends Base_tools_class { } on_leave() { - // Don't clear mask when switching tools - AI inpaint needs it + this.selectionActions.hide(); return []; } }