Fix selection tools, add float selection, aspect ratio lock, U2net auto-download
- Fix brush_select and smart_select mask scaling to match layer dimensions - Add "Float Selection" feature to Select tool - when switching to Select with an active AI selection, offers to copy it to a movable layer - Add aspect ratio lock toggle (🔗 button) in layer details panel - When locked, changing width auto-updates height and vice versa - Add U2net model auto-download - will download lightweight u2netp.onnx (~4MB) automatically if no model found - Improve error messages for background removal - Register on_activate for select tool in config Workflow: Select object with AI tool → Click Select tool → "Float" selection → Move/scale the floated layer freely https://claude.ai/code/session_01CLedz6CanT9t46KBvng3vz
This commit is contained in:
@@ -327,10 +327,14 @@ class Brush_select_class extends Base_tools_class {
|
||||
canvas.width = config.layer.width_original;
|
||||
canvas.height = config.layer.height_original;
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img, 0, 0);
|
||||
// Scale the mask image to match the layer dimensions
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
resolve(canvas);
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.onerror = function(e) {
|
||||
console.error('Failed to decode mask image:', e);
|
||||
reject(e);
|
||||
};
|
||||
img.src = 'data:image/png;base64,' + maskBase64;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Base_layers_class from './../core/base-layers.js';
|
||||
import Base_selection_class from './../core/base-selection.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';
|
||||
|
||||
class Select_tool_class extends Base_tools_class {
|
||||
|
||||
@@ -37,6 +38,132 @@ class Select_tool_class extends Base_tools_class {
|
||||
this.Base_selection = new Base_selection_class(ctx, sel_config, this.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the Select tool is activated
|
||||
* If there's an AI selection, offer to float it so it can be moved
|
||||
*/
|
||||
on_activate() {
|
||||
var _this = this;
|
||||
|
||||
// Check if there's an active AI selection (Smart Select, Brush Select, etc.)
|
||||
if (window.smartSelectMask && window.smartSelectMask.canvas) {
|
||||
// Ask user if they want to float the selection
|
||||
alertify.confirm(
|
||||
'Float Selection',
|
||||
'You have an active selection. Would you like to copy it to a new layer so you can move and scale it?',
|
||||
function() {
|
||||
// Yes - float the selection
|
||||
_this.floatSelection();
|
||||
},
|
||||
function() {
|
||||
// No - just clear the selection indicator
|
||||
alertify.message('Tip: Use Ctrl+C in selection tools to copy, or Ctrl+X to cut.');
|
||||
}
|
||||
).set('labels', {ok: 'Yes, Float It', cancel: 'No, Keep Selection'});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Float the current selection to a new layer
|
||||
* This copies the selected pixels to a new layer that can be moved/scaled
|
||||
*/
|
||||
floatSelection() {
|
||||
var maskCanvas = window.smartSelectMask?.canvas;
|
||||
if (!maskCanvas) {
|
||||
alertify.error('No selection to float');
|
||||
return;
|
||||
}
|
||||
|
||||
var layer = config.layer;
|
||||
if (layer.type != 'image') {
|
||||
alertify.error('Please select an image layer first');
|
||||
return;
|
||||
}
|
||||
|
||||
// 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;
|
||||
var hasSelection = false;
|
||||
|
||||
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) {
|
||||
hasSelection = true;
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasSelection || maxX <= minX || maxY <= minY) {
|
||||
alertify.error('Selection is empty or too small');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create masked image
|
||||
var maskedCanvas = document.createElement('canvas');
|
||||
maskedCanvas.width = layer.width_original;
|
||||
maskedCanvas.height = layer.height_original;
|
||||
var maskedCtx = maskedCanvas.getContext('2d');
|
||||
|
||||
maskedCtx.drawImage(layer.link, 0, 0);
|
||||
maskedCtx.globalCompositeOperation = 'destination-in';
|
||||
maskedCtx.drawImage(maskCanvas, 0, 0);
|
||||
|
||||
// Crop to selection bounds
|
||||
var cropWidth = maxX - minX + 1;
|
||||
var cropHeight = maxY - minY + 1;
|
||||
|
||||
var croppedCanvas = document.createElement('canvas');
|
||||
croppedCanvas.width = cropWidth;
|
||||
croppedCanvas.height = cropHeight;
|
||||
var croppedCtx = croppedCanvas.getContext('2d');
|
||||
|
||||
croppedCtx.drawImage(
|
||||
maskedCanvas,
|
||||
minX, minY, cropWidth, cropHeight,
|
||||
0, 0, cropWidth, cropHeight
|
||||
);
|
||||
|
||||
// Calculate position
|
||||
var scaleX = layer.width / layer.width_original;
|
||||
var scaleY = layer.height / layer.height_original;
|
||||
|
||||
var params = {
|
||||
x: Math.round(layer.x + minX * scaleX),
|
||||
y: Math.round(layer.y + minY * scaleY),
|
||||
width: Math.round(cropWidth * scaleX),
|
||||
height: Math.round(cropHeight * scaleY),
|
||||
width_original: cropWidth,
|
||||
height_original: cropHeight,
|
||||
type: 'image',
|
||||
name: layer.name + ' (Floated)',
|
||||
data: croppedCanvas.toDataURL('image/png')
|
||||
};
|
||||
|
||||
app.State.do_action(
|
||||
new app.Actions.Bundle_action('float_selection', 'Float Selection', [
|
||||
new app.Actions.Insert_layer_action(params)
|
||||
])
|
||||
);
|
||||
|
||||
// Clear the selection
|
||||
window.smartSelectMask = null;
|
||||
|
||||
// Enable transparency
|
||||
if (config.TRANSPARENCY == false) {
|
||||
config.TRANSPARENCY = true;
|
||||
this.Base_layers.render();
|
||||
}
|
||||
|
||||
alertify.success('Selection floated to new layer! You can now move and scale it.');
|
||||
}
|
||||
|
||||
load() {
|
||||
var _this = this;
|
||||
|
||||
|
||||
@@ -191,7 +191,8 @@ class Smart_select_class extends Base_tools_class {
|
||||
newMaskCanvas.width = config.layer.width_original;
|
||||
newMaskCanvas.height = config.layer.height_original;
|
||||
var newMaskCtx = newMaskCanvas.getContext('2d');
|
||||
newMaskCtx.drawImage(maskImage, 0, 0);
|
||||
// Scale mask to match layer dimensions
|
||||
newMaskCtx.drawImage(maskImage, 0, 0, newMaskCanvas.width, newMaskCanvas.height);
|
||||
|
||||
// If additive and we have an existing mask, combine them
|
||||
if (isAdditive && _this.maskCanvas) {
|
||||
|
||||
Reference in New Issue
Block a user