mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-22 05:02:11 -03:00
592 lines
28 KiB
JavaScript
592 lines
28 KiB
JavaScript
import { createCanvas } from "./utils/CommonUtils.js";
|
|
import { createModuleLogger } from "./utils/LoggerUtils.js";
|
|
import { showErrorNotification } from "./utils/NotificationUtils.js";
|
|
import { webSocketManager } from "./utils/WebSocketManager.js";
|
|
const log = createModuleLogger('CanvasIO');
|
|
export class CanvasIO {
|
|
constructor(canvas) {
|
|
this.canvas = canvas;
|
|
this._saveInProgress = null;
|
|
}
|
|
async saveToServer(fileName, outputMode = 'disk') {
|
|
if (outputMode === 'disk') {
|
|
if (!window.canvasSaveStates) {
|
|
window.canvasSaveStates = new Map();
|
|
}
|
|
const nodeId = this.canvas.node.id;
|
|
const saveKey = `${nodeId}_${fileName}`;
|
|
if (this._saveInProgress || window.canvasSaveStates.get(saveKey)) {
|
|
log.warn(`Save already in progress for node ${nodeId}, waiting...`);
|
|
return this._saveInProgress || window.canvasSaveStates.get(saveKey);
|
|
}
|
|
log.info(`Starting saveToServer (disk) with fileName: ${fileName} for node: ${nodeId}`);
|
|
this._saveInProgress = this._performSave(fileName, outputMode);
|
|
window.canvasSaveStates.set(saveKey, this._saveInProgress);
|
|
try {
|
|
return await this._saveInProgress;
|
|
}
|
|
finally {
|
|
this._saveInProgress = null;
|
|
window.canvasSaveStates.delete(saveKey);
|
|
log.debug(`Save completed for node ${nodeId}, lock released`);
|
|
}
|
|
}
|
|
else {
|
|
log.info(`Starting saveToServer (RAM) for node: ${this.canvas.node.id}`);
|
|
return this._performSave(fileName, outputMode);
|
|
}
|
|
}
|
|
async _performSave(fileName, outputMode) {
|
|
if (this.canvas.layers.length === 0) {
|
|
log.warn(`Node ${this.canvas.node.id} has no layers, creating empty canvas`);
|
|
return Promise.resolve(true);
|
|
}
|
|
await this.canvas.canvasState.saveStateToDB();
|
|
const nodeId = this.canvas.node.id;
|
|
const delay = (nodeId % 10) * 50;
|
|
if (delay > 0) {
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
}
|
|
return new Promise((resolve) => {
|
|
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(this.canvas.width, this.canvas.height);
|
|
const { canvas: maskCanvas, ctx: maskCtx } = createCanvas(this.canvas.width, this.canvas.height);
|
|
const originalShape = this.canvas.outputAreaShape;
|
|
this.canvas.outputAreaShape = null;
|
|
const { canvas: visibilityCanvas, ctx: visibilityCtx } = createCanvas(this.canvas.width, this.canvas.height, '2d', { alpha: true });
|
|
if (!visibilityCtx)
|
|
throw new Error("Could not create visibility context");
|
|
if (!maskCtx)
|
|
throw new Error("Could not create mask context");
|
|
if (!tempCtx)
|
|
throw new Error("Could not create temp context");
|
|
maskCtx.fillStyle = '#ffffff';
|
|
maskCtx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
|
log.debug(`Canvas contexts created, starting layer rendering`);
|
|
this.canvas.canvasLayers.drawLayersToContext(tempCtx, this.canvas.layers);
|
|
this.canvas.canvasLayers.drawLayersToContext(visibilityCtx, this.canvas.layers);
|
|
log.debug(`Finished rendering layers`);
|
|
const visibilityData = visibilityCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
|
const maskData = maskCtx.getImageData(0, 0, this.canvas.width, this.canvas.height);
|
|
for (let i = 0; i < visibilityData.data.length; i += 4) {
|
|
const alpha = visibilityData.data[i + 3];
|
|
const maskValue = 255 - alpha;
|
|
maskData.data[i] = maskData.data[i + 1] = maskData.data[i + 2] = maskValue;
|
|
maskData.data[i + 3] = 255;
|
|
}
|
|
maskCtx.putImageData(maskData, 0, 0);
|
|
this.canvas.outputAreaShape = originalShape;
|
|
// Use optimized getMaskForOutputArea() instead of getMask() for better performance
|
|
// This only processes chunks that overlap with the output area
|
|
const toolMaskCanvas = this.canvas.maskTool.getMaskForOutputArea();
|
|
if (toolMaskCanvas) {
|
|
log.debug(`Using optimized output area mask (${toolMaskCanvas.width}x${toolMaskCanvas.height}) instead of full mask`);
|
|
// The optimized mask is already sized and positioned for the output area
|
|
// So we can draw it directly without complex positioning calculations
|
|
const tempMaskData = toolMaskCanvas.getContext('2d', { willReadFrequently: true })?.getImageData(0, 0, toolMaskCanvas.width, toolMaskCanvas.height);
|
|
if (tempMaskData) {
|
|
// Ensure the mask data is in the correct format (white with alpha)
|
|
for (let i = 0; i < tempMaskData.data.length; i += 4) {
|
|
const alpha = tempMaskData.data[i + 3];
|
|
tempMaskData.data[i] = tempMaskData.data[i + 1] = tempMaskData.data[i + 2] = 255;
|
|
tempMaskData.data[i + 3] = alpha;
|
|
}
|
|
// Create a temporary canvas to hold the processed mask
|
|
const { canvas: tempMaskCanvas, ctx: tempMaskCtx } = createCanvas(this.canvas.width, this.canvas.height, '2d', { willReadFrequently: true });
|
|
if (!tempMaskCtx)
|
|
throw new Error("Could not create temp mask context");
|
|
// Put the processed mask data into a canvas that matches the output area size
|
|
const { canvas: outputMaskCanvas, ctx: outputMaskCtx } = createCanvas(toolMaskCanvas.width, toolMaskCanvas.height, '2d', { willReadFrequently: true });
|
|
if (!outputMaskCtx)
|
|
throw new Error("Could not create output mask context");
|
|
outputMaskCtx.putImageData(tempMaskData, 0, 0);
|
|
// Draw the optimized mask at the correct position (output area bounds)
|
|
const bounds = this.canvas.outputAreaBounds;
|
|
tempMaskCtx.drawImage(outputMaskCanvas, bounds.x, bounds.y);
|
|
maskCtx.globalCompositeOperation = 'source-over';
|
|
maskCtx.drawImage(tempMaskCanvas, 0, 0);
|
|
}
|
|
}
|
|
if (outputMode === 'ram') {
|
|
const imageData = tempCanvas.toDataURL('image/png');
|
|
const maskData = maskCanvas.toDataURL('image/png');
|
|
log.info("Returning image and mask data as base64 for RAM mode.");
|
|
resolve({ image: imageData, mask: maskData });
|
|
return;
|
|
}
|
|
const fileNameWithoutMask = fileName.replace('.png', '_without_mask.png');
|
|
log.info(`Saving image without mask as: ${fileNameWithoutMask}`);
|
|
tempCanvas.toBlob(async (blobWithoutMask) => {
|
|
if (!blobWithoutMask)
|
|
return;
|
|
log.debug(`Created blob for image without mask, size: ${blobWithoutMask.size} bytes`);
|
|
const formDataWithoutMask = new FormData();
|
|
formDataWithoutMask.append("image", blobWithoutMask, fileNameWithoutMask);
|
|
formDataWithoutMask.append("overwrite", "true");
|
|
try {
|
|
const response = await fetch("/upload/image", {
|
|
method: "POST",
|
|
body: formDataWithoutMask,
|
|
});
|
|
log.debug(`Image without mask upload response: ${response.status}`);
|
|
}
|
|
catch (error) {
|
|
log.error(`Error uploading image without mask:`, error);
|
|
}
|
|
}, "image/png");
|
|
log.info(`Saving main image as: ${fileName}`);
|
|
tempCanvas.toBlob(async (blob) => {
|
|
if (!blob)
|
|
return;
|
|
log.debug(`Created blob for main image, size: ${blob.size} bytes`);
|
|
const formData = new FormData();
|
|
formData.append("image", blob, fileName);
|
|
formData.append("overwrite", "true");
|
|
try {
|
|
const resp = await fetch("/upload/image", {
|
|
method: "POST",
|
|
body: formData,
|
|
});
|
|
log.debug(`Main image upload response: ${resp.status}`);
|
|
if (resp.status === 200) {
|
|
const maskFileName = fileName.replace('.png', '_mask.png');
|
|
log.info(`Saving mask as: ${maskFileName}`);
|
|
maskCanvas.toBlob(async (maskBlob) => {
|
|
if (!maskBlob)
|
|
return;
|
|
log.debug(`Created blob for mask, size: ${maskBlob.size} bytes`);
|
|
const maskFormData = new FormData();
|
|
maskFormData.append("image", maskBlob, maskFileName);
|
|
maskFormData.append("overwrite", "true");
|
|
try {
|
|
const maskResp = await fetch("/upload/image", {
|
|
method: "POST",
|
|
body: maskFormData,
|
|
});
|
|
log.debug(`Mask upload response: ${maskResp.status}`);
|
|
if (maskResp.status === 200) {
|
|
const data = await resp.json();
|
|
if (this.canvas.widget) {
|
|
this.canvas.widget.value = fileName;
|
|
}
|
|
log.info(`All files saved successfully, widget value set to: ${fileName}`);
|
|
resolve(true);
|
|
}
|
|
else {
|
|
log.error(`Error saving mask: ${maskResp.status}`);
|
|
resolve(false);
|
|
}
|
|
}
|
|
catch (error) {
|
|
log.error(`Error saving mask:`, error);
|
|
resolve(false);
|
|
}
|
|
}, "image/png");
|
|
}
|
|
else {
|
|
log.error(`Main image upload failed: ${resp.status} - ${resp.statusText}`);
|
|
resolve(false);
|
|
}
|
|
}
|
|
catch (error) {
|
|
log.error(`Error uploading main image:`, error);
|
|
resolve(false);
|
|
}
|
|
}, "image/png");
|
|
});
|
|
}
|
|
async _renderOutputData() {
|
|
log.info("=== RENDERING OUTPUT DATA FOR COMFYUI ===");
|
|
// Użyj zunifikowanych funkcji z CanvasLayers
|
|
const imageBlob = await this.canvas.canvasLayers.getFlattenedCanvasAsBlob();
|
|
const maskBlob = await this.canvas.canvasLayers.getFlattenedMaskAsBlob();
|
|
if (!imageBlob || !maskBlob) {
|
|
throw new Error("Failed to generate canvas or mask blobs");
|
|
}
|
|
// Konwertuj blob na data URL
|
|
const imageDataUrl = await new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => resolve(reader.result);
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(imageBlob);
|
|
});
|
|
const maskDataUrl = await new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = () => resolve(reader.result);
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(maskBlob);
|
|
});
|
|
const bounds = this.canvas.outputAreaBounds;
|
|
log.info(`=== OUTPUT DATA GENERATED ===`);
|
|
log.info(`Image size: ${bounds.width}x${bounds.height}`);
|
|
log.info(`Image data URL length: ${imageDataUrl.length}`);
|
|
log.info(`Mask data URL length: ${maskDataUrl.length}`);
|
|
return { image: imageDataUrl, mask: maskDataUrl };
|
|
}
|
|
async sendDataViaWebSocket(nodeId) {
|
|
log.info(`Preparing to send data for node ${nodeId} via WebSocket.`);
|
|
const { image, mask } = await this._renderOutputData();
|
|
try {
|
|
log.info(`Sending data for node ${nodeId}...`);
|
|
await webSocketManager.sendMessage({
|
|
type: 'canvas_data',
|
|
nodeId: String(nodeId),
|
|
image: image,
|
|
mask: mask,
|
|
}, true); // `true` requires an acknowledgment
|
|
log.info(`Data for node ${nodeId} has been sent and acknowledged by the server.`);
|
|
return true;
|
|
}
|
|
catch (error) {
|
|
log.error(`Failed to send data for node ${nodeId}:`, error);
|
|
throw new Error(`Failed to get confirmation from server for node ${nodeId}. ` +
|
|
`Make sure that the nodeId: (${nodeId}) matches the "node_id" value in the node options. If they don't match, you may need to manually set the node_id to ${nodeId}.` +
|
|
`If the issue persists, try using a different browser. Some issues have been observed specifically with portable versions of Chrome, ` +
|
|
`which may have limitations related to memory or WebSocket handling. Consider testing in a standard Chrome installation, Firefox, or another browser.`);
|
|
}
|
|
}
|
|
async addInputToCanvas(inputImage, inputMask) {
|
|
try {
|
|
log.debug("Adding input to canvas:", { inputImage });
|
|
const { canvas: tempCanvas, ctx: tempCtx } = createCanvas(inputImage.width, inputImage.height);
|
|
if (!tempCtx)
|
|
throw new Error("Could not create temp context");
|
|
const imgData = new ImageData(new Uint8ClampedArray(inputImage.data), inputImage.width, inputImage.height);
|
|
tempCtx.putImageData(imgData, 0, 0);
|
|
const image = new Image();
|
|
await new Promise((resolve, reject) => {
|
|
image.onload = resolve;
|
|
image.onerror = reject;
|
|
image.src = tempCanvas.toDataURL();
|
|
});
|
|
const bounds = this.canvas.outputAreaBounds;
|
|
const scale = Math.min(bounds.width / inputImage.width * 0.8, bounds.height / inputImage.height * 0.8);
|
|
const layer = await this.canvas.canvasLayers.addLayerWithImage(image, {
|
|
x: bounds.x + (bounds.width - inputImage.width * scale) / 2,
|
|
y: bounds.y + (bounds.height - inputImage.height * scale) / 2,
|
|
width: inputImage.width * scale,
|
|
height: inputImage.height * scale,
|
|
});
|
|
if (inputMask && layer) {
|
|
layer.mask = inputMask.data;
|
|
}
|
|
log.info("Layer added successfully");
|
|
return true;
|
|
}
|
|
catch (error) {
|
|
log.error("Error in addInputToCanvas:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
async convertTensorToImage(tensor) {
|
|
try {
|
|
log.debug("Converting tensor to image:", tensor);
|
|
if (!tensor || !tensor.data || !tensor.width || !tensor.height) {
|
|
throw new Error("Invalid tensor data");
|
|
}
|
|
const { canvas, ctx } = createCanvas(tensor.width, tensor.height, '2d', { willReadFrequently: true });
|
|
if (!ctx)
|
|
throw new Error("Could not create canvas context");
|
|
const imageData = new ImageData(new Uint8ClampedArray(tensor.data), tensor.width, tensor.height);
|
|
ctx.putImageData(imageData, 0, 0);
|
|
return new Promise((resolve, reject) => {
|
|
const img = new Image();
|
|
img.onload = () => resolve(img);
|
|
img.onerror = (e) => reject(new Error("Failed to load image: " + e));
|
|
img.src = canvas.toDataURL();
|
|
});
|
|
}
|
|
catch (error) {
|
|
log.error("Error converting tensor to image:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
async convertTensorToMask(tensor) {
|
|
if (!tensor || !tensor.data) {
|
|
throw new Error("Invalid mask tensor");
|
|
}
|
|
try {
|
|
return new Float32Array(tensor.data);
|
|
}
|
|
catch (error) {
|
|
throw new Error(`Mask conversion failed: ${error.message}`);
|
|
}
|
|
}
|
|
async initNodeData() {
|
|
try {
|
|
log.info("Starting node data initialization...");
|
|
if (!this.canvas.node || !this.canvas.node.inputs) {
|
|
log.debug("Node or inputs not ready");
|
|
return this.scheduleDataCheck();
|
|
}
|
|
if (this.canvas.node.inputs[0] && this.canvas.node.inputs[0].link) {
|
|
const imageLinkId = this.canvas.node.inputs[0].link;
|
|
const imageData = window.app.nodeOutputs[imageLinkId];
|
|
if (imageData) {
|
|
log.debug("Found image data:", imageData);
|
|
await this.processImageData(imageData);
|
|
this.canvas.dataInitialized = true;
|
|
}
|
|
else {
|
|
log.debug("Image data not available yet");
|
|
return this.scheduleDataCheck();
|
|
}
|
|
}
|
|
if (this.canvas.node.inputs[1] && this.canvas.node.inputs[1].link) {
|
|
const maskLinkId = this.canvas.node.inputs[1].link;
|
|
const maskData = window.app.nodeOutputs[maskLinkId];
|
|
if (maskData) {
|
|
log.debug("Found mask data:", maskData);
|
|
await this.processMaskData(maskData);
|
|
}
|
|
}
|
|
}
|
|
catch (error) {
|
|
log.error("Error in initNodeData:", error);
|
|
return this.scheduleDataCheck();
|
|
}
|
|
}
|
|
scheduleDataCheck() {
|
|
if (this.canvas.pendingDataCheck) {
|
|
clearTimeout(this.canvas.pendingDataCheck);
|
|
}
|
|
this.canvas.pendingDataCheck = window.setTimeout(() => {
|
|
this.canvas.pendingDataCheck = null;
|
|
if (!this.canvas.dataInitialized) {
|
|
this.initNodeData();
|
|
}
|
|
}, 1000);
|
|
}
|
|
async processImageData(imageData) {
|
|
try {
|
|
if (!imageData)
|
|
return;
|
|
log.debug("Processing image data:", {
|
|
type: typeof imageData,
|
|
isArray: Array.isArray(imageData),
|
|
shape: imageData.shape,
|
|
hasData: !!imageData.data
|
|
});
|
|
if (Array.isArray(imageData)) {
|
|
imageData = imageData[0];
|
|
}
|
|
if (!imageData.shape || !imageData.data) {
|
|
throw new Error("Invalid image data format");
|
|
}
|
|
const originalWidth = imageData.shape[2];
|
|
const originalHeight = imageData.shape[1];
|
|
const scale = Math.min(this.canvas.width / originalWidth * 0.8, this.canvas.height / originalHeight * 0.8);
|
|
const convertedData = this.convertTensorToImageData(imageData);
|
|
if (convertedData) {
|
|
const image = await this.createImageFromData(convertedData);
|
|
this.addScaledLayer(image, scale);
|
|
log.info("Image layer added successfully with scale:", scale);
|
|
}
|
|
}
|
|
catch (error) {
|
|
log.error("Error processing image data:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
addScaledLayer(image, scale) {
|
|
try {
|
|
const scaledWidth = image.width * scale;
|
|
const scaledHeight = image.height * scale;
|
|
const layer = {
|
|
id: '', // This will be set in addLayerWithImage
|
|
imageId: '', // This will be set in addLayerWithImage
|
|
name: 'Layer',
|
|
image: image,
|
|
x: (this.canvas.width - scaledWidth) / 2,
|
|
y: (this.canvas.height - scaledHeight) / 2,
|
|
width: scaledWidth,
|
|
height: scaledHeight,
|
|
rotation: 0,
|
|
zIndex: this.canvas.layers.length,
|
|
originalWidth: image.width,
|
|
originalHeight: image.height,
|
|
blendMode: 'normal',
|
|
opacity: 1,
|
|
visible: true
|
|
};
|
|
this.canvas.layers.push(layer);
|
|
this.canvas.updateSelection([layer]);
|
|
this.canvas.render();
|
|
log.debug("Scaled layer added:", {
|
|
originalSize: `${image.width}x${image.height}`,
|
|
scaledSize: `${scaledWidth}x${scaledHeight}`,
|
|
scale: scale
|
|
});
|
|
}
|
|
catch (error) {
|
|
log.error("Error adding scaled layer:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
convertTensorToImageData(tensor) {
|
|
try {
|
|
const shape = tensor.shape;
|
|
const height = shape[1];
|
|
const width = shape[2];
|
|
const channels = shape[3];
|
|
log.debug("Converting tensor:", {
|
|
shape: shape,
|
|
dataRange: {
|
|
min: tensor.min_val,
|
|
max: tensor.max_val
|
|
}
|
|
});
|
|
const imageData = new ImageData(width, height);
|
|
const data = new Uint8ClampedArray(width * height * 4);
|
|
const flatData = tensor.data;
|
|
const pixelCount = width * height;
|
|
for (let i = 0; i < pixelCount; i++) {
|
|
const pixelIndex = i * 4;
|
|
const tensorIndex = i * channels;
|
|
for (let c = 0; c < channels; c++) {
|
|
const value = flatData[tensorIndex + c];
|
|
const normalizedValue = (value - tensor.min_val) / (tensor.max_val - tensor.min_val);
|
|
data[pixelIndex + c] = Math.round(normalizedValue * 255);
|
|
}
|
|
data[pixelIndex + 3] = 255;
|
|
}
|
|
imageData.data.set(data);
|
|
return imageData;
|
|
}
|
|
catch (error) {
|
|
log.error("Error converting tensor:", error);
|
|
return null;
|
|
}
|
|
}
|
|
async createImageFromData(imageData) {
|
|
return new Promise((resolve, reject) => {
|
|
const { canvas, ctx } = createCanvas(imageData.width, imageData.height, '2d', { willReadFrequently: true });
|
|
if (!ctx)
|
|
throw new Error("Could not create canvas context");
|
|
ctx.putImageData(imageData, 0, 0);
|
|
const img = new Image();
|
|
img.onload = () => resolve(img);
|
|
img.onerror = reject;
|
|
img.src = canvas.toDataURL();
|
|
});
|
|
}
|
|
async processMaskData(maskData) {
|
|
try {
|
|
if (!maskData)
|
|
return;
|
|
log.debug("Processing mask data:", maskData);
|
|
if (Array.isArray(maskData)) {
|
|
maskData = maskData[0];
|
|
}
|
|
if (!maskData.shape || !maskData.data) {
|
|
throw new Error("Invalid mask data format");
|
|
}
|
|
if (this.canvas.canvasSelection.selectedLayers.length > 0) {
|
|
const maskTensor = await this.convertTensorToMask(maskData);
|
|
this.canvas.canvasSelection.selectedLayers[0].mask = maskTensor;
|
|
this.canvas.render();
|
|
log.info("Mask applied to selected layer");
|
|
}
|
|
}
|
|
catch (error) {
|
|
log.error("Error processing mask data:", error);
|
|
}
|
|
}
|
|
async importLatestImage() {
|
|
try {
|
|
log.info("Fetching latest image from server...");
|
|
const response = await fetch('/ycnode/get_latest_image');
|
|
const result = await response.json();
|
|
if (result.success && result.image_data) {
|
|
log.info("Latest image received, adding to canvas.");
|
|
const img = new Image();
|
|
await new Promise((resolve, reject) => {
|
|
img.onload = resolve;
|
|
img.onerror = reject;
|
|
img.src = result.image_data;
|
|
});
|
|
await this.canvas.canvasLayers.addLayerWithImage(img, {}, 'fit');
|
|
log.info("Latest image imported and placed on canvas successfully.");
|
|
return true;
|
|
}
|
|
else {
|
|
throw new Error(result.error || "Failed to fetch the latest image.");
|
|
}
|
|
}
|
|
catch (error) {
|
|
log.error("Error importing latest image:", error);
|
|
showErrorNotification(`Failed to import latest image: ${error.message}`);
|
|
return false;
|
|
}
|
|
}
|
|
async importLatestImages(sinceTimestamp, targetArea = null) {
|
|
try {
|
|
log.info(`Fetching latest images since ${sinceTimestamp}...`);
|
|
const response = await fetch(`/layerforge/get-latest-images/${sinceTimestamp}`);
|
|
const result = await response.json();
|
|
if (result.success && result.images && result.images.length > 0) {
|
|
log.info(`Received ${result.images.length} new images, adding to canvas.`);
|
|
const newLayers = [];
|
|
for (const imageData of result.images) {
|
|
const img = new Image();
|
|
await new Promise((resolve, reject) => {
|
|
img.onload = resolve;
|
|
img.onerror = reject;
|
|
img.src = imageData;
|
|
});
|
|
let processedImage = img;
|
|
// If there's a custom shape, clip the image to that shape
|
|
if (this.canvas.outputAreaShape && this.canvas.outputAreaShape.isClosed) {
|
|
processedImage = await this.clipImageToShape(img, this.canvas.outputAreaShape);
|
|
}
|
|
const newLayer = await this.canvas.canvasLayers.addLayerWithImage(processedImage, {}, 'fit', targetArea);
|
|
newLayers.push(newLayer);
|
|
}
|
|
log.info("All new images imported and placed on canvas successfully.");
|
|
return newLayers.filter(l => l !== null);
|
|
}
|
|
else if (result.success) {
|
|
log.info("No new images found since last generation.");
|
|
return [];
|
|
}
|
|
else {
|
|
throw new Error(result.error || "Failed to fetch latest images.");
|
|
}
|
|
}
|
|
catch (error) {
|
|
log.error("Error importing latest images:", error);
|
|
showErrorNotification(`Failed to import latest images: ${error.message}`);
|
|
return [];
|
|
}
|
|
}
|
|
async clipImageToShape(image, shape) {
|
|
return new Promise((resolve, reject) => {
|
|
const { canvas, ctx } = createCanvas(image.width, image.height);
|
|
if (!ctx) {
|
|
reject(new Error("Could not create canvas context for clipping"));
|
|
return;
|
|
}
|
|
// Draw the image first
|
|
ctx.drawImage(image, 0, 0);
|
|
// Calculate custom shape position accounting for extensions
|
|
// Custom shape should maintain its relative position within the original canvas area
|
|
const ext = this.canvas.outputAreaExtensionEnabled ? this.canvas.outputAreaExtensions : { top: 0, bottom: 0, left: 0, right: 0 };
|
|
const shapeOffsetX = ext.left; // Add left extension to maintain relative position
|
|
const shapeOffsetY = ext.top; // Add top extension to maintain relative position
|
|
// Create a clipping mask using the shape with extension offset
|
|
ctx.globalCompositeOperation = 'destination-in';
|
|
ctx.beginPath();
|
|
ctx.moveTo(shape.points[0].x + shapeOffsetX, shape.points[0].y + shapeOffsetY);
|
|
for (let i = 1; i < shape.points.length; i++) {
|
|
ctx.lineTo(shape.points[i].x + shapeOffsetX, shape.points[i].y + shapeOffsetY);
|
|
}
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
// Create a new image from the clipped canvas
|
|
const clippedImage = new Image();
|
|
clippedImage.onload = () => resolve(clippedImage);
|
|
clippedImage.onerror = () => reject(new Error("Failed to create clipped image"));
|
|
clippedImage.src = canvas.toDataURL();
|
|
});
|
|
}
|
|
}
|