Files
LAOGOU-666-Comfyui_LG_Tools/web/CanvasNode.js
T
2025-05-24 21:35:21 +08:00

3308 lines
120 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { fabric } from "./lib/fabric.js"; // 请确保 fabric.js 已被正确导入
// 定义常量
const CANVAS_SIZE = {
WIDTH: 420,
HEIGHT: 200,
BOTTOM_MARGIN: 110, // 节点下沿扩展值
RIGHT_MARGIN: 20, // 节点右沿扩展值
CONTROL_PANEL_HEIGHT: 30 // 控制面板高度
};
class FastCanvas {
constructor(node, initialSize = null) {
this.node = node;
this.lastCanvasState = null;
// 使用传入的初始尺寸或默认尺寸
this.originalSize = initialSize || {
width: CANVAS_SIZE.WIDTH,
height: CANVAS_SIZE.HEIGHT
};
// 立即应用尺寸到实例属性
this.currentSize = { ...this.originalSize };
this.maxDisplaySize = 768;
// 创建缩放值显示元素
this.scaleText = document.createElement('div');
this.scaleText.style.cssText = `
position: absolute;
top: 10px;
right: 10px;
background: rgba(0,0,0,0.5);
color: white;
padding: 2px 5px;
border-radius: 3px;
font-size: 12px;
pointer-events: none;
z-index: 1000;
display: none;
`;
this.initCanvas();
this.setupDragAndDrop();
this.setupPaste();
}
initCanvas() {
this.canvasContainer = document.createElement('div');
this.canvasContainer.className = 'fast-canvas-container';
// 创建 canvas 元素
const canvasElement = document.createElement('canvas');
// 使用原始尺寸创建画布
this.canvas = new fabric.Canvas(canvasElement, {
width: this.originalSize.width,
height: this.originalSize.height,
preserveObjectStacking: true,
selection: true
});
// 设置选中框样式
this.canvas.selectionColor = 'rgba(0, 123, 255, 0.3)'; // 选中区域填充色
this.canvas.selectionBorderColor = '#007bff'; // 选中框边框颜色
this.canvas.selectionLineWidth = 2; // 选中框边框宽度
// 设置控制点样式
fabric.Object.prototype.set({
transparentCorners: false, // 控制点不透明
cornerColor: '#007bff', // 控制点颜色
cornerSize: 20, // 控制点大小
cornerStyle: 'circle', // 控制点形状为圆形
cornerStrokeColor: '#ffffff', // 控制点边框颜色
cornerStrokeWidth: 2, // 控制点边框宽度
padding: 5, // 选中框内边距
borderColor: '#007bff', // 边框颜色
borderScaleFactor: 2, // 边框宽度
hasRotatingPoint: true, // 显示旋转控制点
rotatingPointOffset: 30 // 旋转控制点偏移距离
});
// 创建默认背景图像
const defaultBackground = new fabric.Rect({
width: this.originalSize.width,
height: this.originalSize.height,
left: 0,
top: 0,
fill: '#000000',
selectable: false,
evented: false,
excludeFromExport: false
});
// 设置为背景图像
this.canvas.setBackgroundImage(defaultBackground, () => {
this.canvas.renderAll();
});
this.canvasContainer.style.cssText = `
position: relative;
width: ${this.originalSize.width}px;
height: ${this.originalSize.height + CANVAS_SIZE.CONTROL_PANEL_HEIGHT}px;
background: transparent; // 改为透明背景
`;
// 创建画布包装容器
const canvasWrapper = document.createElement('div');
canvasWrapper.style.cssText = `
width: 100%;
height: ${this.originalSize.height}px;
`;
canvasWrapper.appendChild(this.canvas.wrapperEl);
this.canvasContainer.appendChild(canvasWrapper);
this.canvasContainer.appendChild(this.scaleText);
// 创建控制面板
this.controlPanel = new ControlPanel(this.canvas, this.node);
this.canvasContainer.appendChild(this.controlPanel.getContainer());
// 初始化状态
this.layers = new Map();
this.background = defaultBackground; // 保存背景引用
this.isDragging = false;
this.isLocked = false;
this.initContextMenu();
// 设置事件监听
this.setupEventListeners();
this.setupWebSocket();
// 设置节点的初始尺寸
this.node.size = [
this.originalSize.width + CANVAS_SIZE.RIGHT_MARGIN,
this.originalSize.height + CANVAS_SIZE.CONTROL_PANEL_HEIGHT + CANVAS_SIZE.BOTTOM_MARGIN
];
}
initContextMenu() {
// 创建菜单元素
const menu = document.createElement('div');
menu.className = 'fast-canvas-context-menu';
menu.style.cssText = `
position: fixed;
display: none;
background: #2a2a2a;
border: 1px solid #3f3f3f;
border-radius: 6px;
padding: 3px;
z-index: 1000;
color: white;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
transition: background-color 0.2s ease;
min-width: 120px;
`;
const menuItems = [
{ text: '设为背景', action: () => this.setLayerAsBackground() },
{
text: '变换',
submenu: [
{ text: '水平翻转', action: () => this.flipObject('horizontal') },
{ text: '垂直翻转', action: () => this.flipObject('vertical') },
{ text: '居中', action: () => this.centerObject('both') },
{ text: '水平居中', action: () => this.centerObject('horizontal') },
{ text: '垂直居中', action: () => this.centerObject('vertical') }
]
},
{
text: '调整图层',
submenu: [
{ text: '置于顶层', action: () => {
const obj = this.canvas.getActiveObject();
if (obj) {
obj.bringToFront();
this.canvas.renderAll();
this.requestStateUpdate();
}
}},
{ text: '上移一层', action: () => {
const obj = this.canvas.getActiveObject();
if (obj) {
obj.bringForward();
this.canvas.renderAll();
this.requestStateUpdate();
}
}},
{ text: '下移一层', action: () => {
const obj = this.canvas.getActiveObject();
if (obj) {
obj.sendBackwards();
this.canvas.renderAll();
this.requestStateUpdate();
}
}},
{ text: '置于底层', action: () => {
const obj = this.canvas.getActiveObject();
if (obj) {
obj.sendToBack();
this.canvas.renderAll();
this.requestStateUpdate();
}
}}
]
},
{ text: '恢复原尺寸', action: () => this.resetObjectSize() }
];
// 创建菜单项
menuItems.forEach(item => {
const menuItem = document.createElement('div');
menuItem.textContent = item.text;
menuItem.style.cssText = `
padding: 6px 15px;
margin: 1px 3px;
cursor: pointer;
white-space: nowrap;
transition: background-color 0.15s ease;
font-size: 13px;
border-radius: 4px;
position: relative;
`;
if (item.submenu) {
// 添加子菜单指示器
menuItem.style.paddingRight = '25px';
const arrow = document.createElement('span');
arrow.textContent = '▶';
arrow.style.cssText = `
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
font-size: 10px;
`;
menuItem.appendChild(arrow);
// 创建子菜单
const submenu = document.createElement('div');
submenu.style.cssText = `
position: absolute;
left: 100%;
top: 0;
display: none;
background: #2a2a2a;
border: 1px solid #3f3f3f;
border-radius: 6px;
padding: 3px;
z-index: 1001;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
`;
// 创建子菜单项
item.submenu.forEach(subItem => {
const subMenuItem = document.createElement('div');
subMenuItem.textContent = subItem.text;
subMenuItem.style.cssText = `
padding: 6px 15px;
margin: 1px 3px;
cursor: pointer;
white-space: nowrap;
transition: background-color 0.15s ease;
font-size: 13px;
border-radius: 4px;
`;
subMenuItem.onmouseover = () => subMenuItem.style.backgroundColor = '#3f3f3f';
subMenuItem.onmouseout = () => subMenuItem.style.backgroundColor = '';
subMenuItem.onclick = (e) => {
e.stopPropagation();
subItem.action();
menu.style.display = 'none';
};
submenu.appendChild(subMenuItem);
});
// 显示/隐藏子菜单
menuItem.onmouseover = () => {
submenu.style.display = 'block';
menuItem.style.backgroundColor = '#3f3f3f';
};
menuItem.onmouseout = (e) => {
if (!e.relatedTarget || !submenu.contains(e.relatedTarget)) {
submenu.style.display = 'none';
menuItem.style.backgroundColor = '';
}
};
submenu.onmouseover = () => {
submenu.style.display = 'block';
menuItem.style.backgroundColor = '#3f3f3f';
};
submenu.onmouseout = (e) => {
if (!menuItem.contains(e.relatedTarget)) {
submenu.style.display = 'none';
menuItem.style.backgroundColor = '';
}
};
menuItem.appendChild(submenu);
} else {
// 普通菜单项的鼠标悬停效果
menuItem.onmouseover = () => menuItem.style.backgroundColor = '#3f3f3f';
menuItem.onmouseout = () => menuItem.style.backgroundColor = '';
menuItem.onclick = (e) => {
e.stopPropagation();
item.action();
menu.style.display = 'none';
};
}
menu.appendChild(menuItem);
});
// 在画布容器上监听右键事件
this.canvasContainer.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
const activeObject = this.canvas.getActiveObject();
if (activeObject) {
menu.style.left = `${e.pageX}px`;
menu.style.top = `${e.pageY}px`;
menu.style.display = 'block';
// 添加一次性点击事件监听器
const closeMenu = (e) => {
if (!menu.contains(e.target)) {
menu.style.display = 'none';
document.removeEventListener('click', closeMenu);
document.removeEventListener('contextmenu', closeMenu);
}
};
// 延迟添加事件监听器,避免立即触发
setTimeout(() => {
document.addEventListener('click', closeMenu);
document.addEventListener('contextmenu', closeMenu);
}, 0);
}
return false;
}, true);
// 阻止菜单内右键点击
menu.addEventListener('contextmenu', (e) => {
e.preventDefault();
e.stopPropagation();
return false;
});
document.body.appendChild(menu);
}
// 菜单操作方法
flipObject(direction) {
const activeObject = this.canvas.getActiveObject();
if (activeObject) {
if (direction === 'horizontal') {
activeObject.flipX = !activeObject.flipX;
} else {
activeObject.flipY = !activeObject.flipY;
}
this.canvas.renderAll();
}
}
centerObject(direction) {
const activeObject = this.canvas.getActiveObject();
if (activeObject) {
if (direction === 'both') {
activeObject.center();
} else if (direction === 'horizontal') {
activeObject.centerH();
} else {
activeObject.centerV();
}
activeObject.setCoords();
this.canvas.renderAll();
}
}
resetObjectSize() {
const activeObject = this.canvas.getActiveObject();
const backgroundImage = this.canvas.backgroundImage;
if (activeObject && backgroundImage) {
const scaleRatio = backgroundImage.width / backgroundImage.getScaledWidth();
const center = activeObject.getCenterPoint();
activeObject.set({
scaleX: 1 / scaleRatio,
scaleY: 1 / scaleRatio
});
activeObject.setPositionByOrigin(center, 'center', 'center');
activeObject.setCoords();
this.canvas.renderAll();
// 更新右上角缩放值显示
const relativeScale = 1.0; // 恢复原尺寸时,相对缩放比例为1
this.scaleText.innerHTML = `scale: ${relativeScale}×`;
this.scaleText.style.display = 'block';
}
}
updateContainerSize(width, height) {
// 更新画布容器尺寸为缩放后的显示尺寸
this.canvasContainer.style.width = `${width}px`;
this.canvasContainer.style.height = `${height + CANVAS_SIZE.CONTROL_PANEL_HEIGHT}px`;
// 确保控制面板始终在最上层
this.canvasContainer.style.position = 'relative'; // 添加这行
this.canvasContainer.style.zIndex = '1'; // 添加这行
// 更新控制面板尺寸
this.controlPanel.updateSize(width);
// 更新节点的 DOM 元素尺寸
if (this.node.canvasElement) {
this.node.canvasElement.style.width = `${width}px`;
this.node.canvasElement.style.height = `${height + CANVAS_SIZE.CONTROL_PANEL_HEIGHT}px`;
}
// 更新节点尺寸
this.node.size = [
width + CANVAS_SIZE.RIGHT_MARGIN,
height + CANVAS_SIZE.BOTTOM_MARGIN + CANVAS_SIZE.CONTROL_PANEL_HEIGHT
];
// 强制更新节点
this.node.setDirtyCanvas(true, true);
}
updateCanvasSize(displayWidth, displayHeight) {
// 1. 计算缩放比例
const scaleX = displayWidth / this.originalSize.width;
const scaleY = displayHeight / this.originalSize.height;
const scale = Math.min(scaleX, scaleY); // 使用统一的缩放比例
// 2. 设置画布为原始尺寸
this.canvas.setDimensions({
width: this.originalSize.width,
height: this.originalSize.height
});
// 3. 更新画布包装器的CSS transform
this.canvas.wrapperEl.style.transform = `scale(${scale})`;
this.canvas.wrapperEl.style.transformOrigin = 'top left';
this.canvas.wrapperEl.style.width = `${displayWidth}px`;
this.canvas.wrapperEl.style.height = `${displayHeight}px`;
// 4. 同步Fabric.js的缩放
this.canvas.setZoom(1); // 重置zoom,因为我们使用CSS缩放
// 5. 如果有背景图片,确保它使用原始尺寸
if (this.canvas.backgroundImage) {
this.canvas.backgroundImage.set({
width: this.originalSize.width,
height: this.originalSize.height,
scaleX: 1,
scaleY: 1,
left: 0,
top: 0,
originX: 'left',
originY: 'top'
});
// 强制更新背景坐标
this.canvas.backgroundImage.setCoords();
}
// 6. 更新容器尺寸
this.updateContainerSize(displayWidth, displayHeight);
// 7. 重新渲染
this.canvas.renderAll();
// 8. 更新事件监听区域
this.canvas.calcOffset();
}
// 修改 updateDisplayScale 方法
updateDisplayScale(maxSize) {
this.maxDisplaySize = maxSize;
if (this.originalSize?.width && this.originalSize?.height) {
const scaledSize = this.calculateScaledSize(
this.originalSize.width,
this.originalSize.height,
maxSize
);
// 更新显示尺寸,但保持画布原始尺寸
this.updateCanvasSize(scaledSize.width, scaledSize.height);
// 确保画布完全重置和更新
this.canvas.requestRenderAll();
}
}
// 计算缩放后的尺寸
calculateScaledSize(width, height, maxSize) {
if (width <= maxSize && height <= maxSize) {
return { width, height };
}
const ratio = width / height;
if (width > height) {
return {
width: maxSize,
height: Math.round(maxSize / ratio)
};
} else {
return {
width: Math.round(maxSize * ratio),
height: maxSize
};
}
}
setupPaste() {
// 移除可能存在的旧事件监听器
if (this._pasteHandler) {
document.removeEventListener('paste', this._pasteHandler);
}
let canvasActive = false;
// 创建新的粘贴处理函数
this._pasteHandler = async (e) => {
if (!canvasActive) return; // 如果画布不是活动状态,不处理粘贴
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
for (const item of items) {
if (item.type.indexOf('image') === 0) {
e.preventDefault();
e.stopPropagation();
const blob = item.getAsFile();
await this.handleImageUpload(blob, { center: true, autoScale: true });
break;
}
}
};
// 添加内部复制功能
document.addEventListener('keydown', (e) => {
// 检查是否在画布区域内且画布处于活动状态
const activeElement = document.activeElement;
const isInputActive = activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA';
if (isInputActive || !canvasActive) return;
// 复制 (Ctrl+C)
if (e.ctrlKey && e.key === 'c') {
const activeObjects = this.canvas.getActiveObjects();
if (activeObjects.length > 0) {
activeObjects.forEach(obj => {
// 创建图层的副本
obj.clone((cloned) => {
// 生成新的图层ID
const newId = this.controlPanel.maxLayerId + 1;
this.controlPanel.maxLayerId = newId;
cloned.set({
left: obj.left + 20,
top: obj.top + 20,
id: newId
});
// 添加到画布和图层管理
this.canvas.add(cloned);
this.layers.set(newId, cloned);
});
});
this.canvas.renderAll();
this.controlPanel.updateLayerSelector();
console.log('已复制选中的图层');
}
}
});
// 添加新的事件监听器到 document
document.addEventListener('paste', this._pasteHandler, true);
// 点击画布时设置活动状态
this.canvasContainer.addEventListener('mousedown', () => {
canvasActive = true;
});
// 点击其他地方时取消活动状态
document.addEventListener('mousedown', (e) => {
if (!this.canvasContainer.contains(e.target)) {
canvasActive = false;
}
});
// 清理函数
this.cleanup = () => {
document.removeEventListener('paste', this._pasteHandler, true);
};
}
setupDragAndDrop() {
// 防止浏览器默认的拖放行为
this.canvasContainer.addEventListener('dragenter', (e) => {
e.preventDefault();
e.stopPropagation();
this.canvasContainer.classList.add('drag-over');
});
this.canvasContainer.addEventListener('dragover', (e) => {
e.preventDefault();
e.stopPropagation();
});
this.canvasContainer.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
if (e.target === this.canvasContainer) {
this.canvasContainer.classList.remove('drag-over');
}
});
this.canvasContainer.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
this.canvasContainer.classList.remove('drag-over');
const files = e.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
if (file.type.startsWith('image/')) {
await this.handleImageUpload(file, { center: true, autoScale: true });
}
}
});
// 添加样式
const style = document.createElement('style');
style.textContent = `
.fast-canvas-container.drag-over::after {
content: '拖放图片到这里';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: ${CANVAS_SIZE.CONTROL_PANEL_HEIGHT}px;
background: rgba(0, 0, 0, 0.7);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
pointer-events: none;
z-index: 1000;
}
`;
document.head.appendChild(style);
}
async handleImageUpload(file, options = { center: true, autoScale: true }) {
try {
const reader = new FileReader();
const imageLoadPromise = new Promise((resolve, reject) => {
reader.onload = (e) => {
const dataUrl = e.target.result;
resolve(dataUrl);
};
reader.onerror = reject;
});
reader.readAsDataURL(file);
const imageData = await imageLoadPromise;
// 统一按图层方式加载图片
fabric.Image.fromURL(imageData, (img) => {
if (options.center && options.autoScale) {
// 计算适合画布的缩放比例
const scale = Math.min(
this.canvas.width / img.width,
this.canvas.height / img.height
);
// 计算居中位置
const canvasCenter = this.canvas.getCenter();
img.set({
scaleX: scale,
scaleY: scale,
left: canvasCenter.left,
top: canvasCenter.top,
originX: 'center',
originY: 'center'
});
} else {
// 不进行居中和缩放,使用原始尺寸和位置
img.set({
left: 0,
top: 0,
originX: 'left',
originY: 'top'
});
}
// 添加到画布并更新图层ID
this.canvas.add(img);
this.controlPanel.updateLayerIds();
this.canvas.setActiveObject(img);
this.canvas.renderAll();
this.requestStateUpdate();
});
} catch (error) {
console.error('图片上传失败:', error);
alert('图片上传失败,请重试');
}
}
setupWebSocket() {
// 先移除旧的监听器
api.removeEventListener("fast_canvas_update", this._handleCanvasUpdate);
api.addEventListener("fast_canvas_update", async (event) => {
const data = event.detail;
if (data && data.node_id && data.node_id === this.node.id.toString()) {
if (data.canvas_data) {
await this.updateCanvas(data.canvas_data);
}
}
});
api.addEventListener("fast_canvas_get_state", async (event) => {
const data = event.detail;
if (data && data.node_id && data.node_id === this.node.id.toString()) {
await this.sendCanvasState();
}
});
}
requestStateUpdate() {
requestAnimationFrame(async () => {
if (!this.canvas || this.canvas.isEmpty()) return;
// 只在画布真正变化时更新种子
if (this.hasCanvasChanged()) {
if (this.node && typeof this.node.updateSeed === 'function') {
this.node.updateSeed();
}
this.canvas.renderAll();
}
});
}
setupEventListeners() {
// 开始拖拽
this.canvas.on('mouse:down', (event) => {
const activeObject = this.canvas.getActiveObject();
if (activeObject) {
this.isDragging = true;
}
});
// 结束拖拽
this.canvas.on('mouse:up', (event) => {
if (this.isDragging) {
this.isDragging = false;
this.requestStateUpdate(); // 只在拖拽结束时更新状态
}
});
// 监听图层添加/删除
this.canvas.on('object:added', () => {
this.requestStateUpdate();
});
this.canvas.on('object:removed', () => {
this.requestStateUpdate();
});
// 监听所有对象修改
this.canvas.on('object:modified', () => {
this.requestStateUpdate();
});
// 监听背景变化
this.canvas.on('background:modified', () => {
this.requestStateUpdate();
});
// 添加一个计算相对缩放值的辅助方法
const getRelativeScale = (obj) => {
const backgroundImage = this.canvas.backgroundImage;
if (!backgroundImage) return obj.scaleX;
// 计算背景图片的缩放比例
const bgScaleRatio = backgroundImage.width / backgroundImage.getScaledWidth();
// 计算相对于背景图片的实际缩放比例
return Number((obj.scaleX * bgScaleRatio).toFixed(2));
};
// 创建一个绑定到当前实例的事件处理函数
this._handleKeyDown = (e) => {
// 首先检查画布和节点是否还存在
if (!this.canvas || !this.node) {
// 如果画布或节点不存在,移除事件监听器
document.removeEventListener('keydown', this._handleKeyDown);
return;
}
// 检查是否有选中的对象且当前焦点不在输入框上
if (this.canvas.getActiveObject() &&
document.activeElement.tagName !== 'INPUT' &&
document.activeElement.tagName !== 'TEXTAREA') {
// Delete 键或 Backspace 键
if (e.key === 'Delete' || e.key === 'Backspace') {
e.preventDefault();
const activeObject = this.canvas.getActiveObject();
// 确保不是背景图片
if (activeObject !== this.canvas.backgroundImage) {
// 如果是图层,从图层映射中移除
if (activeObject.id) {
this.layers.delete(activeObject.id);
}
// 从画布中移除
this.canvas.remove(activeObject);
this.canvas.renderAll();
// 触发状态更新
this.requestStateUpdate();
}
}
}
};
// 添加事件监听器
document.addEventListener('keydown', this._handleKeyDown);
// 添加滚轮缩放
this.canvas.on('mouse:wheel', (opt) => {
const delta = opt.e.deltaY;
const activeObject = this.canvas.getActiveObject();
if (activeObject) {
let scale = activeObject.scaleX;
if (delta < 0) {
scale *= 1.1; // 放大
} else {
scale *= 0.9; // 缩小
}
// 限制缩放范围
scale = Math.min(Math.max(scale, 0.01), 10);
activeObject.scale(scale);
this.canvas.requestRenderAll();
// 更新缩放值显示
const relativeScale = getRelativeScale(activeObject);
this.scaleText.innerHTML = `scale: ${relativeScale}×`;
this.scaleText.style.display = 'block';
opt.e.preventDefault();
opt.e.stopPropagation();
}
});
}
hasCanvasChanged() {
if (!this.canvas) return false;
const currentState = {
objects: this.canvas.getObjects().map(obj => ({
id: obj.id,
left: obj.left,
top: obj.top,
scaleX: obj.scaleX,
scaleY: obj.scaleY,
angle: obj.angle
})),
background: this.background?.fill,
selectedId: this.canvas.getActiveObject()?.id
};
const stateJSON = JSON.stringify(currentState);
const hasChanged = !this.lastCanvasState || stateJSON !== this.lastCanvasState;
if (hasChanged) {
this.lastCanvasState = stateJSON;
}
return hasChanged;
}
async updateCanvas(canvasData) {
if (!canvasData) return;
try {
// 先获取当前所有图层的状态
const currentLayers = this.canvas.getObjects().map(obj => ({
object: obj,
state: {
left: obj.left,
top: obj.top,
scaleX: obj.scaleX,
scaleY: obj.scaleY,
angle: obj.angle
}
}));
// 先设置背景
if (canvasData.background) {
await this.setBackground(canvasData.background);
}
// 再更新图层
if (canvasData.layers) {
await this.updateLayers(canvasData.layers);
// 恢复图层状态
currentLayers.forEach(({object, state}) => {
const existingObj = this.canvas.getObjects().find(obj => obj.id === object.id);
if (existingObj) {
existingObj.set(state);
}
});
}
// 确保画布完全渲染
await new Promise(resolve => {
requestAnimationFrame(() => {
this.canvas.renderAll();
resolve();
});
});
await this.sendCanvasState();
} catch (error) {
}
}
// 添加一个通用的图像加载函数
loadImage(imageUrl, timeout = 5000) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error('Image loading timeout'));
}, timeout);
fabric.Image.fromURL(imageUrl,
(img) => {
clearTimeout(timeoutId);
if (!img) {
console.error("[FastCanvas] Failed to create image from URL");
reject(new Error('Failed to create image'));
return;
}
resolve(img);
},
(error) => {
clearTimeout(timeoutId);
console.error("[FastCanvas] Error loading image:", error);
reject(error);
},
{ crossOrigin: 'anonymous' } // 添加跨域支持
);
});
}
setLayerAsBackground() {
const activeObject = this.canvas.getActiveObject();
if (!activeObject) return;
// 移除当前图层
this.canvas.remove(activeObject);
// 移除图层ID
this.controlPanel.removeLayerId(activeObject);
// 准备背景数据
const backgroundData = {
image: activeObject.getSrc()
};
// 使用现有的 setBackground 方法设置背景
this.setBackground(backgroundData);
}
async setBackground(backgroundData) {
if (!backgroundData?.image) {
console.log("[FastCanvas] No background image data");
return;
}
try {
console.log("[FastCanvas] Loading background image...");
const img = await this.loadImage(backgroundData.image);
// 保存当前所有图层的状态
const layerStates = this.canvas.getObjects().map(obj => ({
object: obj,
state: {
left: obj.left,
top: obj.top,
scaleX: obj.scaleX,
scaleY: obj.scaleY,
angle: obj.angle
}
}));
// 保存原始尺寸
this.originalSize = {
width: img.width,
height: img.height
};
// 计算显示尺寸
const scaledSize = this.calculateScaledSize(
img.width,
img.height,
this.maxDisplaySize
);
// 设置画布尺寸为原始尺寸
this.canvas.setDimensions({
width: this.originalSize.width,
height: this.originalSize.height
});
// 设置背景图片属性
img.set({
scaleX: 1,
scaleY: 1,
left: 0,
top: 0,
selectable: false,
evented: false,
originX: 'left',
originY: 'top'
});
// 设置CSS缩放
const scale = scaledSize.width / this.originalSize.width;
this.canvas.wrapperEl.style.transform = `scale(${scale})`;
this.canvas.wrapperEl.style.transformOrigin = 'top left';
// 更新容器尺寸
this.updateContainerSize(scaledSize.width, scaledSize.height);
// 更新尺寸输入框
if (this.node?.sizeOverlay?.updateSizeInputs) {
this.node.sizeOverlay.updateSizeInputs(
this.originalSize.width,
this.originalSize.height
);
}
// 直接设置背景图片
return new Promise((resolve) => {
this.canvas.setBackgroundImage(img, () => {
// 恢复所有图层的状态
layerStates.forEach(({object, state}) => {
object.set(state);
});
console.log("[FastCanvas] Background set successfully");
this.canvas.renderAll();
resolve();
});
});
} catch (error) {
console.error("[FastCanvas] Background loading failed:", error);
this.sendErrorState("Background loading failed");
}
}
addLayer(layerData) {
return new Promise((resolve) => {
fabric.Image.fromURL(layerData.image, (img) => {
if (!img) {
console.error("[FastCanvas] Failed to load layer image:", layerData.id);
resolve();
return;
}
// 计算适合画布的缩放比例
const scale = Math.min(
this.canvas.width / img.width,
this.canvas.height / img.height
);
// 计算居中位置
const centerX = (this.canvas.width - img.width * scale) / 2;
const centerY = (this.canvas.height - img.height * scale) / 2;
img.set({
id: layerData.id,
scaleX: scale,
scaleY: scale,
left: centerX,
top: centerY,
selectable: true,
hasControls: true,
hasBorders: true,
originalWidth: img.width,
originalHeight: img.height
});
this.canvas.add(img);
this.layers.set(layerData.id, img);
// 添加以下代码来保持选中状态
this.canvas.setActiveObject(img);
this.canvas.renderAll();
resolve();
});
});
}
updateLayer(layerData) {
return new Promise((resolve) => {
const existingObj = this.layers.get(layerData.id);
if (existingObj) {
// 保存当前的变换状态
const currentState = {
scaleX: existingObj.scaleX,
scaleY: existingObj.scaleY,
left: existingObj.left,
top: existingObj.top,
angle: existingObj.angle,
flipX: existingObj.flipX,
flipY: existingObj.flipY
};
// 检查当前对象是否被选中
const wasActive = this.canvas.getActiveObject() === existingObj;
fabric.Image.fromURL(layerData.image, (newImg) => {
if (!newImg) {
resolve();
return;
}
// 直接应用所有保存的变换状态
newImg.set({
...currentState,
id: layerData.id,
selectable: true,
hasControls: true,
hasBorders: true,
originalWidth: newImg.width,
originalHeight: newImg.height
});
// 替换旧图层
const index = this.canvas.getObjects().indexOf(existingObj);
this.canvas.remove(existingObj);
this.canvas.insertAt(newImg, index);
this.layers.set(layerData.id, newImg);
// 如果原对象是选中状态,则保持新对象的选中状态
if (wasActive) {
this.canvas.setActiveObject(newImg);
}
this.canvas.renderAll();
resolve();
});
} else {
// 如果图层不存在,作为新图层添加
this.addLayer(layerData).then(resolve);
}
});
}
async updateLayers(layersData) {
try {
// 异步移除不再需要的图层
const currentIds = new Set(layersData.map(layer => layer.id));
const removePromises = [];
for (const [id, fabricObj] of this.layers.entries()) {
if (!currentIds.has(id)) {
removePromises.push(
new Promise(resolve => {
this.canvas.remove(fabricObj);
this.layers.delete(id);
requestAnimationFrame(resolve);
})
);
}
}
if (removePromises.length > 0) {
console.log("[FastCanvas] Removing old layers");
await Promise.all(removePromises);
this.canvas.renderAll();
}
for (const layerData of layersData) {
if (this.layers.has(layerData.id)) {
await this.updateLayer(layerData);
} else {
await this.addLayer(layerData);
}
// 更新最大图层ID
this.controlPanel.maxLayerId = Math.max(
this.controlPanel.maxLayerId,
layerData.id
);
}
this.controlPanel.updateLayerSelector();
// 确保最终渲染
return new Promise(resolve => {
requestAnimationFrame(() => {
this.canvas.renderAll();
resolve();
});
});
} catch (error) {
throw error;
}
}
async getLayerMask(selectedLayer, ignoreOcclusion = false) { // 添加参数控制是否忽略遮挡
if (!selectedLayer || !this.canvas.backgroundImage) {
return null;
}
// 1. 创建与原始画布相同尺寸的遮罩画布
const maskCanvas = document.createElement('canvas');
maskCanvas.width = this.originalSize.width;
maskCanvas.height = this.originalSize.height;
const ctx = maskCanvas.getContext('2d');
// 清除画布,保持透明背景
ctx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
// 渲染选中图层
if (selectedLayer.visible !== false) {
ctx.save();
// 直接使用图层的变换矩阵,不需要缩放调整
const matrix = selectedLayer.calcTransformMatrix();
ctx.transform(
matrix[0],
matrix[1],
matrix[2],
matrix[3],
matrix[4],
matrix[5]
);
// 绘制图层
if (selectedLayer._element) {
ctx.drawImage(
selectedLayer._element,
-selectedLayer.width / 2,
-selectedLayer.height / 2,
selectedLayer.width,
selectedLayer.height
);
}
ctx.restore();
}
// 2. 只在需要时处理上层遮挡
if (!ignoreOcclusion) {
const allLayers = this.canvas.getObjects()
.filter(obj => obj !== this.canvas.backgroundImage);
const selectedLayerIndex = allLayers.indexOf(selectedLayer);
const upperLayers = allLayers.slice(selectedLayerIndex + 1);
if (upperLayers.length > 0) {
ctx.globalCompositeOperation = 'destination-out';
upperLayers.forEach(layer => {
if (layer.visible !== false) {
ctx.save();
const matrix = layer.calcTransformMatrix();
ctx.transform(
matrix[0],
matrix[1],
matrix[2],
matrix[3],
matrix[4],
matrix[5]
);
if (layer._element) {
ctx.drawImage(
layer._element,
-layer.width / 2,
-layer.height / 2,
layer.width,
layer.height
);
}
ctx.restore();
}
});
}
}
return maskCanvas;
}
async sendCanvasState() {
if (!this.canvas) {
console.log('[FastCanvas] 画布未初始化');
return;
}
try {
const timestamp = Date.now();
console.log('[FastCanvas] 画布信息:', {
'画布元素尺寸': `${this.canvas.width}x${this.canvas.height}`,
'画布渲染尺寸': `${this.canvas.getWidth()}x${this.canvas.getHeight()}`,
'期望输出尺寸': `${this.originalSize.width}x${this.originalSize.height}`,
'设备像素比': window.devicePixelRatio,
'缩放级别': this.canvas.getZoom()
});
// 创建临时画布来处理尺寸
const tempCanvas = document.createElement('canvas');
tempCanvas.width = this.originalSize.width;
tempCanvas.height = this.originalSize.height;
const tempCtx = tempCanvas.getContext('2d');
const activeObjects = this.canvas.getActiveObjects();
const activeObject = this.canvas.getActiveObject();
// 主图像处理
this.canvas.discardActiveObject();
this.canvas.renderAll();
// 将高分辨率画布内容绘制到正确尺寸的临时画布
tempCtx.drawImage(
this.canvas.lowerCanvasEl,
0, 0, this.canvas.lowerCanvasEl.width, this.canvas.lowerCanvasEl.height, // 源尺寸(高分辨率)
0, 0, this.originalSize.width, this.originalSize.height // 目标尺寸
);
if (activeObject) {
this.canvas.setActiveObject(activeObject);
this.canvas.renderAll();
}
// 从临时画布获取正确尺寸的Blob
const imageBlob = await new Promise(resolve => {
tempCanvas.toBlob(async (blob) => {
const tempImg = await createImageBitmap(blob);
console.log('[FastCanvas] 处理后的图像尺寸:', {
'宽度': tempImg.width,
'高度': tempImg.height,
'Blob大小': blob.size
});
resolve(blob);
}, 'image/png', 1.0);
});
// 将Blob转换为ArrayBuffer
const imageBuffer = await imageBlob.arrayBuffer();
// 遮罩处理
const maskCanvas = document.createElement('canvas');
maskCanvas.width = this.originalSize.width;
maskCanvas.height = this.originalSize.height;
const ctx = maskCanvas.getContext('2d');
// 处理选中对象的遮罩
if (activeObjects && activeObjects.length > 1) {
// 设置混合模式为"lighter",确保重叠的白色区域保持白色
ctx.globalCompositeOperation = 'lighter';
// 按照画布中的顺序获取选中的图层
const allLayers = this.canvas.getObjects().filter(obj => obj !== this.canvas.backgroundImage);
const orderedSelectedObjects = allLayers.filter(obj => activeObjects.includes(obj));
// 按顺序处理每个选中的对象
for (const obj of orderedSelectedObjects) {
// 获取带遮挡关系的遮罩(注意这里传入 false 表示不忽略遮挡)
const layerMask = await this.getLayerMask(obj, false);
if (layerMask) {
ctx.drawImage(layerMask, 0, 0);
}
}
} else if (activeObject) {
const layerMask = await this.getLayerMask(activeObject, false);
if (layerMask) {
ctx.drawImage(layerMask, 0, 0);
}
} else {
ctx.clearRect(0, 0, maskCanvas.width, maskCanvas.height);
}
// 主遮罩转换为二进制
const maskBlob = await new Promise(resolve => {
maskCanvas.toBlob(resolve, 'image/png', 1.0);
});
const maskBuffer = await maskBlob.arrayBuffer();
// 收集变换信息(使用之前获取的activeObjects)
const layerTransforms = {};
if (activeObjects && activeObjects.length > 0) {
activeObjects.forEach(obj => {
if (obj.id) {
// 获取对象的中心点坐标
const centerPoint = obj.getCenterPoint();
layerTransforms[obj.id] = {
centerX: centerPoint.x, // 使用中心点 X 坐标
centerY: centerPoint.y, // 使用中心点 Y 坐标
scaleX: obj.scaleX,
scaleY: obj.scaleY,
angle: obj.angle,
width: obj.width,
height: obj.height,
flipX: obj.flipX,
flipY: obj.flipY
};
}
});
}
// 构建发送的数据对象
const data = {
node_id: this.node.id.toString(),
timestamp: timestamp.toString(),
type: 'temp',
subfolder: 'fast_canvas',
overwrite: 'true',
main_image: Array.from(new Uint8Array(imageBuffer)),
main_mask: Array.from(new Uint8Array(maskBuffer)),
layer_transforms: layerTransforms // 替换原来的 layer_masks
};
// 更新调试信息
console.log('[FastCanvas] 准备发送数据到后端:', {
节点ID: data.node_id,
时间戳: data.timestamp,
主图大小: data.main_image.length,
主遮罩大小: data.main_mask.length,
变换信息数量: Object.keys(data.layer_transforms).length
});
// 发送数据
const response = await api.fetchApi('/fast_canvas_all', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
});
console.log('[FastCanvas] 数据发送成功,服务器响应:', response);
await new Promise(resolve => setTimeout(resolve, 200));
} catch (error) {
console.error('[FastCanvas] 更新画布状态失败:', error);
throw error;
}
}
reset() {
// 保存当前的尺寸设置
const currentOriginalSize = { ...this.originalSize };
const currentMaxDisplaySize = this.maxDisplaySize;
// 清理现有画布
if (this.canvas) {
this.canvas.clear();
this.canvas.dispose();
}
// 清理容器
if (this.canvasContainer) {
while (this.canvasContainer.firstChild) {
this.canvasContainer.removeChild(this.canvasContainer.firstChild);
}
}
// 保持原始尺寸不变
this.originalSize = currentOriginalSize;
// 使用当前尺寸重新计算显示尺寸
const scaledSize = this.calculateScaledSize(
currentOriginalSize.width,
currentOriginalSize.height,
currentMaxDisplaySize
);
const STORAGE_KEY = 'canvas_size_';
localStorage.setItem(STORAGE_KEY + this.node.id, JSON.stringify(this.originalSize));
// 重新初始化画布
this.initCanvas();
// 清理节点的 DOM 元素
if (this.node.canvasElement) {
while (this.node.canvasElement.firstChild) {
this.node.canvasElement.removeChild(this.node.canvasElement.firstChild);
}
// 使用计算后的尺寸设置节点的 DOM 元素样式
this.node.canvasElement.style.width = `${scaledSize.width}px`;
this.node.canvasElement.style.height = `${scaledSize.height + CANVAS_SIZE.CONTROL_PANEL_HEIGHT}px`;
this.node.canvasElement.appendChild(this.canvasContainer);
}
// 使用计算后的尺寸设置节点尺寸
this.node.size = [
scaledSize.width + CANVAS_SIZE.RIGHT_MARGIN,
scaledSize.height + CANVAS_SIZE.BOTTOM_MARGIN + CANVAS_SIZE.CONTROL_PANEL_HEIGHT
];
// 重置其他状态
this.layers.clear();
this.background = null;
this.isUpdating = false;
this.isDragging = false;
this.pendingUpdate = false;
this.lastCanvasState = null;
// 强制更新节点
this.node.setDirtyCanvas(true, true);
return this;
}
// 添加清理方法
cleanup() {
// 移除 WebSocket 监听器
api.removeEventListener("fast_canvas_update", this._handleCanvasUpdate);
// 移除事件监听器
if (this.canvas) {
this.canvas.off(); // 移除所有 fabric.js 事件监听器
this.canvas.dispose(); // 销毁 fabric.js 实例
}
// 清理 DOM 元素
if (this.canvasContainer && this.canvasContainer.parentNode) {
this.canvasContainer.parentNode.removeChild(this.canvasContainer);
}
if (this._pasteHandler) {
this.canvasContainer.removeEventListener('paste', this._pasteHandler);
}
// 清理状态
this.layers.clear();
this.background = null;
this.lastCanvasState = null;
// 移除引用
this.canvas = null;
this.canvasContainer = null;
}
// 序列化方法
serialize() {
if (!this.canvas) return null;
// 获取所有对象的状态
const objects = this.canvas.getObjects().map(obj => {
const state = {
id: obj.id,
type: obj.type,
left: obj.left,
top: obj.top,
scaleX: obj.scaleX,
scaleY: obj.scaleY,
angle: obj.angle,
flipX: obj.flipX,
flipY: obj.flipY,
visible: obj.visible,
selectable: obj.selectable,
hasControls: obj.hasControls,
hasBorders: obj.hasBorders
};
// 如果是图片对象,添加图片数据
if (obj.type === 'image') {
state.image = obj.getSrc();
}
return state;
});
// 获取背景状态
const background = this.canvas.backgroundImage;
const backgroundState = background ? {
type: background.type,
fill: background.fill,
width: background.width,
height: background.height,
// 如果是图片类型,保存图片数据
image: background.type === 'image' ? background.getSrc() : null
} : null;
// 返回序列化数据
return {
originalSize: this.originalSize,
maxDisplaySize: this.maxDisplaySize,
objects: objects,
background: backgroundState
};
}
// 反序列化方法
async deserialize(data) {
if (!data || !this.canvas) return;
// 恢复原始尺寸
this.originalSize = data.originalSize;
this.maxDisplaySize = data.maxDisplaySize;
// 清空画布
this.canvas.clear();
// 恢复背景
if (data.background) {
if (data.background.type === 'rect') {
const bg = new fabric.Rect({
width: data.background.width,
height: data.background.height,
fill: data.background.fill,
selectable: false,
evented: false,
excludeFromExport: false
});
this.canvas.setBackgroundImage(bg);
this.background = bg;
} else if (data.background.type === 'image' && data.background.image) {
// 处理图片类型的背景
try {
const img = await this.loadImage(data.background.image);
img.set({
width: data.background.width,
height: data.background.height,
selectable: false,
evented: false,
excludeFromExport: false
});
this.canvas.setBackgroundImage(img);
this.background = img;
} catch (error) {
console.error("[FastCanvas] 背景图片加载失败:", error);
}
}
}
// 恢复其他对象
if (data.objects) {
for (const objData of data.objects) {
if (objData.type === 'image' && objData.image) {
try {
const img = await this.loadImage(objData.image);
img.set(objData);
this.canvas.add(img);
} catch (error) {
console.error("[FastCanvas] 图层图片加载失败:", error);
}
}
}
}
// 更新画布尺寸和缩放
const scaledSize = this.calculateScaledSize(
this.originalSize.width,
this.originalSize.height,
this.maxDisplaySize
);
this.updateCanvasSize(scaledSize.width, scaledSize.height);
}
}
// 控制面板类
class ControlPanel {
constructor(canvas, node) {
this.canvas = canvas; // fabric.js canvas 实例
this.node = node; // ComfyUI 节点实例
this.container = this.createContainer();
this.currentLayerId = null; // 当前选中的图层ID
this.maxLayerId = 0; // 最大图层ID
this.initializeControls();
}
// 创建控制面板容器
createContainer() {
const container = document.createElement('div');
container.className = 'fast-canvas-control-panel';
container.style.cssText = `
position: absolute;
bottom: -5px; // 将bottom设为负值,使控制面板向下移动
left: 0;
width: 100%;
height: ${CANVAS_SIZE.CONTROL_PANEL_HEIGHT}px;
background-color: #353535;
padding: 10px;
display: flex;
align-items: center;
gap: 10px;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
box-sizing: border-box;
`;
return container;
}
// 创建按钮
createButton(text, onClick) {
const button = document.createElement('button');
button.textContent = text;
button.style.cssText = `
padding: 5px 10px;
height: 28px;
line-height: 18px;
background: #444;
border: 1px solid #666;
color: #fff;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
font-size: 14px;
`;
button.onclick = onClick;
return button;
}
// 创建数字输入框
createNumberInput(placeholder, defaultValue, onChange) {
const input = document.createElement('input');
input.type = 'number';
input.placeholder = placeholder;
input.style.cssText = `
padding: 5px 10px;
height: 28px;
line-height: 18px;
background: #333;
border: 1px solid #666;
color: #fff;
border-radius: 4px;
width: 80px;
box-sizing: border-box;
display: flex;
align-items: center;
font-size: 14px;
text-align: center;
`;
const style = document.createElement('style');
style.textContent = `
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}
input[type="number"]::placeholder {
color: #888;
opacity: 1;
text-align: center;
}
`;
document.head.appendChild(style);
input.dataset.defaultValue = defaultValue;
input.onchange = (e) => {
const value = parseInt(e.target.value);
if (value > 0) {
onChange(e);
} else {
e.target.value = input.dataset.defaultValue;
onChange({ target: { value: input.dataset.defaultValue } });
}
};
return input;
}
// 创建图层选择器
createLayerSelector() {
const container = document.createElement('div');
container.className = 'layer-selector';
container.style.cssText = `
position: relative;
display: inline-block;
`;
const button = document.createElement('button');
button.className = 'layer-selector-btn';
button.style.cssText = `
padding: 5px 8px;
height: 28px;
background: #444;
border: 1px solid #666;
color: #fff;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
min-width: 80px;
justify-content: space-between;
font-size: 14px;
`;
const buttonText = document.createElement('span');
buttonText.textContent = '图层';
button.appendChild(buttonText);
const arrow = document.createElement('span');
arrow.textContent = '▼';
arrow.style.marginLeft = '5px';
button.appendChild(arrow);
const dropdown = document.createElement('div');
dropdown.className = 'layer-dropdown';
dropdown.style.cssText = `
position: absolute;
top: 100%;
left: 0;
background: #2a2a2a;
border: 1px solid #666;
border-radius: 4px;
display: none;
z-index: 1000;
min-width: 100%;
max-height: 200px;
overflow-y: auto;
`;
// 点击按钮显示/隐藏下拉菜单
button.onclick = (e) => {
e.stopPropagation();
dropdown.style.display = dropdown.style.display === 'none' ? 'block' : 'none';
};
// 点击其他地方关闭下拉菜单
document.addEventListener('click', () => {
dropdown.style.display = 'none';
});
container.appendChild(button);
container.appendChild(dropdown);
// 保存引用以便更新
this.layerButton = button;
this.layerButtonText = buttonText;
this.layerDropdown = dropdown;
return container;
}
// 更新图层选择器
updateLayerSelector() {
const dropdown = this.layerDropdown;
dropdown.innerHTML = '';
// 获取所有图层,保持原有顺序
const layers = Array.from(this.canvas.getObjects())
.filter(obj => obj !== this.canvas.backgroundImage && obj.id);
// 获取当前选中的图层
const activeObject = this.canvas.getActiveObject();
if (activeObject && activeObject !== this.canvas.backgroundImage) {
this.currentLayerId = activeObject.id;
}
// 更新按钮文本
this.layerButtonText.textContent = this.currentLayerId ?
`图层 ${this.currentLayerId}` : '图层';
// 创建下拉选项
layers.forEach(layer => {
if (layer.id !== this.currentLayerId) {
const option = document.createElement('div');
option.className = 'layer-option';
option.textContent = `图层 ${layer.id}`;
option.style.cssText = `
padding: 5px 10px;
cursor: pointer;
white-space: nowrap;
&:hover {
background: #444;
}
`;
option.onclick = (e) => {
e.stopPropagation();
// 只选中图层,不改变叠加顺序
this.canvas.setActiveObject(layer);
this.currentLayerId = layer.id;
this.updateLayerSelector();
// 触发画布更新,但不改变图层顺序
this.canvas.requestRenderAll();
this.layerDropdown.style.display = 'none';
};
dropdown.appendChild(option);
}
});
}
// 创建设置弹窗
createSettingsDialog() {
const dialog = document.createElement('dialog');
dialog.style.cssText = `
background: #353535;
border: 1px solid #666;
border-radius: 4px;
color: #fff;
padding: 20px;
min-width: 300px;
`;
const title = document.createElement('h3');
title.textContent = '设置';
title.style.cssText = `
margin: 0 0 20px 0;
font-size: 16px;
`;
// 创建设置项容器
const settingsContainer = document.createElement('div');
settingsContainer.style.cssText = `
display: flex;
flex-direction: column;
gap: 15px;
`;
// 窗口最大尺寸设置项
const maxSizeInput = this.createNumberInput('最大尺寸', 1024, (e) => {
const maxSize = parseInt(e.target.value);
if (maxSize > 0) {
this.node.canvasInstance.updateDisplayScale(maxSize);
}
});
const maxSizeItem = this.createSettingItem(
'窗口最大尺寸:',
'限制画布显示尺寸',
maxSizeInput
);
const buttonContainer = document.createElement('div');
buttonContainer.style.cssText = `
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 20px;
`;
const saveButton = this.createButton('保存', () => {
dialog.close();
});
const cancelButton = this.createButton('取消', () => {
dialog.close();
});
settingsContainer.appendChild(maxSizeItem);
buttonContainer.appendChild(cancelButton);
buttonContainer.appendChild(saveButton);
dialog.appendChild(title);
dialog.appendChild(settingsContainer);
dialog.appendChild(buttonContainer);
return dialog;
}
// 创建单个设置项
createSettingItem(labelText, tooltipText, inputElement) {
const container = document.createElement('div');
container.style.cssText = `
display: flex;
align-items: center;
gap: 10px;
position: relative;
`;
const label = document.createElement('label');
label.style.cssText = `
flex: 1;
font-size: 14px;
display: flex;
align-items: center;
gap: 5px;
`;
label.textContent = labelText;
// 添加问号图标
const helpIcon = document.createElement('span');
helpIcon.textContent = '?';
helpIcon.style.cssText = `
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
border-radius: 50%;
background: #666;
font-size: 12px;
cursor: help;
`;
// 创建tooltip
const tooltip = document.createElement('div');
tooltip.textContent = tooltipText;
tooltip.style.cssText = `
position: absolute;
background: #1a1a1a;
border: 1px solid #666;
border-radius: 4px;
padding: 8px 12px;
font-size: 12px;
color: #fff;
width: max-content;
max-width: 200px;
left: 0;
top: -45px;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
z-index: 1000;
line-height: 1.4;
pointer-events: none;
`;
// 添加悬停事件
helpIcon.addEventListener('mouseenter', () => {
tooltip.style.opacity = '1';
tooltip.style.visibility = 'visible';
});
helpIcon.addEventListener('mouseleave', () => {
tooltip.style.opacity = '0';
tooltip.style.visibility = 'hidden';
});
label.appendChild(helpIcon);
container.appendChild(label);
container.appendChild(inputElement);
container.appendChild(tooltip);
return container;
}
initializeControls() {
const layerSelector = this.createLayerSelector();
this.container.appendChild(layerSelector);
const loadButton = this.createButton('加载', () => {
if (this.node.id) {
rgthree.queueOutputNodes([this.node.id]);
}
});
const resetButton = this.createButton('重置', () => {
if (this.node.id) {
// 保存当前节点的状态
const currentNode = this.node;
const currentSize = [...currentNode.size];
const currentPos = [...currentNode.pos];
const currentOriginalSize = {...currentNode.canvasInstance.originalSize};
const currentMaxDisplaySize = currentNode.canvasInstance.maxDisplaySize;
// 获取当前节点的存储键和尺寸
const STORAGE_KEY = 'canvas_size_';
const savedSize = localStorage.getItem(STORAGE_KEY + currentNode.id);
// 创建新节点
let new_node = LiteGraph.createNode("FastCanvas");
new_node.pos = currentPos;
app.graph.add(new_node, false);
requestAnimationFrame(() => {
// 复制节点信息(包括连接)
node_info_copy(currentNode, new_node, true);
if (new_node.canvasInstance) {
// 设置画布实例的属性
new_node.canvasInstance.originalSize = currentOriginalSize;
new_node.canvasInstance.maxDisplaySize = currentMaxDisplaySize;
// 保持原有尺寸
new_node.size = currentSize;
// 如果有保存的尺寸,转移到新节点
if (savedSize) {
localStorage.setItem(STORAGE_KEY + new_node.id, savedSize);
// 删除旧节点的存储
localStorage.removeItem(STORAGE_KEY + currentNode.id);
}
// 设置画布容器尺寸
if (new_node.canvasElement) {
const width = currentOriginalSize.width;
const height = currentOriginalSize.height;
// 计算缩放后的尺寸
const scaledSize = new_node.canvasInstance.calculateScaledSize(
width,
height,
currentMaxDisplaySize
);
// 更新画布容器尺寸
new_node.canvasElement.style.width = `${scaledSize.width}px`;
new_node.canvasElement.style.height = `${scaledSize.height + CANVAS_SIZE.CONTROL_PANEL_HEIGHT}px`;
// 更新画布尺寸
if (new_node.canvasInstance.canvas) {
new_node.canvasInstance.updateCanvasSize(
scaledSize.width,
scaledSize.height
);
}
// 更新尺寸输入框显示
if (new_node.sizeOverlay?.updateSizeInputs) {
new_node.sizeOverlay.updateSizeInputs(width, height);
}
}
}
// 更新画布
app.graph.setDirtyCanvas(true, true);
// 移除旧节点
app.graph.remove(currentNode);
});
}
});
const settingsButton = this.createButton('⚙️', () => {
const dialog = this.createSettingsDialog();
document.body.appendChild(dialog);
dialog.showModal();
dialog.addEventListener('close', () => {
document.body.removeChild(dialog);
});
});
settingsButton.style.padding = '5px 8px';
// 添加控件到容器
this.container.appendChild(loadButton);
this.container.appendChild(resetButton);
this.container.appendChild(settingsButton);
// 监听画布选择变化
this.canvas.on('selection:created', (e) => {
const obj = e.selected[0];
if (obj && obj !== this.canvas.backgroundImage) {
this.currentLayerId = obj.id;
this.updateLayerSelector();
}
});
this.canvas.on('selection:updated', (e) => {
const obj = e.selected[0];
if (obj && obj !== this.canvas.backgroundImage) {
this.currentLayerId = obj.id;
this.updateLayerSelector();
}
});
this.canvas.on('selection:cleared', () => {
this.currentLayerId = null;
this.updateLayerSelector();
});
}
// 获取控制面板容器
getContainer() {
return this.container;
}
// 更新控制面板尺寸
updateSize(width) {
this.container.style.width = `${width}px`;
}
// 更新图层ID
updateLayerIds() {
const objects = this.canvas.getObjects();
let maxId = this.maxLayerId;
objects.forEach(obj => {
if (obj !== this.canvas.backgroundImage) {
if (!obj.id) {
// 为新图层分配ID
maxId++;
obj.id = maxId;
}
}
});
this.maxLayerId = maxId;
// 更新当前选中的图层ID
const activeObject = this.canvas.getActiveObject();
if (activeObject && activeObject !== this.canvas.backgroundImage) {
this.currentLayerId = activeObject.id;
}
this.updateLayerSelector();
}
// 设置背景时移除图层ID
removeLayerId(obj) {
if (obj.id) {
delete obj.id;
if (this.currentLayerId === obj.id) {
this.currentLayerId = null;
}
this.updateLayerSelector();
}
}
}
// 创建覆盖层扩展
class FastCanvasOverlay {
static createOverlay(node) {
// 创建覆盖层容器
const overlayContainer = document.createElement("div");
overlayContainer.style.cssText = `
position: absolute;
left: 0;
top: -35px;
width: 100%;
height: ${CANVAS_SIZE.CONTROL_PANEL_HEIGHT}px;
background: #353535;
display: flex;
gap: 10px;
padding: 10px;
z-index: 10;
align-items: center;
box-sizing: border-box;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
`;
// 创建输入框样式
const inputStyle = `
padding: 5px 10px;
height: 28px;
line-height: 18px;
background: #333;
border: 1px solid #666;
color: #fff;
border-radius: 4px;
width: 60px;
box-sizing: border-box;
display: flex;
align-items: center;
font-size: 14px;
text-align: center;
`;
// 创建按钮样式
const buttonStyle = `
padding: 5px 10px;
height: 28px;
line-height: 18px;
background: #444;
border: 1px solid #666;
color: #fff;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
font-size: 14px;
`;
const STORAGE_KEY = 'canvas_size_';
// 从本地存储中获取保存的尺寸
const savedSize = localStorage.getItem(STORAGE_KEY + node.id);
const initialSize = savedSize ? JSON.parse(savedSize) : {
width: node.canvasInstance.originalSize.width,
height: node.canvasInstance.originalSize.height
};
// 创建宽度输入框时使用保存的值
const widthInput = document.createElement("input");
widthInput.type = "number";
widthInput.id = "canvas-width";
widthInput.placeholder = "宽度";
widthInput.value = initialSize.width; // 使用保存的宽度
widthInput.style.cssText = inputStyle;
// 创建高度输入框时使用保存的值
const heightInput = document.createElement("input");
heightInput.type = "number";
heightInput.id = "canvas-height";
heightInput.placeholder = "高度";
heightInput.value = initialSize.height; // 使用保存的高度
heightInput.style.cssText = inputStyle;
// 更新尺寸显示函数(显示实际尺寸)
const updateSizeInputs = (width, height) => {
widthInput.value = width;
heightInput.value = height;
};
// 创建更新按钮
const updateButton = document.createElement("button");
updateButton.textContent = "更新尺寸";
updateButton.style.cssText = buttonStyle;
updateButton.onclick = async () => {
if (!node.canvasInstance?.canvas) return;
let customWidth = parseInt(widthInput.value, 10);
let customHeight = parseInt(heightInput.value, 10);
if (isNaN(customWidth) && isNaN(customHeight)) {
console.warn('[FastCanvas] 无效的尺寸输入');
return;
}
// 获取当前背景尺寸
const currentBg = node.canvasInstance.canvas.backgroundImage;
const currentWidth = currentBg ? currentBg.width : node.canvasInstance.originalSize.width;
const currentHeight = currentBg ? currentBg.height : node.canvasInstance.originalSize.height;
// 处理按边等比例缩放
if (customWidth === 0 && customHeight > 0) {
// 按高度等比例计算宽度
const ratio = customHeight / currentHeight;
customWidth = Math.round(currentWidth * ratio);
} else if (customHeight === 0 && customWidth > 0) {
// 按宽度等比例计算高度
const ratio = customWidth / currentWidth;
customHeight = Math.round(currentHeight * ratio);
} else if (customWidth === 0 && customHeight === 0) {
console.warn('[FastCanvas] 宽度和高度不能同时为0');
return;
}
try {
// 1. 确保尺寸能被8整除
const adjustedWidth = Math.floor(customWidth / 8) * 8;
const adjustedHeight = Math.floor(customHeight / 8) * 8;
// 2. 更新原始尺寸
node.canvasInstance.originalSize = {
width: adjustedWidth,
height: adjustedHeight
};
// 3. 计算显示尺寸(应用限制窗口大小的功能)
const scaledSize = node.canvasInstance.calculateScaledSize(
adjustedWidth,
adjustedHeight,
node.canvasInstance.maxDisplaySize
);
// 4. 如果有现有背景,创建新的背景
const currentBg = node.canvasInstance.canvas.backgroundImage;
let newBackground;
if (currentBg) {
// 创建临时画布
const tempCanvas = document.createElement('canvas');
tempCanvas.width = adjustedWidth;
tempCanvas.height = adjustedHeight;
const ctx = tempCanvas.getContext('2d');
// 获取当前背景的原始尺寸
const origWidth = currentBg.width;
const origHeight = currentBg.height;
// 计算填充比例
const scaleX = adjustedWidth / origWidth;
const scaleY = adjustedHeight / origHeight;
const scale = Math.max(scaleX, scaleY); // 使用较大的缩放比例以填充
// 计算居中位置
const scaledWidth = origWidth * scale;
const scaledHeight = origHeight * scale;
const left = (adjustedWidth - scaledWidth) / 2;
const top = (adjustedHeight - scaledHeight) / 2;
// 先填充黑色背景
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, adjustedWidth, adjustedHeight);
// 绘制并居中裁剪背景
if (currentBg instanceof fabric.Image) {
// 如果是 Image 对象,使用 getElement()
ctx.drawImage(
currentBg.getElement(),
left, top, scaledWidth, scaledHeight
);
} else if (currentBg instanceof fabric.Rect) {
// 如果是 Rect 对象,创建一个新的临时画布来渲染矩形
const tempRectCanvas = document.createElement('canvas');
tempRectCanvas.width = origWidth;
tempRectCanvas.height = origHeight;
const tempCtx = tempRectCanvas.getContext('2d');
// 绘制矩形
tempCtx.fillStyle = currentBg.fill || '#000000';
tempCtx.fillRect(0, 0, origWidth, origHeight);
// 使用临时画布作为源
ctx.drawImage(
tempRectCanvas,
left, top, scaledWidth, scaledHeight
);
} else if (currentBg._element) {
// 如果有 _element 属性(某些 fabric 对象的情况)
ctx.drawImage(
currentBg._element,
left, top, scaledWidth, scaledHeight
);
} else {
// 如果都不是,填充黑色
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, adjustedWidth, adjustedHeight);
}
// 创建新的背景图像
newBackground = new fabric.Image(tempCanvas, {
width: adjustedWidth,
height: adjustedHeight,
left: 0,
top: 0,
selectable: false,
evented: false,
excludeFromExport: false,
originX: 'left',
originY: 'top'
});
} else {
// 如果没有背景,创建黑色矩形
newBackground = new fabric.Rect({
width: adjustedWidth,
height: adjustedHeight,
left: 0,
top: 0,
fill: '#000000',
selectable: false,
evented: false,
excludeFromExport: false
});
}
// 5. 设置新背景并等待完成
await new Promise(resolve => {
node.canvasInstance.canvas.setBackgroundImage(newBackground, resolve);
});
// 6. 更新背景引用
node.canvasInstance.background = newBackground;
// 7. 更新画布尺寸和缩放
node.canvasInstance.updateCanvasSize(scaledSize.width, scaledSize.height);
// 8. 更新输入框显示实际尺寸
widthInput.value = adjustedWidth;
heightInput.value = adjustedHeight;
localStorage.setItem(STORAGE_KEY + node.id, JSON.stringify({
width: adjustedWidth,
height: adjustedHeight
}));
} catch (error) {
console.error('[FastCanvas] 更新尺寸失败:', error);
}
};
// 创建颜色选择器
const colorInput = document.createElement("input");
colorInput.type = "color";
colorInput.value = "#000000";
colorInput.style.cssText = `
padding: 0;
width: 28px;
height: 28px;
background: #333;
border: 1px solid #666;
border-radius: 4px;
cursor: pointer;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
`;
colorInput.onchange = (e) => {
const newColor = e.target.value;
if (node.canvasInstance?.canvas) {
// 创建新的纯色矩形背景
const newBackground = new fabric.Rect({
width: node.canvasInstance.originalSize.width,
height: node.canvasInstance.originalSize.height,
left: 0,
top: 0,
fill: newColor,
selectable: false,
evented: false,
excludeFromExport: false,
originX: 'left',
originY: 'top'
});
// 设置新背景
node.canvasInstance.canvas.setBackgroundImage(newBackground);
node.canvasInstance.background = newBackground;
node.canvasInstance.canvas.renderAll();
}
};
// 统一的样式规则
if (!document.getElementById('fast-canvas-overlay-styles')) {
const style = document.createElement('style');
style.id = 'fast-canvas-overlay-styles'; // 添加ID避免重复
style.textContent = `
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type="number"] {
-moz-appearance: textfield;
}
input[type="number"]::placeholder {
color: #888;
opacity: 1;
text-align: center;
}
input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
input[type="color"]::-webkit-color-swatch {
border: none;
border-radius: 2px;
}
input[type="color"]::-moz-color-swatch {
border: none;
border-radius: 2px;
}
`;
document.head.appendChild(style);
}
// 创建文件输入框(隐藏)
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = 'image/*';
fileInput.style.display = 'none';
// 创建上传按钮
const uploadButton = document.createElement("button");
uploadButton.textContent = "上传图片";
uploadButton.style.cssText = buttonStyle; // 使用已有的按钮样式
// 处理点击上传按钮
uploadButton.onclick = () => {
fileInput.click();
};
// 处理文件选择
fileInput.onchange = async (e) => {
const file = e.target.files[0];
if (file && node.canvasInstance) {
await node.canvasInstance.handleImageUpload(file);
}
// 清除文件选择,允许选择相同文件
fileInput.value = '';
};
const lockButton = document.createElement("button");
lockButton.innerHTML = '🔓'; // 默认未锁定状态
lockButton.style.cssText = buttonStyle;
lockButton.style.marginLeft = 'auto'; // 将按钮推到最右边
lockButton.dataset.locked = 'false';
const updateLockStyle = (isLocked) => {
if (isLocked) {
lockButton.innerHTML = '🔒';
lockButton.style.background = '#663c3c';
overlayContainer.classList.add('canvas-locked');
// 禁用控制按钮和输入
colorInput.disabled = true;
widthInput.disabled = true;
heightInput.disabled = true;
updateButton.disabled = true;
uploadButton.disabled = true;
if (node.canvasInstance?.canvas) {
const canvas = node.canvasInstance.canvas;
// 创建透明覆盖层
if (!canvas.lockOverlay) {
const overlayDiv = document.createElement('div');
overlayDiv.style.cssText = `
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000;
background: transparent;
cursor: default;
`;
canvas.wrapperEl.appendChild(overlayDiv);
canvas.lockOverlay = overlayDiv;
}
canvas.selection = false;
canvas.skipTargetFind = true;
canvas.renderAll();
}
} else {
lockButton.innerHTML = '🔓';
lockButton.style.background = '#444';
overlayContainer.classList.remove('canvas-locked');
// 启用控制按钮和输入
colorInput.disabled = false;
widthInput.disabled = false;
heightInput.disabled = false;
updateButton.disabled = false;
uploadButton.disabled = false;
if (node.canvasInstance?.canvas) {
const canvas = node.canvasInstance.canvas;
// 移除覆盖层
if (canvas.lockOverlay) {
canvas.wrapperEl.removeChild(canvas.lockOverlay);
delete canvas.lockOverlay;
}
canvas.selection = true;
canvas.skipTargetFind = false;
canvas.renderAll();
}
}
};
// 添加点击事件
lockButton.onclick = () => {
const isLocked = lockButton.dataset.locked === 'true';
lockButton.dataset.locked = (!isLocked).toString();
updateLockStyle(!isLocked);
// 保存锁定状态到节点实例
if (node.canvasInstance) {
node.canvasInstance.isLocked = !isLocked;
}
};
// 添加禁用状态的样式
const additionalStyles = `
.canvas-locked button:disabled,
.canvas-locked input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.canvas-locked input[type="color"]:disabled {
pointer-events: none;
}
`;
// 添加样式到已有的样式表中
if (document.getElementById('fast-canvas-overlay-styles')) {
const styleElement = document.getElementById('fast-canvas-overlay-styles');
styleElement.textContent += additionalStyles;
}
// 将元素添加到容器
overlayContainer.appendChild(colorInput);
overlayContainer.appendChild(widthInput);
overlayContainer.appendChild(heightInput);
overlayContainer.appendChild(updateButton);
overlayContainer.appendChild(uploadButton);
overlayContainer.appendChild(fileInput);
overlayContainer.appendChild(lockButton);
// 保存更新函数引用
overlayContainer.updateSizeInputs = updateSizeInputs;
if (node.canvasInstance?.isLocked) {
lockButton.dataset.locked = 'false';
updateLockStyle(false);
}
return overlayContainer;
}
}
function node_info_copy(src, dest, connect_both) {
// copy input connections
for(let i in src.inputs) {
let input = src.inputs[i];
if (input.widget !== undefined) {
const destWidget = dest.widgets.find(x => x.name === input.widget.name);
dest.convertWidgetToInput(destWidget);
}
if(input.link) {
let link = app.graph.links[input.link];
let src_node = app.graph.getNodeById(link.origin_id);
src_node.connect(link.origin_slot, dest.id, input.name);
}
}
// copy output connections
if(connect_both) {
let output_links = {};
for(let i in src.outputs) {
let output = src.outputs[i];
if(output.links) {
let links = [];
for(let j in output.links) {
links.push(app.graph.links[output.links[j]]);
}
output_links[output.name] = links;
}
}
for(let i in dest.outputs) {
let links = output_links[dest.outputs[i].name];
if(links) {
for(let j in links) {
let link = links[j];
let target_node = app.graph.getNodeById(link.target_id);
dest.connect(parseInt(i), target_node, link.target_slot);
}
}
}
}
dest.color = src.color;
dest.bgcolor = src.bgcolor;
dest.size = src.size;
app.graph.afterChange();
}
// 修改节点执行处理
app.registerExtension({
name: "Custom.FastCanvas",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name === "FastCanvas") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
const result = onNodeCreated?.apply(this, arguments);
// 添加随机种子输入框
this.addWidget(
"text",
"seed",
"initial_seed", // 给一个初始值
(v) => {
this.seed = v;
},
{
multiline: false,
readonly: true
}
);
// 生成随机种子的方法
this.generateSeed = () => {
const timestamp = Date.now();
const random = Math.floor(Math.random() * 1000000);
return `${timestamp}_${random}`;
};
// 更新种子值的方法
this.updateSeed = () => {
const newSeed = this.generateSeed();
const seedWidget = this.widgets?.find(w => w.name === "seed");
if (seedWidget) {
seedWidget.value = newSeed;
this.seed = newSeed; // 确保也更新节点的属性
if (!this.widgets_values) {
this.widgets_values = [];
}
const widgetIndex = this.widgets.findIndex(w => w.name === "seed");
if (widgetIndex !== -1) {
this.widgets_values[widgetIndex] = newSeed;
}
this.setDirtyCanvas(true, false);
}
};
return result;
};
nodeType.prototype.onAdded = function() {
if (this.id !== undefined && this.id !== -1) {
// ... 其他初始化代码 ...
// 从本地存储获取保存的尺寸
const STORAGE_KEY = 'canvas_size_';
const savedSize = localStorage.getItem(STORAGE_KEY + this.id);
const size = savedSize ? JSON.parse(savedSize) : {
width: CANVAS_SIZE.WIDTH,
height: CANVAS_SIZE.HEIGHT
};
// 创建画布容器
const element = document.createElement("div");
element.style.position = "relative";
element.style.width = "100%";
element.style.height = "100%";
// 存储 element 引用
this.canvasElement = element;
// 先创建画布实例
this.canvasInstance = new FastCanvas(this, size);
// 计算缩放后的尺寸
const scaledSize = this.canvasInstance.calculateScaledSize(
size.width,
size.height,
this.canvasInstance.maxDisplaySize
);
// 更新容器尺寸
element.style.minWidth = `${scaledSize.width}px`;
element.style.minHeight = `${scaledSize.height + CANVAS_SIZE.CONTROL_PANEL_HEIGHT}px`;
// 更新画布尺寸
this.canvasInstance.updateCanvasSize(scaledSize.width, scaledSize.height);
// 更新节点尺寸计算方法
this.computeSize = () => [
scaledSize.width + CANVAS_SIZE.RIGHT_MARGIN,
scaledSize.height + CANVAS_SIZE.BOTTOM_MARGIN + CANVAS_SIZE.CONTROL_PANEL_HEIGHT
];
// 创建覆盖层
const overlay = FastCanvasOverlay.createOverlay(this);
element.appendChild(overlay);
this.sizeOverlay = overlay;
// 添加画布组件
this.canvasWidget = this.addDOMWidget("canvas", "canvas", element);
// 添加画布到容器
this.canvasElement.appendChild(this.canvasInstance.canvasContainer);
// 强制更新节点尺寸
requestAnimationFrame(() => {
this.size = this.computeSize();
this.setDirtyCanvas(true, true);
// 如果有保存的尺寸,立即更新输入框显示
if (this.sizeOverlay?.updateSizeInputs) {
this.sizeOverlay.updateSizeInputs(size.width, size.height);
}
});
}
};
nodeType.prototype.onConfigure = function() {
// 防止重复配置
if (this._configuring) {
return;
}
// 检查是否是页面加载时的自动重置
if (document.readyState !== "complete") {
return;
}
// 检查是否需要重置
if (!this.properties?.needsReset) {
return;
}
try {
this._configuring = true;
let new_node = LiteGraph.createNode(nodeType.comfyClass);
if (!new_node) {
return;
}
// 设置新节点位置
new_node.pos = [this.pos[0], this.pos[1]];
// 添加到图表中,保持原有的 false 参数
app.graph.add(new_node, false);
// 复制节点信息,保持原有的调用方式
node_info_copy(this, new_node, true);
// 移除旧节点,使用 requestAnimationFrame 确保正确的执行顺序
requestAnimationFrame(() => {
app.graph.remove(this);
app.graph.setDirtyCanvas(true, true);
});
} catch (error) {
} finally {
this._configuring = false;
// 重置完成后清除标记
if (this.properties) {
this.properties.needsReset = false;
}
}
};
// 添加标记重置的方法
nodeType.prototype.markForReset = function() {
if (!this.properties) {
this.properties = {};
}
this.properties.needsReset = true;
};
// 节点移除
nodeType.prototype.onRemoved = function() {
// 首先检查 this 是否有效
if (!this) return;
// 然后检查 canvasInstance 是否存在
if (this.canvasInstance) {
// 清理状态
if (this.widgets) {
const stateWidget = this.widgets.find(w => w.name === "state");
if (stateWidget) {
stateWidget.value = "0";
}
}
this.canvasInstance.cleanup();
}
};
nodeType.prototype.serialize = function() {
const data = LiteGraph.LGraphNode.prototype.serialize.call(this);
// 添加画布数据
if (this.canvasInstance) {
data.canvasData = this.canvasInstance.serialize();
}
return data;
};
nodeType.prototype.configure = function(data) {
LiteGraph.LGraphNode.prototype.configure.call(this, data);
// 恢复画布数据
if (data.canvasData && this.canvasInstance) {
this.canvasInstance.deserialize(data.canvasData);
}
};
}
}
});
app.registerExtension({
name: "Custom.FastCanvasTool",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData.name === "FastCanvasTool") {
// 处理连接变化
nodeType.prototype.onConnectionsChange = async function(type, index, connected, link_info) {
if (!link_info || type == LiteGraph.OUTPUT) return;
const stackTrace = new Error().stack;
// 如果是背景图片端口,不进行删除操作
if (this.inputs[index]?.name === "bg_img") {
return;
}
// 处理断开连接
if (!connected) {
if (!stackTrace.includes('LGraphNode.prototype.connect') &&
!stackTrace.includes('LGraphNode.connect') &&
!stackTrace.includes('loadGraphData')) {
// 只删除非背景图片的输入端口
if (this.inputs[index]?.name.startsWith("img_")) {
this.removeInput(index);
}
}
}
// 处理新连接
if (connected) {
await app.queuePromise;
}
// 重新编号图层输入(跳过背景图片端口)
let imgIndex = 1;
this.inputs.forEach(input => {
if (input.name.startsWith("img_")) {
const newName = `img_${imgIndex}`;
if (input.name !== newName) {
input.name = newName;
}
imgIndex++;
}
});
// 只有在最后一个非背景端口被连接时添加新端口
const nonBgInputs = this.inputs.filter(input => input.name.startsWith("img_"));
const lastInput = nonBgInputs[nonBgInputs.length - 1];
if (lastInput?.link != null) {
const newIndex = `img_${imgIndex}`;
this.addInput(newIndex, "IMAGE");
}
this.setDirtyCanvas(true, true);
};
// 添加初始化方法
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function() {
if (onNodeCreated) {
onNodeCreated.apply(this, arguments);
}
// 确保始终有一个背景图片端口和一个图层端口
if (!this.inputs.find(input => input.name === "bg_img")) {
this.addInput("bg_img", "IMAGE");
}
if (!this.inputs.find(input => input.name === "img_1")) {
this.addInput("img_1", "IMAGE");
}
};
}
}
});
function getFileItem(baseType, path) {
try {
let pathType = baseType;
if (path.endsWith("[output]")) {
pathType = "output";
path = path.slice(0, -9);
} else if (path.endsWith("[input]")) {
pathType = "input";
path = path.slice(0, -8);
} else if (path.endsWith("[temp]")) {
pathType = "temp";
path = path.slice(0, -7);
}
const subfolder = path.substring(0, path.lastIndexOf('/'));
const filename = path.substring(path.lastIndexOf('/') + 1);
return {
filename: filename,
subfolder: subfolder,
type: pathType
};
}
catch(exception) {
return null;
}
}
async function loadImageFromUrl(image, node_id, v, need_to_load) {
let item = getFileItem('temp', v);
if(item) {
let params = `?node_id=${node_id}&filename=${item.filename}&type=${item.type}&subfolder=${item.subfolder}`;
let res = await api.fetchApi('/impact/set/pb_id_image'+params, { cache: "no-store" });
if(res.status == 200) {
let pb_id = await res.text();
if(need_to_load) {;
image.src = api.apiURL(`/view?filename=${item.filename}&type=${item.type}&subfolder=${item.subfolder}`);
}
return pb_id;
}
else {
return `$${node_id}-0`;
}
}
else {
return `$${node_id}-0`;
}
}
async function loadImageFromId(image, v) {
let res = await api.fetchApi('/impact/get/pb_id_image?id='+v, { cache: "no-store" });
if(res.status == 200) {
let item = await res.json();
image.src = api.apiURL(`/view?filename=${item.filename}&type=${item.type}&subfolder=${item.subfolder}`);
return true;
}
return false;
}
app.registerExtension({
name: "Comfy.LG.CachePreview",
nodeCreated(node, app) {
if (node.comfyClass !== "CachePreviewBridge") return;
let imageWidget = node.widgets.find(w => w.name === 'image');
node._imgs = [new Image()];
node.imageIndex = 0;
// 添加粘贴功能
node.pasteFile = async function(file) {
const uploadWidget = node.widgets.find(w => w.name === 'image');
if (!uploadWidget) return;
// 上传到 clipspace 目录
const fileName = `clipspace_${Date.now()}.png`;
const formData = new FormData();
formData.append('image', file, fileName);
formData.append('type', 'input');
formData.append('subfolder', 'clipspace');
try {
const resp = await fetch('/upload/image', {
method: 'POST',
body: formData,
});
if (resp.ok) {
const data = await resp.json();
if (data.name) {
// 创建一个临时的 item 对象
const item = {
filename: data.name,
type: 'input',
subfolder: 'clipspace'
};
// 使用 set/pb_id_image 创建映射
const params = `?node_id=${node.id}&filename=${item.filename}&type=${item.type}&subfolder=${item.subfolder}`;
const res = await api.fetchApi('/impact/set/pb_id_image'+params, { cache: "no-store" });
if (res.status == 200) {
const pb_id = await res.text();
imageWidget._lock = true;
imageWidget._value = pb_id;
imageWidget._lock = false;
// 加载图像
const image = new Image();
if (await loadImageFromId(image, pb_id)) {
node._imgs = [image];
app.graph.setDirtyCanvas(true);
}
}
}
}
} catch (error) {
console.error("Failed to upload file:", error);
}
};
// 简化后的 value 属性重写
Object.defineProperty(imageWidget, 'value', {
async set(v) {
if (imageWidget._lock) return;
const stackTrace = new Error().stack;
if (stackTrace.includes('presetText.js')) return;
var image = new Image();
// 处理节点反馈的图像
if (v && v.constructor === String && v.startsWith('$')) {
let need_to_load = node._imgs[0].src === '';
if (await loadImageFromId(image, v, need_to_load)) {
imageWidget._value = v;
if (node._imgs[0].src === '') {
node._imgs = [image];
}
} else {
imageWidget._value = `$${node.id}-0`;
}
}
// 处理从剪贴板导入的图像
else {
imageWidget._lock = true;
imageWidget._value = await loadImageFromUrl(image, node.id, v, false);
imageWidget._lock = false;
}
},
get() {
if (imageWidget._value === undefined) {
imageWidget._value = `$${node.id}-0`;
}
return imageWidget._value;
}
});
Object.defineProperty(node, 'imgs', {
set(v) {
const stackTrace = new Error().stack;
if (!v || v.length === 0) return;
if (stackTrace.includes('pasteFromClipspace')) {
// 检查是否来自ComfyUI系统的clipspace
if (v[0].src && v[0].src.includes('/api/view')) {
(async () => {
try {
// 从原始图像创建 blob
const response = await fetch(v[0].src);
const blob = await response.blob();
// 使用相同的命名方式
const fileName = `clipspace_${Date.now()}.png`;
const formData = new FormData();
formData.append('image', blob, fileName);
formData.append('type', 'input');
formData.append('subfolder', 'clipspace');
// 上传到 clipspace 目录
const resp = await fetch('/upload/image', {
method: 'POST',
body: formData,
});
if (resp.ok) {
const data = await resp.json();
if (data.name) {
// 创建临时 item 对象
const item = {
filename: data.name,
type: 'input',
subfolder: 'clipspace'
};
// 创建映射
const params = `?node_id=${node.id}&filename=${item.filename}&type=${item.type}&subfolder=${item.subfolder}`;
const res = await api.fetchApi('/impact/set/pb_id_image'+params, { cache: "no-store" });
if (res.status == 200) {
const pb_id = await res.text();
imageWidget._lock = true;
imageWidget._value = pb_id;
imageWidget._lock = false;
// 加载新图像
const image = new Image();
if (await loadImageFromId(image, pb_id)) {
node._imgs = [image];
app.graph.setDirtyCanvas(true);
}
}
}
}
} catch (error) {
console.error("Failed to process clipspace paste:", error);
}
})();
} else {
// 原有的处理逻辑
let sp = new URLSearchParams(v[0].src.split("?")[1]);
let str = "";
if (sp.get('subfolder')) {
str += sp.get('subfolder') + '/';
}
str += `${sp.get("filename")} [${sp.get("type")}]`;
imageWidget.value = str;
node._imgs = v;
}
return;
}
node._imgs = v;
},
get() {
return node._imgs;
}
});
}
});