diff --git a/backend/app/routers/tools.py b/backend/app/routers/tools.py
index 86a81d5..b9bb257 100644
--- a/backend/app/routers/tools.py
+++ b/backend/app/routers/tools.py
@@ -8,6 +8,7 @@ import numpy as np
import json
import base64
import cv2
+from pydantic import BaseModel
from app.database import get_db
from app.models.project import Project
@@ -16,6 +17,115 @@ from app.schemas import StatusResponse
router = APIRouter(prefix="/tools", tags=["tools"])
+# Pydantic models for JSON API
+class SmartSelectRequest(BaseModel):
+ image: str # Base64 encoded image
+ point_x: int
+ point_y: int
+
+
+class InpaintRequest(BaseModel):
+ image: str # Base64 encoded image
+ mask: str # Base64 encoded mask
+ prompt: str
+ negative_prompt: Optional[str] = ""
+ strength: Optional[float] = 0.8
+ guidance_scale: Optional[float] = 7.5
+
+
+@router.post("/smart-select-base64")
+async def smart_select_base64(request: SmartSelectRequest):
+ """
+ Smart select using base64 encoded image (no project required).
+ Used by miniPaint frontend.
+ """
+ try:
+ # Decode base64 image
+ image_bytes = base64.b64decode(request.image)
+ img = Image.open(BytesIO(image_bytes)).convert('RGB')
+ img_array = np.array(img)
+
+ # Run SAM selection
+ try:
+ mask = await _sam_select(img_array, request.point_x, request.point_y)
+ except Exception as e:
+ print(f"SAM not available, using flood fill: {e}")
+ mask = _flood_fill_select(img_array, request.point_x, request.point_y)
+
+ # Convert mask to base64 PNG
+ mask_img = Image.fromarray((mask * 255).astype(np.uint8), mode='L')
+ buffer = BytesIO()
+ mask_img.save(buffer, format='PNG')
+ mask_b64 = base64.b64encode(buffer.getvalue()).decode('utf-8')
+
+ # Get polygon and bbox
+ polygon, bbox = _mask_to_polygon(mask)
+
+ return {
+ "mask": mask_b64,
+ "polygon": polygon,
+ "bbox": bbox
+ }
+
+ except Exception as e:
+ raise HTTPException(status_code=500, detail=str(e))
+
+
+@router.post("/inpaint")
+async def inpaint_base64(request: InpaintRequest):
+ """
+ AI inpainting using base64 encoded image and mask.
+ Used by miniPaint frontend.
+ """
+ try:
+ # Decode base64 image and mask
+ image_bytes = base64.b64decode(request.image)
+ mask_bytes = base64.b64decode(request.mask)
+
+ img = Image.open(BytesIO(image_bytes)).convert('RGB')
+ mask_img = Image.open(BytesIO(mask_bytes)).convert('L')
+
+ # Resize mask to match image if needed
+ if mask_img.size != img.size:
+ mask_img = mask_img.resize(img.size, Image.Resampling.LANCZOS)
+
+ # Get the AI provider and run inpainting
+ from app.services.ai_provider import get_ai_provider
+ from app.config import settings
+
+ provider = get_ai_provider()
+
+ # Convert images to format expected by provider
+ img_buffer = BytesIO()
+ img.save(img_buffer, format='PNG')
+ img_buffer.seek(0)
+
+ mask_buffer = BytesIO()
+ mask_img.save(mask_buffer, format='PNG')
+ mask_buffer.seek(0)
+
+ # Run inpainting
+ result_bytes = await provider.inpaint(
+ image=img_buffer,
+ mask=mask_buffer,
+ prompt=request.prompt,
+ negative_prompt=request.negative_prompt,
+ strength=request.strength
+ )
+
+ # Convert result to base64
+ result_b64 = base64.b64encode(result_bytes).decode('utf-8')
+
+ return {
+ "result": result_b64
+ }
+
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ raise HTTPException(status_code=500, detail=str(e))
+
+
@router.post("/remove-background")
async def remove_background(
project_id: Optional[int] = Form(None),
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index 2f552b7..b462921 100644
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -3,22 +3,25 @@ FROM node:20-alpine as build
WORKDIR /app
# Copy package files
-COPY package.json ./
+COPY package.json package-lock.json ./
# Install dependencies
-RUN npm install
+RUN npm ci
# Copy source
COPY . .
-# Build app
+# Build webpack bundle
RUN npm run build
# Production stage
FROM nginx:alpine
-# Copy built files
-COPY --from=build /app/dist /usr/share/nginx/html
+# Copy all static files needed by miniPaint
+COPY --from=build /app/index.html /usr/share/nginx/html/
+COPY --from=build /app/dist /usr/share/nginx/html/dist
+COPY --from=build /app/images /usr/share/nginx/html/images
+COPY --from=build /app/src/css /usr/share/nginx/html/src/css
# Copy nginx config
COPY nginx.conf /etc/nginx/conf.d/default.conf
diff --git a/frontend/images/icons/ai_inpaint.svg b/frontend/images/icons/ai_inpaint.svg
new file mode 100644
index 0000000..28f4a2e
--- /dev/null
+++ b/frontend/images/icons/ai_inpaint.svg
@@ -0,0 +1,12 @@
+
diff --git a/frontend/images/icons/smart_select.svg b/frontend/images/icons/smart_select.svg
new file mode 100644
index 0000000..7dba581
--- /dev/null
+++ b/frontend/images/icons/smart_select.svg
@@ -0,0 +1,15 @@
+
diff --git a/frontend/nginx.conf b/frontend/nginx.conf
index 530bfeb..6fd904d 100644
--- a/frontend/nginx.conf
+++ b/frontend/nginx.conf
@@ -70,6 +70,18 @@ server {
proxy_pass http://backend:8000;
}
+ # API prefix for frontend (maps /api/tools to /tools, etc.)
+ location /api/ {
+ rewrite ^/api/(.*)$ /$1 break;
+ proxy_pass http://backend:8000;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection 'upgrade';
+ proxy_set_header Host $host;
+ proxy_cache_bypass $http_upgrade;
+ proxy_read_timeout 300s;
+ }
+
location /docs {
proxy_pass http://backend:8000;
}
diff --git a/frontend/src/css/layout.css b/frontend/src/css/layout.css
index 119fd77..ae907e7 100644
--- a/frontend/src/css/layout.css
+++ b/frontend/src/css/layout.css
@@ -321,6 +321,8 @@ IMPORTANT: any new icon should also must be added on /service-worker.js + its ve
.sidebar_left .desaturate:after{ background-image: url('images/icons/desaturate.svg'); }
.sidebar_left .bulge_pinch:after{ background-image: url('images/icons/bulge_pinch.svg'); }
.sidebar_left .animation:after{ background-image: url('images/icons/animation.svg'); }
+.sidebar_left .smart_select:after{ background-image: url('images/icons/smart_select.svg'); }
+.sidebar_left .ai_inpaint:after{ background-image: url('images/icons/ai_inpaint.svg'); }
@media screen and (max-width:550px){
#sidebar_left{
diff --git a/frontend/src/js/config.js b/frontend/src/js/config.js
index b4056fa..4ba089e 100644
--- a/frontend/src/js/config.js
+++ b/frontend/src/js/config.js
@@ -92,6 +92,17 @@ config.TOOLS = [
attributes: {},
on_leave: 'on_leave',
},
+ {
+ name: 'smart_select',
+ title: 'Smart Select (AI)',
+ attributes: {},
+ },
+ {
+ name: 'ai_inpaint',
+ title: 'AI Inpaint',
+ on_activate: 'on_activate',
+ attributes: {},
+ },
{
name: 'brush',
attributes: {
diff --git a/frontend/src/js/services/api.js b/frontend/src/js/services/api.js
new file mode 100644
index 0000000..d219568
--- /dev/null
+++ b/frontend/src/js/services/api.js
@@ -0,0 +1,88 @@
+/**
+ * API Service for communicating with the FastAPI backend
+ * Handles SAM selection and AI inpainting requests
+ */
+
+class ApiService {
+ constructor() {
+ // Backend API base URL - adjust for your deployment
+ this.baseUrl = window.API_BASE_URL || '/api';
+ }
+
+ /**
+ * Call SAM (Segment Anything Model) for smart selection
+ * @param {string} imageData - Base64 encoded image data
+ * @param {number} pointX - X coordinate of click point
+ * @param {number} pointY - Y coordinate of click point
+ * @returns {Promise<{mask: ImageData, polygon: Array}>}
+ */
+ async smartSelect(imageData, pointX, pointY) {
+ const response = await fetch(`${this.baseUrl}/tools/smart-select-base64`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ image: imageData,
+ point_x: pointX,
+ point_y: pointY,
+ }),
+ });
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
+ throw new Error(error.detail || `SAM request failed: ${response.status}`);
+ }
+
+ return response.json();
+ }
+
+ /**
+ * Call AI inpainting to edit a selected region
+ * @param {string} imageData - Base64 encoded image data
+ * @param {string} maskData - Base64 encoded mask data (white = area to edit)
+ * @param {string} prompt - Text prompt describing desired edit
+ * @param {Object} options - Additional options
+ * @returns {Promise<{result: string}>} - Base64 encoded result image
+ */
+ async inpaint(imageData, maskData, prompt, options = {}) {
+ const response = await fetch(`${this.baseUrl}/tools/inpaint`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ image: imageData,
+ mask: maskData,
+ prompt: prompt,
+ negative_prompt: options.negativePrompt || '',
+ strength: options.strength || 0.8,
+ guidance_scale: options.guidanceScale || 7.5,
+ }),
+ });
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({ detail: 'Unknown error' }));
+ throw new Error(error.detail || `Inpaint request failed: ${response.status}`);
+ }
+
+ return response.json();
+ }
+
+ /**
+ * Health check for the backend
+ * @returns {Promise}
+ */
+ async healthCheck() {
+ try {
+ const response = await fetch(`${this.baseUrl}/health`);
+ return response.ok;
+ } catch {
+ return false;
+ }
+ }
+}
+
+// Singleton instance
+const apiService = new ApiService();
+export default apiService;
diff --git a/frontend/src/js/tools/ai_inpaint.js b/frontend/src/js/tools/ai_inpaint.js
new file mode 100644
index 0000000..b346829
--- /dev/null
+++ b/frontend/src/js/tools/ai_inpaint.js
@@ -0,0 +1,315 @@
+/**
+ * AI Inpaint Tool - Edit selected regions using AI with text prompts
+ * Works with Smart Select tool's mask or manual 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 Dialog_class from './../libs/popup.js';
+import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js';
+import apiService from './../services/api.js';
+
+class Ai_inpaint_class extends Base_tools_class {
+
+ constructor(ctx) {
+ super();
+ this.Base_layers = new Base_layers_class();
+ this.Helper = new Helper_class();
+ this.POP = new Dialog_class();
+ this.ctx = ctx;
+ this.name = 'ai_inpaint';
+ this.isProcessing = false;
+ }
+
+ load() {
+ // No mouse events needed - this tool uses a dialog
+ }
+
+ on_activate() {
+ this.showInpaintDialog();
+ }
+
+ /**
+ * Show the inpainting dialog
+ */
+ showInpaintDialog() {
+ var _this = this;
+
+ // Check if we have a selection
+ var hasMask = window.smartSelectMask != null;
+ var hasRectSelection = this.getRectSelection() != null;
+
+ if (!hasMask && !hasRectSelection) {
+ alertify.warning('No selection found. Use Smart Select or Selection tool first.');
+ return;
+ }
+
+ var settings = {
+ title: 'AI Inpaint',
+ params: [
+ {
+ name: "prompt",
+ title: "Describe what you want:",
+ type: "textarea",
+ value: "",
+ placeholder: "e.g., 'a red rose', 'remove the object', 'blue sky with clouds'"
+ },
+ {
+ name: "negative_prompt",
+ title: "What to avoid (optional):",
+ value: "",
+ placeholder: "e.g., 'blurry, distorted, low quality'"
+ },
+ {
+ name: "strength",
+ title: "Edit Strength:",
+ type: "range",
+ value: 80,
+ range: [1, 100],
+ step: 1
+ }
+ ],
+ on_finish: async function (params) {
+ await _this.executeInpaint(params);
+ },
+ };
+
+ this.POP.show(settings);
+ }
+
+ /**
+ * Execute the inpainting operation
+ */
+ async executeInpaint(params) {
+ if (this.isProcessing) {
+ alertify.warning('Already processing... please wait');
+ return;
+ }
+
+ if (!params.prompt || params.prompt.trim() === '') {
+ alertify.error('Please enter a prompt describing what you want');
+ return;
+ }
+
+ // Check if we have an image layer
+ if (config.layer.type != 'image') {
+ alertify.error('Please select an image layer');
+ return;
+ }
+
+ this.isProcessing = true;
+ alertify.message('AI is generating... this may take a moment');
+
+ try {
+ // Get image data
+ var imageData = this.getLayerImageData();
+
+ // Get mask data (from Smart Select or rectangular selection)
+ var maskData = this.getMaskData();
+
+ if (!maskData) {
+ throw new Error('No valid selection/mask found');
+ }
+
+ // Call inpaint API
+ var result = await apiService.inpaint(
+ imageData,
+ maskData,
+ params.prompt,
+ {
+ negativePrompt: params.negative_prompt || '',
+ strength: params.strength / 100
+ }
+ );
+
+ // Apply result to layer
+ await this.applyResult(result.result);
+
+ alertify.success('Inpainting complete!');
+
+ } catch (error) {
+ console.error('Inpaint error:', error);
+ alertify.error('Inpainting failed: ' + error.message);
+ } finally {
+ this.isProcessing = false;
+ }
+ }
+
+ /**
+ * Get the current layer's image data as base64
+ */
+ getLayerImageData() {
+ var canvas = document.createElement('canvas');
+ var ctx = canvas.getContext('2d');
+
+ canvas.width = config.layer.width_original;
+ canvas.height = config.layer.height_original;
+ ctx.drawImage(config.layer.link, 0, 0);
+
+ return canvas.toDataURL('image/png').split(',')[1];
+ }
+
+ /**
+ * Get mask data - either from Smart Select or rectangular selection
+ */
+ getMaskData() {
+ // First try Smart Select mask
+ if (window.smartSelectMask && window.smartSelectMask.canvas) {
+ var maskCanvas = window.smartSelectMask.canvas;
+ return maskCanvas.toDataURL('image/png').split(',')[1];
+ }
+
+ // Fall back to rectangular selection
+ var selection = this.getRectSelection();
+ if (selection) {
+ return this.createRectMask(selection);
+ }
+
+ return null;
+ }
+
+ /**
+ * Get rectangular selection from miniPaint's selection tool
+ */
+ getRectSelection() {
+ // Try to get selection from selection tool
+ var Selection = null;
+ try {
+ var GUI_tools = app.GUI?.GUI_tools || this.Base_layers?.Base_gui?.GUI_tools;
+ if (GUI_tools && GUI_tools.tools_modules && GUI_tools.tools_modules.selection) {
+ Selection = GUI_tools.tools_modules.selection.object;
+ }
+ } catch (e) {
+ // Selection tool not available
+ }
+
+ if (Selection && Selection.selection &&
+ Selection.selection.width > 0 && Selection.selection.height > 0) {
+ return Selection.selection;
+ }
+
+ return null;
+ }
+
+ /**
+ * Create a white rectangle mask from selection coordinates
+ */
+ createRectMask(selection) {
+ var canvas = document.createElement('canvas');
+ var ctx = canvas.getContext('2d');
+
+ canvas.width = config.layer.width_original;
+ canvas.height = config.layer.height_original;
+
+ // Fill with black (unselected)
+ ctx.fillStyle = '#000000';
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ // Calculate selection position relative to layer
+ var x = selection.x - config.layer.x;
+ var y = selection.y - config.layer.y;
+ var width = selection.width;
+ var height = selection.height;
+
+ // Scale to original image size
+ var scaleX = config.layer.width_original / config.layer.width;
+ var scaleY = config.layer.height_original / config.layer.height;
+
+ x = x * scaleX;
+ y = y * scaleY;
+ width = width * scaleX;
+ height = height * scaleY;
+
+ // Draw white rectangle (selected area)
+ ctx.fillStyle = '#FFFFFF';
+ ctx.fillRect(x, y, width, height);
+
+ return canvas.toDataURL('image/png').split(',')[1];
+ }
+
+ /**
+ * Apply the inpainted result to the current layer
+ */
+ async applyResult(resultBase64) {
+ var _this = this;
+
+ return new Promise((resolve, reject) => {
+ var img = new Image();
+ img.onload = function() {
+ // Create canvas with result
+ var canvas = document.createElement('canvas');
+ canvas.width = config.layer.width_original;
+ canvas.height = config.layer.height_original;
+ var ctx = canvas.getContext('2d');
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
+
+ // Update layer through action system for undo support
+ app.State.do_action(
+ new app.Actions.Bundle_action('ai_inpaint', 'AI Inpaint', [
+ new app.Actions.Update_layer_image_action(canvas)
+ ])
+ );
+
+ // Clear the smart select mask
+ window.smartSelectMask = null;
+
+ config.need_render = true;
+ resolve();
+ };
+ img.onerror = function() {
+ reject(new Error('Failed to load result image'));
+ };
+ img.src = 'data:image/png;base64,' + resultBase64;
+ });
+ }
+
+ render_overlay(ctx) {
+ // Show visual indicator if there's a selection ready for inpainting
+ if (window.smartSelectMask && window.smartSelectMask.canvas) {
+ // Draw a subtle border around the tool indicating mask is ready
+ ctx.save();
+ ctx.strokeStyle = '#00ff00';
+ ctx.lineWidth = 2;
+ ctx.setLineDash([5, 5]);
+
+ var scaleX = config.layer.width / config.layer.width_original;
+ var scaleY = config.layer.height / config.layer.height_original;
+
+ // Get mask bounds
+ var maskCanvas = window.smartSelectMask.canvas;
+ var maskCtx = maskCanvas.getContext('2d');
+ var imageData = maskCtx.getImageData(0, 0, maskCanvas.width, maskCanvas.height);
+
+ var minX = maskCanvas.width, minY = maskCanvas.height;
+ var maxX = 0, maxY = 0;
+
+ for (var y = 0; y < maskCanvas.height; y += 4) { // Sample every 4th pixel for speed
+ for (var x = 0; x < maskCanvas.width; x += 4) {
+ var i = (y * maskCanvas.width + x) * 4;
+ if (imageData.data[i] > 128) {
+ minX = Math.min(minX, x);
+ minY = Math.min(minY, y);
+ maxX = Math.max(maxX, x);
+ maxY = Math.max(maxY, y);
+ }
+ }
+ }
+
+ if (maxX > minX && maxY > minY) {
+ ctx.strokeRect(
+ config.layer.x + minX * scaleX,
+ config.layer.y + minY * scaleY,
+ (maxX - minX) * scaleX,
+ (maxY - minY) * scaleY
+ );
+ }
+
+ ctx.restore();
+ }
+ }
+}
+
+export default Ai_inpaint_class;
diff --git a/frontend/src/js/tools/smart_select.js b/frontend/src/js/tools/smart_select.js
new file mode 100644
index 0000000..747fa18
--- /dev/null
+++ b/frontend/src/js/tools/smart_select.js
@@ -0,0 +1,243 @@
+/**
+ * Smart Select Tool - Uses SAM (Segment Anything Model) for AI-powered selection
+ * Click on any object to automatically select it
+ */
+
+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 Dialog_class from './../libs/popup.js';
+import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js';
+import apiService from './../services/api.js';
+
+class Smart_select_class extends Base_tools_class {
+
+ constructor(ctx) {
+ super();
+ this.Base_layers = new Base_layers_class();
+ this.Helper = new Helper_class();
+ this.POP = new Dialog_class();
+ this.ctx = ctx;
+ this.name = 'smart_select';
+
+ // Store the current mask data
+ this.currentMask = null;
+ this.maskCanvas = null;
+ this.isProcessing = false;
+ }
+
+ load() {
+ var _this = this;
+
+ // Mouse click event for selection
+ document.addEventListener('mousedown', function (e) {
+ _this.mousedown(e);
+ });
+ }
+
+ async mousedown(e) {
+ var mouse = this.get_mouse_info(e);
+
+ if (config.TOOL.name != this.name) return;
+ if (mouse.click_valid == false) return;
+ if (this.isProcessing) {
+ alertify.warning('Processing... please wait');
+ 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);
+ }
+
+ // 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;
+ }
+
+ this.isProcessing = true;
+ alertify.message('AI is analyzing the image...');
+
+ try {
+ // Get image data as base64
+ var imageData = this.getLayerImageData();
+
+ // 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);
+
+ alertify.success('Selection complete! Use AI Inpaint to edit.');
+
+ } catch (error) {
+ console.error('Smart select error:', error);
+ alertify.error('Selection failed: ' + error.message);
+ } finally {
+ this.isProcessing = false;
+ }
+ }
+
+ /**
+ * Get the current layer's image data as base64
+ */
+ getLayerImageData() {
+ var canvas = document.createElement('canvas');
+ var ctx = canvas.getContext('2d');
+
+ canvas.width = config.layer.width_original;
+ canvas.height = config.layer.height_original;
+
+ // Draw the layer's image
+ ctx.drawImage(config.layer.link, 0, 0);
+
+ // Return as base64 (remove data:image/png;base64, prefix)
+ return canvas.toDataURL('image/png').split(',')[1];
+ }
+
+ /**
+ * Apply the SAM mask as a selection
+ * @param {string} maskBase64 - Base64 encoded mask image
+ * @param {Object} bbox - Bounding box {x, y, width, height}
+ */
+ applyMask(maskBase64, bbox) {
+ 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);
+
+ _this.currentMask = {
+ canvas: _this.maskCanvas,
+ bbox: bbox
+ };
+
+ // Store globally for AI inpaint tool to access
+ window.smartSelectMask = _this.currentMask;
+
+ // Visual feedback - render mask overlay
+ _this.renderMaskOverlay();
+
+ config.need_render = true;
+ };
+ maskImage.src = 'data:image/png;base64,' + maskBase64;
+ }
+
+ /**
+ * Render a visual overlay showing the selected region
+ */
+ renderMaskOverlay() {
+ if (!this.maskCanvas) return;
+
+ // Create overlay layer or update existing
+ // For now, we'll use the selection system
+ var maskCtx = this.maskCanvas.getContext('2d');
+ var imageData = maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height);
+
+ // Find bounding box of selection
+ var minX = this.maskCanvas.width, minY = this.maskCanvas.height;
+ var maxX = 0, maxY = 0;
+
+ 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) { // White pixel in mask
+ minX = Math.min(minX, x);
+ minY = Math.min(minY, y);
+ maxX = Math.max(maxX, x);
+ maxY = Math.max(maxY, y);
+ }
+ }
+ }
+
+ if (maxX > minX && maxY > minY) {
+ // Set selection using miniPaint's selection system
+ var Selection = this.Base_layers.Base_gui?.GUI_tools?.tools_modules?.selection?.object;
+ if (Selection) {
+ Selection.selection = {
+ x: config.layer.x + minX * (config.layer.width / config.layer.width_original),
+ y: config.layer.y + minY * (config.layer.height / config.layer.height_original),
+ width: (maxX - minX) * (config.layer.width / config.layer.width_original),
+ height: (maxY - minY) * (config.layer.height / config.layer.height_original)
+ };
+ }
+ }
+ }
+
+ render_overlay(ctx) {
+ // Render marching ants or highlight around selected region
+ if (!this.currentMask || !this.maskCanvas) return;
+
+ var mouse = this.get_mouse_info(event);
+
+ // Draw semi-transparent overlay on non-selected areas
+ ctx.save();
+ ctx.globalAlpha = 0.3;
+ ctx.fillStyle = '#000000';
+
+ // Scale to match layer
+ var scaleX = config.layer.width / config.layer.width_original;
+ var scaleY = config.layer.height / config.layer.height_original;
+
+ ctx.translate(config.layer.x, config.layer.y);
+ ctx.scale(scaleX, scaleY);
+
+ // Draw inverse mask (darken unselected areas)
+ var maskCtx = this.maskCanvas.getContext('2d');
+ var imageData = maskCtx.getImageData(0, 0, this.maskCanvas.width, this.maskCanvas.height);
+
+ // Create inverse mask canvas
+ var inverseCanvas = document.createElement('canvas');
+ inverseCanvas.width = this.maskCanvas.width;
+ inverseCanvas.height = this.maskCanvas.height;
+ var inverseCtx = inverseCanvas.getContext('2d');
+
+ inverseCtx.fillStyle = '#000000';
+ inverseCtx.fillRect(0, 0, inverseCanvas.width, inverseCanvas.height);
+ inverseCtx.globalCompositeOperation = 'destination-out';
+ inverseCtx.drawImage(this.maskCanvas, 0, 0);
+
+ ctx.drawImage(inverseCanvas, 0, 0);
+
+ ctx.restore();
+ }
+
+ /**
+ * Clear the current selection
+ */
+ clearSelection() {
+ this.currentMask = null;
+ this.maskCanvas = null;
+ window.smartSelectMask = null;
+ config.need_render = true;
+ }
+
+ on_leave() {
+ // Don't clear mask when switching tools - AI inpaint needs it
+ return [];
+ }
+}
+
+export default Smart_select_class;