Fix AI Edit tool events, add icon, move color swatch to toolbar bottom
ai_edit.js: - Extend Base_tools_class and add load() + default_events() so mouse events actually wire up (was the root cause of tool not working) - Use get_mouse_info() for coordinate mapping instead of manual clientX/Y math — consistent with all other miniPaint tools - Fix _mouseToImage() to use mouse.x/y (already in image coords) and correct display-scale for overlay brush rendering - mousedown/mousemove/mouseup now guard on config.TOOL.name ai_edit.svg: new icon (brush + sparkle star) for the left toolbar layout.css: add .ai_edit:after CSS rule for the icon main.js: - Collapse right-panel Colors section by default (respects saved cookie so user preference persists) - Mount compact foreground/background color swatches at the bottom of the left toolbar; click either square to toggle the full color picker open/closed; syncs live with config.COLOR every 250ms https://claude.ai/code/session_01B58MaJCU1R6KwBDJCp8AfN
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<!-- Brush handle -->
|
||||||
|
<path d="M16.5 3.5a2.12 2.12 0 013 3L7 19l-4 1 1-4L16.5 3.5z"/>
|
||||||
|
<!-- AI sparkle star -->
|
||||||
|
<path d="M20 13l.94 2.06L23 16l-2.06.94L20 19l-.94-2.06L17 16l2.06-.94z" fill="currentColor" stroke="none"/>
|
||||||
|
<!-- Small dot -->
|
||||||
|
<circle cx="20" cy="16" r="0" fill="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 471 B |
@@ -324,6 +324,7 @@ IMPORTANT: any new icon should also must be added on /service-worker.js + its ve
|
|||||||
.sidebar_left .smart_select:after{ background-image: url('images/icons/smart_select.svg'); }
|
.sidebar_left .smart_select:after{ background-image: url('images/icons/smart_select.svg'); }
|
||||||
.sidebar_left .brush_select:after{ background-image: url('images/icons/brush_select.svg'); }
|
.sidebar_left .brush_select:after{ background-image: url('images/icons/brush_select.svg'); }
|
||||||
.sidebar_left .ai_inpaint:after{ background-image: url('images/icons/ai_inpaint.svg'); }
|
.sidebar_left .ai_inpaint:after{ background-image: url('images/icons/ai_inpaint.svg'); }
|
||||||
|
.sidebar_left .ai_edit:after{ background-image: url('images/icons/ai_edit.svg'); }
|
||||||
.sidebar_left .magic_wand:after{ background-image: url('images/icons/magic_wand.svg'); }
|
.sidebar_left .magic_wand:after{ background-image: url('images/icons/magic_wand.svg'); }
|
||||||
.sidebar_left .lasso:after{ background-image: url('images/icons/lasso.svg'); }
|
.sidebar_left .lasso:after{ background-image: url('images/icons/lasso.svg'); }
|
||||||
.sidebar_left .ellipse_select:after{ background-image: url('images/icons/ellipse_select.svg'); }
|
.sidebar_left .ellipse_select:after{ background-image: url('images/icons/ellipse_select.svg'); }
|
||||||
|
|||||||
@@ -58,4 +58,70 @@ window.addEventListener('load', function (e) {
|
|||||||
|
|
||||||
// Mount provider badge in the tools panel footer
|
// Mount provider badge in the tools panel footer
|
||||||
mountProviderBadge(document.getElementById('tools_container') || document.body);
|
mountProviderBadge(document.getElementById('tools_container') || document.body);
|
||||||
|
|
||||||
|
// Collapse right-panel Colors section by default (compact color swatch on left toolbar instead)
|
||||||
|
_collapseColorsPanel();
|
||||||
|
// Mount compact foreground/background color swatches at bottom of left toolbar
|
||||||
|
_mountToolbarColorSwatch();
|
||||||
}, false);
|
}, false);
|
||||||
|
|
||||||
|
function _collapseColorsPanel() {
|
||||||
|
var toggle = document.querySelector('[data-target="toggle_colors"]');
|
||||||
|
var panel = document.getElementById('toggle_colors');
|
||||||
|
if (toggle && panel) {
|
||||||
|
// Only collapse if user hasn't explicitly expanded it (no saved cookie)
|
||||||
|
var Helper = { getCookie: (k) => { var m = document.cookie.match('(^|;)\\s*' + k + '\\s*=\\s*([^;]+)'); return m ? m.pop() : null; } };
|
||||||
|
if (Helper.getCookie('toggle_colors') !== '1') {
|
||||||
|
panel.classList.add('hidden');
|
||||||
|
toggle.classList.add('toggled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _mountToolbarColorSwatch() {
|
||||||
|
var toolbar = document.getElementById('tools_container');
|
||||||
|
if (!toolbar) return;
|
||||||
|
|
||||||
|
// Spacer to push swatch to bottom
|
||||||
|
var spacer = document.createElement('div');
|
||||||
|
spacer.style.cssText = 'flex:1;min-height:8px;width:100%;';
|
||||||
|
toolbar.appendChild(spacer);
|
||||||
|
|
||||||
|
// Foreground / background color squares (click to open full color picker)
|
||||||
|
var wrap = document.createElement('div');
|
||||||
|
wrap.id = 'toolbar_color_swatch';
|
||||||
|
wrap.title = 'Foreground / Background color — click to open color picker';
|
||||||
|
wrap.style.cssText = 'position:relative;width:30px;height:30px;margin:4px 0 4px 5px;cursor:pointer;flex-shrink:0;';
|
||||||
|
wrap.innerHTML = `
|
||||||
|
<div id="tc_bg" style="position:absolute;right:0;bottom:0;width:20px;height:20px;
|
||||||
|
border:1px solid #555;background:#000;border-radius:3px;"></div>
|
||||||
|
<div id="tc_fg" style="position:absolute;left:0;top:0;width:20px;height:20px;
|
||||||
|
border:1px solid #777;background:#008000;border-radius:3px;"></div>`;
|
||||||
|
toolbar.appendChild(wrap);
|
||||||
|
|
||||||
|
// Keep swatch in sync with config.COLOR
|
||||||
|
function _syncSwatch() {
|
||||||
|
var fg = document.getElementById('tc_fg');
|
||||||
|
var bg = document.getElementById('tc_bg');
|
||||||
|
if (fg) fg.style.background = window.config && config.COLOR ? config.COLOR : '#008000';
|
||||||
|
}
|
||||||
|
setInterval(_syncSwatch, 250);
|
||||||
|
|
||||||
|
// Click → open the right-side color panel
|
||||||
|
wrap.addEventListener('click', function () {
|
||||||
|
var panel = document.getElementById('toggle_colors');
|
||||||
|
var toggle = document.querySelector('[data-target="toggle_colors"]');
|
||||||
|
if (!panel) return;
|
||||||
|
var hidden = panel.classList.contains('hidden');
|
||||||
|
if (hidden) {
|
||||||
|
panel.classList.remove('hidden');
|
||||||
|
if (toggle) toggle.classList.remove('toggled');
|
||||||
|
// Scroll right panel to top so color picker is visible
|
||||||
|
var sidebar = document.querySelector('.sidebar_right');
|
||||||
|
if (sidebar) sidebar.scrollTop = 0;
|
||||||
|
} else {
|
||||||
|
panel.classList.add('hidden');
|
||||||
|
if (toggle) toggle.classList.add('toggled');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,17 +16,18 @@
|
|||||||
import app from './../app.js';
|
import app from './../app.js';
|
||||||
import config from './../config.js';
|
import config from './../config.js';
|
||||||
import Base_layers_class from './../core/base-layers.js';
|
import Base_layers_class from './../core/base-layers.js';
|
||||||
|
import Base_tools_class from './../core/base-tools.js';
|
||||||
import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js';
|
import alertify from './../../../node_modules/alertifyjs/build/alertify.min.js';
|
||||||
|
|
||||||
var instance = null;
|
var instance = null;
|
||||||
|
|
||||||
const BRUSH_DEFAULT = 30;
|
const BRUSH_DEFAULT = 30;
|
||||||
const OVERLAY_COLOR = 'rgba(255, 55, 55, 0.50)';
|
const OVERLAY_COLOR = 'rgba(255, 55, 55, 0.50)';
|
||||||
const ERASE_COLOR = 'rgba(0, 0, 0, 0.70)'; // brush-erase preview
|
|
||||||
|
|
||||||
class Tools_ai_edit_class {
|
class Tools_ai_edit_class extends Base_tools_class {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
super();
|
||||||
if (instance) return instance;
|
if (instance) return instance;
|
||||||
instance = this;
|
instance = this;
|
||||||
this.Base_layers = new Base_layers_class();
|
this.Base_layers = new Base_layers_class();
|
||||||
@@ -47,6 +48,10 @@ class Tools_ai_edit_class {
|
|||||||
|
|
||||||
// ── Tool lifecycle ────────────────────────────────────────────────────────
|
// ── Tool lifecycle ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
load() {
|
||||||
|
this.default_events();
|
||||||
|
}
|
||||||
|
|
||||||
on_activate() {
|
on_activate() {
|
||||||
if (!config.layer || config.layer.type !== 'image') {
|
if (!config.layer || config.layer.type !== 'image') {
|
||||||
alertify.error('Select an image layer first.');
|
alertify.error('Select an image layer first.');
|
||||||
@@ -66,45 +71,50 @@ class Tools_ai_edit_class {
|
|||||||
// ── Input routing ─────────────────────────────────────────────────────────
|
// ── Input routing ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
mousedown(e) {
|
mousedown(e) {
|
||||||
|
if (config.TOOL.name !== this.name) return;
|
||||||
|
var mouse = this.get_mouse_info(e);
|
||||||
|
if (!mouse.click_valid) return;
|
||||||
if (!config.layer || config.layer.type !== 'image') return;
|
if (!config.layer || config.layer.type !== 'image') return;
|
||||||
if (this._mode === 'sam') {
|
if (this._mode === 'sam') {
|
||||||
this._handleSamClick(e);
|
this._handleSamClick(e, mouse);
|
||||||
} else {
|
} else {
|
||||||
this._painting = true;
|
this._painting = true;
|
||||||
this._brushPaint(e);
|
this._brushPaint(mouse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mousemove(e) {
|
mousemove(e) {
|
||||||
if (this._mode !== 'sam' && this._painting) this._brushPaint(e);
|
if (config.TOOL.name !== this.name) return;
|
||||||
|
if (this._mode !== 'sam' && this._painting) {
|
||||||
|
var mouse = this.get_mouse_info(e);
|
||||||
|
if (mouse.is_drag) this._brushPaint(mouse);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mouseup() {
|
mouseup(e) {
|
||||||
|
if (config.TOOL.name !== this.name) return;
|
||||||
if (this._painting) {
|
if (this._painting) {
|
||||||
this._painting = false;
|
this._painting = false;
|
||||||
if (this._hasMask) this._showActions();
|
if (this._hasMask) this._showActions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Coordinate mapping ────────────────────────────────────────────────────
|
// ── Coordinate mapping — uses miniPaint's get_mouse_info ─────────────────
|
||||||
|
// get_mouse_info returns { x, y } already in canvas/layer coordinates.
|
||||||
|
// We still need to know the display scale to size brush strokes on the overlay.
|
||||||
|
|
||||||
_screenToImage(e) {
|
_mouseToImage(mouse) {
|
||||||
const canvasEl = document.getElementById('canvas_minipaint') || document.querySelector('canvas');
|
// mouse.x/y are already in original image coords from get_mouse_info
|
||||||
if (!canvasEl) return null;
|
const scaleX = (config.WIDTH * config.ZOOM) / config.layer.width_original;
|
||||||
const rect = canvasEl.getBoundingClientRect();
|
const scaleY = (config.HEIGHT * config.ZOOM) / config.layer.height_original;
|
||||||
const scaleX = config.layer.width_original / (config.WIDTH * config.ZOOM);
|
return { ix: mouse.x, iy: mouse.y, scaleX, scaleY };
|
||||||
const scaleY = config.layer.height_original / (config.HEIGHT * config.ZOOM);
|
|
||||||
const ix = ((e.clientX - rect.left) - config.layer.x * config.ZOOM) * scaleX;
|
|
||||||
const iy = ((e.clientY - rect.top) - config.layer.y * config.ZOOM) * scaleY;
|
|
||||||
return { ix, iy, scaleX, scaleY };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── SAM click selection ───────────────────────────────────────────────────
|
// ── SAM click selection ───────────────────────────────────────────────────
|
||||||
|
|
||||||
async _handleSamClick(e) {
|
async _handleSamClick(e, mouse) {
|
||||||
if (this._samWorking) return;
|
if (this._samWorking) return;
|
||||||
const coords = this._screenToImage(e);
|
const coords = this._mouseToImage(mouse);
|
||||||
if (!coords) return;
|
|
||||||
|
|
||||||
const label = e.altKey ? 0 : 1; // alt = exclude, normal = include
|
const label = e.altKey ? 0 : 1; // alt = exclude, normal = include
|
||||||
const x = Math.round(coords.ix);
|
const x = Math.round(coords.ix);
|
||||||
@@ -240,13 +250,11 @@ class Tools_ai_edit_class {
|
|||||||
|
|
||||||
// ── Brush painting ────────────────────────────────────────────────────────
|
// ── Brush painting ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
_brushPaint(e) {
|
_brushPaint(mouse) {
|
||||||
const coords = this._screenToImage(e);
|
const { ix, iy, scaleX, scaleY } = this._mouseToImage(mouse);
|
||||||
if (!coords) return;
|
|
||||||
const { ix, iy, scaleX, scaleY } = coords;
|
|
||||||
const r = (config.tools[this.name]?.size ?? BRUSH_DEFAULT) / 2;
|
const r = (config.tools[this.name]?.size ?? BRUSH_DEFAULT) / 2;
|
||||||
|
|
||||||
// Paint on mask canvas
|
// Paint on mask canvas (image coords)
|
||||||
this._maskCtx.globalCompositeOperation =
|
this._maskCtx.globalCompositeOperation =
|
||||||
this._mode === 'brush_sub' ? 'destination-out' : 'source-over';
|
this._mode === 'brush_sub' ? 'destination-out' : 'source-over';
|
||||||
this._maskCtx.fillStyle = '#ffffff';
|
this._maskCtx.fillStyle = '#ffffff';
|
||||||
@@ -255,13 +263,13 @@ class Tools_ai_edit_class {
|
|||||||
this._maskCtx.fill();
|
this._maskCtx.fill();
|
||||||
this._maskCtx.globalCompositeOperation = 'source-over';
|
this._maskCtx.globalCompositeOperation = 'source-over';
|
||||||
|
|
||||||
// Mirror on overlay
|
// Mirror on overlay (display coords)
|
||||||
const oc = this._overlayEl;
|
const oc = this._overlayEl;
|
||||||
if (!oc) return;
|
if (!oc) return;
|
||||||
const oct = oc.getContext('2d');
|
const oct = oc.getContext('2d');
|
||||||
const ox = (ix / scaleX) + config.layer.x * config.ZOOM;
|
const ox = ix * scaleX + config.layer.x * config.ZOOM;
|
||||||
const oy = (iy / scaleY) + config.layer.y * config.ZOOM;
|
const oy = iy * scaleY + config.layer.y * config.ZOOM;
|
||||||
const or_ = r / scaleX;
|
const or_ = r * scaleX;
|
||||||
|
|
||||||
if (this._mode === 'brush_sub') {
|
if (this._mode === 'brush_sub') {
|
||||||
oct.globalCompositeOperation = 'destination-out';
|
oct.globalCompositeOperation = 'destination-out';
|
||||||
|
|||||||
Reference in New Issue
Block a user