Fix AI tools and My Library, add U2net background removal

- Fix My Library: Add CSS styling for library browser, items now visible
- Integrate My Library into Shapes tool with tabbed interface
- Improve AI Inpaint: Add transform mode for scaling/sizing selections
- Add helpful guidance explaining inpaint vs transform modes
- Add U2net as alternative background removal (avoids rembg issues)
- Create U2net model definition and download script
- Improve Caddyfile with multiple options and troubleshooting guide

Note: Brush Select (AI Paint) tool was already implemented and working.

https://claude.ai/code/session_01CLedz6CanT9t46KBvng3vz
This commit is contained in:
Claude
2026-01-27 22:29:35 +00:00
parent 17806a0403
commit 549a1a4e82
8 changed files with 1327 additions and 48 deletions
+154 -5
View File
@@ -49,14 +49,20 @@ class Ai_inpaint_class extends Base_tools_class {
}
var settings = {
title: 'AI Inpaint',
title: 'AI Edit Selection',
params: [
{
name: "mode",
title: "Edit Mode:",
value: "inpaint",
values: ["inpaint", "transform"]
},
{
name: "prompt",
title: "Describe what you want:",
title: "AI Inpaint - Describe replacement:",
type: "textarea",
value: "",
placeholder: "e.g., 'a red rose', 'remove the object', 'blue sky with clouds'"
placeholder: "AI will REPLACE the selection with what you describe.\nExamples: 'a red rose', 'empty background', 'blue sky'"
},
{
name: "negative_prompt",
@@ -66,21 +72,164 @@ class Ai_inpaint_class extends Base_tools_class {
},
{
name: "strength",
title: "Edit Strength:",
title: "AI Edit Strength:",
type: "range",
value: 80,
range: [1, 100],
step: 1
},
{
name: "scale",
title: "Transform - Scale %:",
type: "range",
value: 100,
range: [10, 200],
step: 5
}
],
on_load: function(el) {
// Add info text
var infoDiv = document.createElement('div');
infoDiv.className = 'ai-inpaint-info';
infoDiv.innerHTML = '<p style="font-size:12px;color:#aaa;margin-bottom:10px;">' +
'<strong>Inpaint Mode:</strong> AI replaces the selected area with generated content.<br>' +
'<strong>Transform Mode:</strong> Scale, shrink, or enlarge the selection without AI.<br>' +
'<em>Tip: To shrink something by 35%, use Transform mode with Scale at 65%.</em></p>';
var dialogContent = el.querySelector('.dialog_content');
if (dialogContent && dialogContent.firstChild) {
dialogContent.insertBefore(infoDiv, dialogContent.firstChild);
}
},
on_finish: async function (params) {
await _this.executeInpaint(params);
if (params.mode === 'transform') {
await _this.executeTransform(params);
} else {
await _this.executeInpaint(params);
}
},
};
this.POP.show(settings);
}
/**
* Execute transform operation (scale without AI)
*/
async executeTransform(params) {
if (this.isProcessing) {
alertify.warning('Already processing... please wait');
return;
}
// Check if we have an image layer
if (config.layer.type != 'image') {
alertify.error('Please select an image layer');
return;
}
var maskCanvas = window.smartSelectMask?.canvas;
if (!maskCanvas) {
alertify.error('No selection mask found');
return;
}
this.isProcessing = true;
alertify.message('Transforming selection...');
try {
var layer = config.layer;
var scale = params.scale / 100;
// Get mask bounds
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++) {
for (var x = 0; x < maskCanvas.width; x++) {
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) {
throw new Error('Selection is too small');
}
var selWidth = maxX - minX + 1;
var selHeight = maxY - minY + 1;
var centerX = minX + selWidth / 2;
var centerY = minY + selHeight / 2;
// Extract selected pixels
var extractCanvas = document.createElement('canvas');
extractCanvas.width = layer.width_original;
extractCanvas.height = layer.height_original;
var extractCtx = extractCanvas.getContext('2d');
extractCtx.drawImage(layer.link, 0, 0);
extractCtx.globalCompositeOperation = 'destination-in';
extractCtx.drawImage(maskCanvas, 0, 0);
// Create result canvas
var resultCanvas = document.createElement('canvas');
resultCanvas.width = layer.width_original;
resultCanvas.height = layer.height_original;
var resultCtx = resultCanvas.getContext('2d');
// Draw original image
resultCtx.drawImage(layer.link, 0, 0);
// Remove original selection (create hole)
resultCtx.globalCompositeOperation = 'destination-out';
resultCtx.drawImage(maskCanvas, 0, 0);
// Calculate scaled dimensions
var newWidth = selWidth * scale;
var newHeight = selHeight * scale;
var newX = centerX - newWidth / 2;
var newY = centerY - newHeight / 2;
// Draw scaled selection back
resultCtx.globalCompositeOperation = 'source-over';
// Create temp canvas for just the selection
var selCanvas = document.createElement('canvas');
selCanvas.width = selWidth;
selCanvas.height = selHeight;
var selCtx = selCanvas.getContext('2d');
selCtx.drawImage(extractCanvas, minX, minY, selWidth, selHeight, 0, 0, selWidth, selHeight);
// Draw scaled
resultCtx.drawImage(selCanvas, 0, 0, selWidth, selHeight, newX, newY, newWidth, newHeight);
// Apply result
app.State.do_action(
new app.Actions.Bundle_action('transform_selection', 'Transform Selection', [
new app.Actions.Update_layer_image_action(resultCanvas)
])
);
// Clear selection
window.smartSelectMask = null;
config.need_render = true;
alertify.success('Transform complete! Selection scaled to ' + params.scale + '%');
} catch (error) {
console.error('Transform error:', error);
alertify.error('Transform failed: ' + error.message);
} finally {
this.isProcessing = false;
}
}
/**
* Execute the inpainting operation
*/
+148 -13
View File
@@ -4,6 +4,7 @@ import Base_tools_class from './../core/base-tools.js';
import Base_layers_class from './../core/base-layers.js';
import Dialog_class from './../libs/popup.js';
import GUI_tools_class from './../core/gui/gui-tools.js';
import File_my_library_class from './../modules/file/my_library.js';
var instance = null;
@@ -21,11 +22,13 @@ class Shape_class extends Base_tools_class {
this.Base_layers = new Base_layers_class();
this.GUI_tools = new GUI_tools_class();
this.POP = new Dialog_class();
this.My_library = new File_my_library_class();
this.ctx = ctx;
this.name = 'shape';
this.layer = {};
this.preview_width = 150;
this.preview_height = 120;
this.activeTab = 'shapes'; // 'shapes' or 'library'
this.set_events();
}
@@ -53,35 +56,84 @@ class Shape_class extends Base_tools_class {
async show_shapes(){
var _this = this;
var html = '';
// Build tabs HTML
var tabsHtml = '<div class="shape-tabs">';
tabsHtml += '<button class="shape-tab active" data-tab="shapes">Built-in Shapes</button>';
tabsHtml += '<button class="shape-tab" data-tab="library">My Library</button>';
tabsHtml += '</div>';
// Build shapes HTML
var shapesHtml = '<div class="tab-content shapes-content">';
var data = this.get_shapes();
for (var i in data) {
html += '<div class="item">';
html += ' <canvas id="c_' + data[i].key + '" width="' + this.preview_width + '" height="'
shapesHtml += '<div class="item">';
shapesHtml += ' <canvas id="c_' + data[i].key + '" width="' + this.preview_width + '" height="'
+ this.preview_height + '" class="effectsPreview" data-key="'
+ data[i].key + '"></canvas>';
html += '<div class="preview-item-title">' + data[i].title + '</div>';
html += '</div>';
shapesHtml += '<div class="preview-item-title">' + data[i].title + '</div>';
shapesHtml += '</div>';
}
for (var i = 0; i < 4; i++) {
html += '<div class="item"></div>';
shapesHtml += '<div class="item"></div>';
}
shapesHtml += '</div>';
// Build library HTML placeholder
var libraryHtml = '<div class="tab-content library-content" style="display:none;">';
libraryHtml += '<div class="library-loading">Loading library...</div>';
libraryHtml += '</div>';
var settings = {
title: 'Shapes',
title: 'Shapes & Library',
className: 'wide',
on_load: function (params, popup) {
var node = document.createElement("div");
node.classList.add('flex-container');
node.innerHTML = html;
popup.el.querySelector('.dialog_content').appendChild(node);
//events
// Add tabs
var tabsNode = document.createElement("div");
tabsNode.innerHTML = tabsHtml;
popup.el.querySelector('.dialog_content').insertBefore(tabsNode, popup.el.querySelector('.dialog_content').firstChild);
// Add shapes container
var shapesNode = document.createElement("div");
shapesNode.classList.add('flex-container');
shapesNode.innerHTML = shapesHtml;
popup.el.querySelector('.dialog_content').appendChild(shapesNode);
// Add library container
var libraryNode = document.createElement("div");
libraryNode.innerHTML = libraryHtml;
popup.el.querySelector('.dialog_content').appendChild(libraryNode);
// Tab click events
var tabs = popup.el.querySelectorAll('.shape-tab');
tabs.forEach(function(tab) {
tab.addEventListener('click', function() {
var targetTab = this.dataset.tab;
// Update active tab
tabs.forEach(t => t.classList.remove('active'));
this.classList.add('active');
// Show/hide content
var shapesContent = popup.el.querySelector('.shapes-content');
var libraryContent = popup.el.querySelector('.library-content');
if (targetTab === 'shapes') {
shapesContent.style.display = '';
libraryContent.style.display = 'none';
} else {
shapesContent.style.display = 'none';
libraryContent.style.display = '';
_this.loadLibraryContent(libraryContent);
}
});
});
// Shape click events
var targets = popup.el.querySelectorAll('.item canvas');
for (var i = 0; i < targets.length; i++) {
targets[i].addEventListener('click', function (event) {
//we have click
_this.GUI_tools.activate_tool(this.dataset.key);
_this.POP.hide();
});
@@ -106,6 +158,89 @@ class Shape_class extends Base_tools_class {
}
}
/**
* Load library content into the library tab
*/
loadLibraryContent(container) {
var _this = this;
this.My_library.getAllAssets(function(assets) {
var html = '';
if (assets.length === 0) {
html = '<div class="library-empty">';
html += '<p>Your library is empty.</p>';
html += '<p>Use <strong>File > My Library > Save to Library</strong> to add assets.</p>';
html += '</div>';
} else {
// Group by category
var categories = {};
assets.forEach(function(asset) {
var cat = asset.category || 'General';
if (!categories[cat]) categories[cat] = [];
categories[cat].push(asset);
});
html = '<div class="library-browser">';
html += '<div class="library-categories">';
for (var cat in categories) {
html += '<div class="library-category">';
html += '<h3>' + cat + ' (' + categories[cat].length + ')</h3>';
html += '<div class="library-items">';
categories[cat].forEach(function(asset) {
html += '<div class="library-item" data-id="' + asset.id + '">';
html += '<img src="' + asset.thumbnail + '" alt="' + asset.name + '" title="' + asset.name + '">';
html += '<div class="library-item-name">' + asset.name + '</div>';
html += '<div class="library-item-actions">';
html += '<button class="insert-btn" data-id="' + asset.id + '">Insert</button>';
html += '<button class="delete-btn" data-id="' + asset.id + '">Delete</button>';
html += '</div>';
html += '</div>';
});
html += '</div></div>';
}
html += '</div></div>';
}
container.innerHTML = html;
// Add event handlers for library items
container.querySelectorAll('.insert-btn').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
var id = parseInt(this.dataset.id);
_this.My_library.insertAsset(id);
_this.POP.hide();
});
});
container.querySelectorAll('.delete-btn').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
var id = parseInt(this.dataset.id);
if (confirm('Delete this asset?')) {
_this.My_library.deleteAsset(id, function() {
_this.loadLibraryContent(container);
});
}
});
});
// Double-click to insert
container.querySelectorAll('.library-item').forEach(function(item) {
item.addEventListener('dblclick', function() {
var id = parseInt(this.dataset.id);
_this.My_library.insertAsset(id);
_this.POP.hide();
});
});
});
}
render(ctx, layer) {
}