1611 lines
48 KiB
JavaScript
1611 lines
48 KiB
JavaScript
import { app } from "../../../scripts/app.js";
|
|
import { fabric } from "./fabric.js";
|
|
import { api } from "../../../scripts/api.js";
|
|
import {getDrawColor, hexToRgba} from "./utils.js"
|
|
import {PainterWidget,createMessage,loadData,LS_PSave} from "./painter_node.js"
|
|
|
|
const painters_settings_json = false;
|
|
|
|
fabric.Object.prototype.transparentCorners = false;
|
|
fabric.Object.prototype.cornerColor = "#108ce6";
|
|
fabric.Object.prototype.borderColor = "#108ce6";
|
|
fabric.Object.prototype.cornerSize = 10;
|
|
|
|
const connect_keypoints = [
|
|
[0, 1],
|
|
[1, 2],
|
|
[2, 3],
|
|
[3, 4],
|
|
[1, 5],
|
|
[5, 6],
|
|
[6, 7],
|
|
[1, 8],
|
|
[8, 9],
|
|
[9, 10],
|
|
[1, 11],
|
|
[11, 12],
|
|
[12, 13],
|
|
[0, 14],
|
|
[14, 16],
|
|
[0, 15],
|
|
[15, 17],
|
|
];
|
|
const connect_color = [
|
|
[0, 0, 255],
|
|
[255, 0, 0],
|
|
[255, 170, 0],
|
|
[255, 255, 0],
|
|
[255, 85, 0],
|
|
[170, 255, 0],
|
|
[85, 255, 0],
|
|
[0, 255, 0],
|
|
[0, 255, 85],
|
|
[0, 255, 170],
|
|
[0, 255, 255],
|
|
[0, 170, 255],
|
|
[0, 85, 255],
|
|
[85, 0, 255],
|
|
[170, 0, 255],
|
|
[255, 0, 255],
|
|
[255, 0, 170],
|
|
[255, 0, 85],
|
|
];
|
|
|
|
const default_keypoints = [
|
|
[241, 57],
|
|
[241, 120],
|
|
[191, 118],
|
|
[177, 183],
|
|
[163, 252],
|
|
[298, 118],
|
|
[317, 182],
|
|
[332, 245],
|
|
[225, 241],
|
|
[213, 359],
|
|
[215, 454],
|
|
[270, 240],
|
|
[282, 360],
|
|
[286, 456],
|
|
[230, 48],
|
|
[255, 48],
|
|
[215, 52],
|
|
[270, 52],
|
|
];
|
|
const default_subset=[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17]
|
|
const asyncLoadImg = (imgURL) => {
|
|
return new Promise((resolve,reject) => {
|
|
// 2.6.0 版本没有对图片加载异常的处理,需要自己处理
|
|
fabric.Image.fromURL(imgURL, img => {
|
|
// 加载出错提示,否则会造成渲染出错
|
|
if(img._originalElement === null){
|
|
reject('图片加载出错啦~~~')
|
|
}else {
|
|
resolve(img)
|
|
}
|
|
})
|
|
// 4.2.0版本有错误处理
|
|
// fabric.Image.fromURL(imgURL, (img,isError) => {})
|
|
})
|
|
}
|
|
|
|
/** 将屏幕坐标映射为 Fabric 画布坐标。 */
|
|
function screenToCanvas(screenX, screenY, canvas) {
|
|
const bounds = canvas.upperCanvasEl.getBoundingClientRect();
|
|
return {
|
|
x: (screenX - bounds.left) * canvas.width / bounds.width,
|
|
y: (screenY - bounds.top) * canvas.height / bounds.height,
|
|
};
|
|
}
|
|
|
|
class OpenPose {
|
|
constructor(node, canvasElement) {
|
|
this.lockMode = false;
|
|
this.visibleEyes = true;
|
|
this.flipped = false;
|
|
this.node = node;
|
|
this.undo_history = LS_Poses[node.name].undo_history || [];
|
|
this.redo_history = LS_Poses[node.name].redo_history || [];
|
|
this.groups = [];
|
|
this.subsets = [];
|
|
this.hands = [];
|
|
this.prompts = [];
|
|
this.negatives = [];
|
|
this.promptInput=null;
|
|
this.negativeInput=null;
|
|
this.backgImg=null
|
|
this.index = 1
|
|
this.history_index = 0;
|
|
this.history_change = false;
|
|
this.canvas = this.initCanvas(canvasElement);
|
|
this.disabled=false
|
|
this._widgetIndex = null;
|
|
this._widgetWidth = null;
|
|
this._widgetHeight = null;
|
|
// 创建用于选择图片的input元素
|
|
this.addPoseDemoInput = document.createElement("input");
|
|
this.addPoseDemoInput.type = "file";
|
|
this.addPoseDemoInput.accept = "image/*";
|
|
this.addPoseDemoInput.style.display = "none";
|
|
this.addPoseDemoInput.addEventListener("change", this.addPoseDemo.bind(this));
|
|
document.body.appendChild(this.addPoseDemoInput);
|
|
// 创建用于选择图片的input元素
|
|
this.backgroundInput = document.createElement("input");
|
|
this.backgroundInput.type = "file";
|
|
this.backgroundInput.accept = "image/*";
|
|
this.backgroundInput.style.display = "none";
|
|
this.backgroundInput.addEventListener("change", this.onLoadBackground.bind(this));
|
|
document.body.appendChild(this.backgroundInput);
|
|
}
|
|
|
|
setCanvasWidth(width){
|
|
this.canvas.setWidth(width);
|
|
}
|
|
setCanvasHeight(height){
|
|
this.canvas.setHeight(height);
|
|
}
|
|
|
|
|
|
/** 获取缓存的 widget 引用 */
|
|
getWidgetIndex() {
|
|
if (!this._widgetIndex) this._widgetIndex = this.node.widgets.find(w => w.name === 'index');
|
|
return this._widgetIndex;
|
|
}
|
|
getWidgetWidth() {
|
|
if (!this._widgetWidth) this._widgetWidth = this.node.widgets.find(w => w.name === 'width');
|
|
return this._widgetWidth;
|
|
}
|
|
getWidgetHeight() {
|
|
if (!this._widgetHeight) this._widgetHeight = this.node.widgets.find(w => w.name === 'height');
|
|
return this._widgetHeight;
|
|
}
|
|
|
|
/** 缓存全部 widget 引用(节点创建完成后调用) */
|
|
cacheWidgets() {
|
|
this._widgetIndex = this.node.widgets.find(w => w.name === 'index');
|
|
this._widgetWidth = this.node.widgets.find(w => w.name === 'width');
|
|
this._widgetHeight = this.node.widgets.find(w => w.name === 'height');
|
|
}
|
|
|
|
/** 保存当前状态到 undo_history */
|
|
pushUndoState() {
|
|
this.undo_history.push({
|
|
'groups': JSON.parse(JSON.stringify(this.groups)),
|
|
'hands': JSON.parse(JSON.stringify(this.hands)),
|
|
'index': this.history_index,
|
|
});
|
|
this.redo_history.length = 0;
|
|
this.history_change = true;
|
|
}
|
|
|
|
// 处理背景图片的加载
|
|
onLoadBackground(e) {
|
|
const file = this.backgroundInput.files[0];
|
|
const reader = new FileReader();
|
|
reader.onload = (event) => {
|
|
const base64 = event.target.result;
|
|
this.backgImg=base64;
|
|
this.setBackgroundImage(base64);
|
|
};
|
|
|
|
|
|
reader.readAsDataURL(file);
|
|
}
|
|
//添加示例图片
|
|
addPoseDemo(e) {
|
|
const file = this.addPoseDemoInput.files[0];
|
|
if(file){
|
|
this.getImageOpse(file)
|
|
}
|
|
}
|
|
|
|
// 设置背景图片
|
|
setBackgroundImage(url) {
|
|
fabric.Image.fromURL(url, (img) => {
|
|
img.set({
|
|
originX: 'left',
|
|
originY: 'top',
|
|
opacity: 0.95
|
|
});
|
|
const w = this.getWidgetWidth();
|
|
const h = this.getWidgetHeight();
|
|
w.value=img.width
|
|
h.value=img.height
|
|
this.setCanvasWidth(img.width)
|
|
this.setCanvasHeight(img.height)
|
|
this.node.painter.setCanvasSize(w.value,h.value)
|
|
this.canvas.setBackgroundImage(img, this.canvas.renderAll.bind(this.canvas));
|
|
});
|
|
}
|
|
|
|
//左右翻转
|
|
fliplf() {
|
|
let aobj=this.canvas.getActiveObject()
|
|
if(!aobj){
|
|
alert("请选择一只需要翻转的手")
|
|
return
|
|
}
|
|
if(aobj?.itype === 'hand'){
|
|
aobj.set('flipX', !aobj.flipX);
|
|
this.canvas.renderAll();
|
|
}else{
|
|
alert("只有手能翻转")
|
|
}
|
|
}
|
|
//上下翻转
|
|
fliptd() {
|
|
let aobj=this.canvas.getActiveObject()
|
|
if(!aobj){
|
|
alert("请选择一只需要翻转的手")
|
|
return
|
|
}
|
|
if(aobj?.itype === 'hand'){
|
|
aobj.set('flipY', !aobj.flipY);
|
|
this.canvas.renderAll();
|
|
}else{
|
|
alert("只有手能翻转")
|
|
}
|
|
}
|
|
addRect(){ //添加矩形遮罩
|
|
this.changeIndexPose()
|
|
this.groups.push({
|
|
left: 100, // 初始水平位置
|
|
top: 100, // 初始垂直位置
|
|
width: 100, // 正方形的宽度
|
|
height: 100, // 正方形的高度
|
|
fill: 'rgba(255,0,0,0.5)' // 填充颜色,这里设置为红色半透明
|
|
});
|
|
this.subsets.push({});
|
|
this.hands.push([]);
|
|
this.prompts.push('');
|
|
this.negatives.push('');
|
|
this.getWidgetIndex().value = this.groups.length-1;
|
|
this.getWidgetIndex().options['max']=this.groups.length-1;
|
|
for(let i=0;i<this.groups.length;i++){
|
|
if(!Array.isArray(this.groups[i])){
|
|
let indexColor=getDrawColor(i/this.groups.length,"7F") //hexToRgba(,0.5);
|
|
this.groups[i]['fill']=indexColor
|
|
}
|
|
}
|
|
this.setIndexPose(this.getWidgetIndex().value,false);
|
|
}
|
|
// 异步加载图片
|
|
async addHand(obj,isEdit=true,group=undefined){
|
|
let x=obj['l'],y=obj['t'];
|
|
let img = await asyncLoadImg(`/lam/getImage?type=hands&name=${obj.name}`)
|
|
let left=x?x-img.width*0.1/2:0,top=y?y-img.height*0.1/2:0;
|
|
let nobj=Object.assign({
|
|
left: left, // 图片的初始水平位置
|
|
top: top, // 图片的初始垂直位置
|
|
angle: 0, // 图片的初始旋转角度
|
|
opacity: 0.95, // 图片透明度
|
|
scaleX: 0.1,
|
|
scaleY: 0.1,
|
|
flipX:false,
|
|
flipY:false,
|
|
selectable: isEdit,
|
|
evented: isEdit,
|
|
}, obj);
|
|
img.set(nobj);
|
|
img['itype']='hand'
|
|
if(group){
|
|
group.addWithUpdate(img);
|
|
}else{
|
|
this.canvas.add(img); // 将图片添加到画布上
|
|
}
|
|
if(x && y){
|
|
this.canvas.setActiveObject(img); //选中
|
|
this.hands[this.history_index].push({
|
|
left: left, // 图片的初始水平位置
|
|
top: top, // 图片的初始垂直位置
|
|
angle: 0, // 图片的初始旋转角度
|
|
name:obj.name,
|
|
scaleX: 0.1,
|
|
scaleY: 0.1,
|
|
flipX:false,
|
|
flipY:false,
|
|
x:x,
|
|
y:y
|
|
})
|
|
this.canvas.requestRenderAll();
|
|
}
|
|
return img
|
|
}
|
|
|
|
|
|
delIndexPose(){
|
|
let delIndex=this.history_index
|
|
this.groups.splice(delIndex,1)
|
|
this.subsets.splice(delIndex,1)
|
|
this.hands.splice(delIndex,1)
|
|
this.prompts.splice(delIndex,1)
|
|
this.negatives.splice(delIndex,1)
|
|
this.getWidgetIndex().value =0;
|
|
this.getWidgetIndex().options['max']=this.groups.length-1;
|
|
this.node.setDirtyCanvas(true);
|
|
this.setIndexPose(0,false)
|
|
}
|
|
setIndexPose(v,isChangIndex=true){
|
|
this.lockMode=true
|
|
if(isChangIndex){
|
|
this.changeIndexPose()
|
|
}
|
|
this.history_index=v
|
|
let groups=this.groups
|
|
let hands=this.hands
|
|
let vgroups=[]
|
|
for (let i = 0; i < groups.length; i++) {
|
|
if(i==v){
|
|
continue;
|
|
}
|
|
vgroups.push(groups[i])
|
|
}
|
|
this.setPose([],vgroups,false)
|
|
for (let i = 0; i < hands.length; i++) {
|
|
if(i==v){
|
|
continue;
|
|
}
|
|
for(let j = 0; j < hands[i].length; j++){
|
|
this.addHand(hands[i][j],false)
|
|
}
|
|
}
|
|
this.addPose(groups[v],true,hands[v])
|
|
this.promptInput.value=this.prompts[v]
|
|
this.negativeInput.value=this.negatives[v]
|
|
this.lockMode=false
|
|
}
|
|
|
|
changeIndexPose(index=undefined){
|
|
if(index===undefined){
|
|
index=this.history_index
|
|
}
|
|
this.canvas.discardActiveObject();
|
|
let json=this.getJSON();
|
|
let keypoints=json["keypoints"]
|
|
if(Array.isArray(keypoints)){
|
|
if(keypoints.length>1){
|
|
let group=[]
|
|
let subset=this.subsets[index]
|
|
if(subset===undefined){
|
|
subset=default_subset
|
|
}
|
|
for(let i = 0; i < subset.length; i ++){
|
|
if(subset[i]<0){
|
|
group.push([-1,-1])
|
|
}else{
|
|
group.push(keypoints[subset[i]])
|
|
}
|
|
}
|
|
this.groups[index]=group
|
|
}
|
|
}else{
|
|
this.groups[index]=keypoints
|
|
}
|
|
|
|
if(json['hands'].length>0){
|
|
this.hands[index]=json['hands']
|
|
}else{
|
|
this.hands[index]=[]
|
|
}
|
|
}
|
|
|
|
clearCanvas(){
|
|
//this.canvas.backgroundColor = "#000";
|
|
this.canvas.remove(...this.canvas.getObjects());
|
|
}
|
|
|
|
setPose(keypoints,groups=[],isEdit=true) {
|
|
this.clearCanvas();
|
|
const res = [...groups];
|
|
for (let i = 0; i < keypoints.length; i += 18) {
|
|
res.push(keypoints.slice(i, i + 18));
|
|
}
|
|
|
|
for (let item of res) {
|
|
this.addPose(item,isEdit);
|
|
this.canvas.discardActiveObject();
|
|
}
|
|
}
|
|
|
|
async addPose(keypoints = undefined,isEdit=true,hands=[]) {
|
|
if (keypoints === undefined) {
|
|
keypoints = default_keypoints;
|
|
this.changeIndexPose()
|
|
this.setPose([],this.groups,false)
|
|
this.groups.push(keypoints);
|
|
this.subsets.push(default_subset);
|
|
this.hands.push(hands);
|
|
this.prompts.push('');
|
|
this.negatives.push('');
|
|
this.getWidgetIndex().value = this.groups.length-1;
|
|
this.getWidgetIndex().options['max']=this.groups.length-1;
|
|
this.history_index=this.getWidgetIndex().value
|
|
this.promptInput.value=this.prompts[this.history_index]
|
|
this.negativeInput.value=this.negatives[this.history_index]
|
|
}
|
|
|
|
if(!Array.isArray(keypoints)){
|
|
let nobj=Object.assign({
|
|
left: 100, // 初始水平位置
|
|
top: 100, // 初始垂直位置
|
|
width: 100, // 正方形的宽度
|
|
height: 100, // 正方形的高度
|
|
lockRotation: true, // 设置不可旋转
|
|
selectable: isEdit,
|
|
evented: isEdit,
|
|
}, keypoints);
|
|
const square = new fabric.Rect(nobj);
|
|
this.canvas.add(square);
|
|
if(isEdit){
|
|
this.canvas.setActiveObject(square);
|
|
}
|
|
this.canvas.renderAll();
|
|
}else{
|
|
const group = new fabric.Group();
|
|
const makeCircle = (
|
|
color,
|
|
left,
|
|
top,
|
|
line1,
|
|
line2,
|
|
line3,
|
|
line4,
|
|
line5
|
|
) => {
|
|
let c = new fabric.Circle({
|
|
left: left,
|
|
top: top,
|
|
strokeWidth: 1,
|
|
radius: 5,
|
|
fill: color,
|
|
stroke: color,
|
|
});
|
|
|
|
c.hasControls = c.hasBorders = false;
|
|
c.line1 = line1;
|
|
c.line2 = line2;
|
|
c.line3 = line3;
|
|
c.line4 = line4;
|
|
c.line5 = line5;
|
|
|
|
return c;
|
|
};
|
|
|
|
const makeLine = (coords, color) => {
|
|
return new fabric.Line(coords, {
|
|
fill: color,
|
|
stroke: color,
|
|
strokeWidth: 10,
|
|
selectable: false,
|
|
evented: false,
|
|
});
|
|
};
|
|
|
|
const lines = {};
|
|
const circles = [];
|
|
for (let i = 0; i < connect_keypoints.length; i++) {
|
|
const item = connect_keypoints[i];
|
|
if(keypoints[item[0]][0]<0||keypoints[item[0]][1]<0||keypoints[item[1]][0]<0||keypoints[item[1]][1]<0){
|
|
continue;
|
|
}
|
|
const line = makeLine(
|
|
keypoints[item[0]].concat(keypoints[item[1]]),
|
|
`rgba(${connect_color[i].join(", ")}, 0.7)`
|
|
);
|
|
lines[i]=line;
|
|
this.canvas.add(line);
|
|
}
|
|
|
|
for (let i = 0; i < keypoints.length; i++) {
|
|
if(keypoints[i][0]<0||keypoints[i][1]<0){
|
|
continue;
|
|
}
|
|
let list = [];
|
|
|
|
connect_keypoints.filter((item, idx) => {
|
|
if (item.includes(i)) {
|
|
list.push(lines[idx]);
|
|
return idx;
|
|
}
|
|
});
|
|
const circle = makeCircle(
|
|
`rgb(${connect_color[i].join(", ")})`,
|
|
keypoints[i][0],
|
|
keypoints[i][1],
|
|
...list
|
|
);
|
|
circle["id"] = i;
|
|
circles.push(circle);
|
|
group.addWithUpdate(circle);
|
|
}
|
|
for(let i = 0; i < hands.length; i++){
|
|
await this.addHand(hands[i],true,group)
|
|
}
|
|
this.canvas.discardActiveObject();
|
|
if(isEdit){
|
|
this.canvas.setActiveObject(group);
|
|
this.canvas.add(group);
|
|
group.toActiveSelection();
|
|
}
|
|
this.canvas.requestRenderAll();
|
|
}
|
|
|
|
}
|
|
|
|
/** 更新关键点连线位置的公共逻辑 */
|
|
_updatePointLines(p, left, top, showEyes) {
|
|
if (p["id"] === 0) {
|
|
p.line1 && p.line1.set({ x1: left, y1: top });
|
|
} else {
|
|
p.line1 && p.line1.set({ x2: left, y2: top });
|
|
}
|
|
if (p["id"] === 14 || p["id"] === 15) {
|
|
p.radius = showEyes ? 5 : 0.3;
|
|
if (p.line1) p.line1.strokeWidth = showEyes ? 10 : 0;
|
|
if (p.line2) p.line2.strokeWidth = showEyes ? 10 : 0;
|
|
}
|
|
p.line2 && p.line2.set({ x1: left, y1: top });
|
|
p.line3 && p.line3.set({ x1: left, y1: top });
|
|
p.line4 && p.line4.set({ x1: left, y1: top });
|
|
p.line5 && p.line5.set({ x1: left, y1: top });
|
|
}
|
|
|
|
initCanvas(canvasElement) {
|
|
this.canvas = new fabric.Canvas(canvasElement, {
|
|
backgroundColor: "#000",
|
|
preserveObjectStacking: true,
|
|
});
|
|
|
|
const updateLines = (target) => {
|
|
if (!("_objects" in target)) {
|
|
const p = target;
|
|
this._updatePointLines(p, p.left, p.top, this.visibleEyes);
|
|
this.canvas.renderAll();
|
|
return;
|
|
}
|
|
|
|
const flipX = target.flipX ? -1 : 1;
|
|
const flipY = target.flipY ? -1 : 1;
|
|
this.flipped = flipX * flipY === -1;
|
|
const showEyes = this.flipped ? !this.visibleEyes : this.visibleEyes;
|
|
|
|
if (target.angle === 0) {
|
|
const rtop = target.top;
|
|
const rleft = target.left;
|
|
for (const p of target._objects) {
|
|
if ((p['itype'] && p['itype'] === 'hand') || p.type === "rect") continue;
|
|
p.scaleX = 1;
|
|
p.scaleY = 1;
|
|
const top = rtop + p.top * target.scaleY * flipY + (target.height * target.scaleY) / 2;
|
|
const left = rleft + p.left * target.scaleX * flipX + (target.width * target.scaleX) / 2;
|
|
p["_top"] = top;
|
|
p["_left"] = left;
|
|
this._updatePointLines(p, left, top, showEyes);
|
|
}
|
|
} else {
|
|
const aCoords = target.aCoords;
|
|
const center = {
|
|
x: (aCoords.tl.x + aCoords.br.x) / 2,
|
|
y: (aCoords.tl.y + aCoords.br.y) / 2,
|
|
};
|
|
const rad = (target.angle * Math.PI) / 180;
|
|
const sin = Math.sin(rad);
|
|
const cos = Math.cos(rad);
|
|
for (const p of target._objects) {
|
|
const p_top = p.top * target.scaleY * flipY;
|
|
const p_left = p.left * target.scaleX * flipX;
|
|
const left = center.x + p_left * cos - p_top * sin;
|
|
const top = center.y + p_left * sin + p_top * cos;
|
|
p["_top"] = top;
|
|
p["_left"] = left;
|
|
this._updatePointLines(p, left, top, showEyes);
|
|
}
|
|
}
|
|
this.canvas.renderAll();
|
|
};
|
|
|
|
this.canvas.on("object:moving", (e) => {
|
|
updateLines(e.target);
|
|
});
|
|
|
|
this.canvas.on("object:scaling", (e) => {
|
|
updateLines(e.target);
|
|
this.canvas.renderAll();
|
|
});
|
|
|
|
this.canvas.on("object:rotating", (e) => {
|
|
updateLines(e.target);
|
|
this.canvas.renderAll();
|
|
});
|
|
|
|
this.canvas.on("object:modified", () => {
|
|
console.log('object:modified取消的事件')
|
|
if (
|
|
this.lockMode ||
|
|
(this.canvas.getActiveObject()?.type == "activeSelection")
|
|
)
|
|
return;
|
|
|
|
this.changeIndexPose()
|
|
this.pushUndoState();
|
|
this.history_change = true;
|
|
});
|
|
|
|
this.canvas.on("selection:cleared", (e) => {
|
|
if (this.lockMode)
|
|
return;
|
|
|
|
if(e?.deselected?.length > 1 || (e?.deselected?.length === 1 && e?.deselected[0].itype === "hand")){
|
|
let json=this.getJSON();
|
|
if (!Array.isArray(json["keypoints"])||(json["keypoints"].length>0&&json["keypoints"].length >1)) {
|
|
this.changeIndexPose()
|
|
this.pushUndoState();
|
|
this.history_change = true;
|
|
}
|
|
|
|
}
|
|
console.log('selection:cleared取消的事件')
|
|
});
|
|
|
|
// if (!LS_Poses[this.node.name].undo_history.length) {
|
|
// this.groups=[]
|
|
// this.hands=[]
|
|
// this.addPose();
|
|
this.pushUndoState();
|
|
// }
|
|
return this.canvas;
|
|
}
|
|
|
|
undo() {
|
|
if (this.undo_history.length > 0) {
|
|
this.lockMode = true;
|
|
if (this.undo_history.length > 1)
|
|
this.redo_history.push(this.undo_history.pop());
|
|
|
|
const content = this.undo_history[this.undo_history.length - 1];
|
|
this.loadPreset(JSON.parse(JSON.stringify(content)));
|
|
this.canvas.renderAll();
|
|
this.lockMode = false;
|
|
this.history_change = true;
|
|
}
|
|
}
|
|
|
|
redo() {
|
|
if (this.redo_history.length > 0) {
|
|
this.lockMode = true;
|
|
const content = this.redo_history.pop();
|
|
this.undo_history.push(JSON.parse(JSON.stringify(content)));
|
|
this.loadPreset(content);
|
|
this.canvas.renderAll();
|
|
this.lockMode = false;
|
|
this.history_change = true;
|
|
}
|
|
}
|
|
|
|
resetCanvas() {
|
|
this.canvas.clear();
|
|
this.canvas.backgroundColor = "#000";
|
|
this.groups=[]
|
|
this.subsets = [];
|
|
this.hands = [];
|
|
this.prompts = [];
|
|
this.negatives = [];
|
|
this.backgImg = null;
|
|
this.addPose();
|
|
this.pushUndoState();
|
|
}
|
|
|
|
updateHistoryData() {
|
|
if (this.history_change) {
|
|
LS_Poses[this.node.name].undo_history = this.undo_history;
|
|
LS_Poses[this.node.name].redo_history = this.redo_history;
|
|
LS_Save();
|
|
this.history_change = false;
|
|
}
|
|
}
|
|
|
|
//拖拽手势处理函数
|
|
onDragDrop = (e) => {
|
|
const name = e.dataTransfer.files[0].name;
|
|
const { x, y } = screenToCanvas(e.x, e.y, this.canvas);
|
|
this.addHand({ name, l: x, t: y });
|
|
return true;
|
|
};
|
|
|
|
|
|
getHands(){ //获取手势
|
|
const addHandsBtn = async () => {
|
|
try {
|
|
const resp = await api.fetchApi(`/lam/getHeads`);
|
|
if (resp.status === 200) {
|
|
const data = await resp.json();
|
|
console.log(data)
|
|
let rightButtons = document.createElement("div");
|
|
rightButtons.className = "panelRightButtons comfy-menu-btns";
|
|
for(let i=0;i<data.length;i++){
|
|
var img = document.createElement('img');
|
|
img.src = `/lam/getImage?type=hands&name=${data[i]}`;
|
|
img.alt = data[i];
|
|
img.style.width = "40px";
|
|
img.style.height = "40px";
|
|
img.addEventListener("dragend", (e) => {
|
|
const pt = screenToCanvas(e.x, e.y, this.canvas);
|
|
this.addHand({ name: e.target.alt, l: pt.x, t: pt.y });
|
|
});
|
|
|
|
img.addEventListener("drag", (e) => {
|
|
app.dragOverNode = thi;
|
|
});
|
|
rightButtons.appendChild(img);
|
|
}
|
|
this.canvas.wrapperEl.appendChild(rightButtons);
|
|
} else {
|
|
alert(resp.status + " - " + resp.statusText);
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
};
|
|
addHandsBtn();
|
|
}
|
|
|
|
getImageOpse(image){ //上传图片识别骨骼姿态
|
|
// 创建一个自定义的Loading控件
|
|
var loading = (function() {
|
|
if(!this.canvas.wrapperEl.loadingDiv){
|
|
var loadingDiv = document.createElement('div');
|
|
loadingDiv.style.position = 'absolute';
|
|
loadingDiv.style.left = '0';
|
|
loadingDiv.style.top = '0';
|
|
loadingDiv.style.right = '0';
|
|
loadingDiv.style.bottom = '0';
|
|
loadingDiv.style.background = 'rgba(255, 255, 255, 0.5)';
|
|
loadingDiv.style.display = 'flex';
|
|
loadingDiv.style.justifyContent = 'center';
|
|
loadingDiv.style.alignItems = 'center';
|
|
loadingDiv.style.zIndex = '100';
|
|
loadingDiv.style.textAlign = 'center';
|
|
loadingDiv.style.paddingTop = this.canvas.height/2+'px';
|
|
var loadingText = document.createElement('p');
|
|
loadingText.textContent = '图片识别中...';
|
|
loadingDiv.appendChild(loadingText);
|
|
|
|
this.canvas.wrapperEl.appendChild(loadingDiv);
|
|
this.canvas.wrapperEl.loadingDiv = loadingDiv;
|
|
}
|
|
return {
|
|
show: function() {
|
|
this.canvas.wrapperEl.loadingDiv.style.display = 'block';
|
|
},
|
|
hide: function() {
|
|
this.canvas.wrapperEl.loadingDiv.style.display = 'none';
|
|
}
|
|
};
|
|
})();
|
|
// 在执行某些异步操作前显示Loading
|
|
loading.show();
|
|
|
|
const uploadFile = async (blobFile) => {
|
|
try {
|
|
const resp = await fetch("/lam/getImagePose", {
|
|
method: "POST",
|
|
body: blobFile,
|
|
});
|
|
if (resp.status === 200) {
|
|
const data = await resp.json();
|
|
console.log(data)
|
|
if(data.groups.length<=0){
|
|
alert('未识别到姿态骨骼');
|
|
// 操作完成后隐藏Loading
|
|
loading.hide();
|
|
return ;
|
|
}
|
|
for(let i = 0; i < data.groups.length; i++){
|
|
this.groups.push(data.groups[i]);
|
|
this.subsets.push(data.subsets[i]);
|
|
this.hands.push([]);
|
|
this.prompts.push('');
|
|
this.negatives.push('');
|
|
}
|
|
|
|
this.getWidgetIndex().value = this.groups.length-1;
|
|
this.getWidgetIndex().options['max']=this.groups.length-1;
|
|
this.node.setDirtyCanvas(true);
|
|
this.setIndexPose(this.groups.length-1,false)
|
|
this.updateHistoryData();
|
|
// 操作完成后隐藏Loading
|
|
loading.hide();
|
|
} else {
|
|
alert(resp.status + " - " + resp.statusText);
|
|
loading.hide();
|
|
}
|
|
} catch (error) {
|
|
console.error(error);
|
|
loading.hide();
|
|
}
|
|
};
|
|
let formData = new FormData();
|
|
formData.append("image", image, image.name);
|
|
uploadFile(formData);
|
|
}
|
|
|
|
|
|
|
|
getJSON() {
|
|
const json = {
|
|
keypoints: this.canvas
|
|
.getObjects()
|
|
.filter((item) => {
|
|
if (item.type === "circle" || (item.type === "rect" && item.selectable)) return item;
|
|
})
|
|
.map((item) => {
|
|
if(item.type === "rect"){
|
|
return {left:item.left,top:item.top,
|
|
width:item['scaleX']?item.width*item['scaleX']:item.width,
|
|
height:item['scaleY']?item.height*item['scaleY']:item.height,
|
|
fill:item.fill};
|
|
}
|
|
return [Math.round(item.left), Math.round(item.top)];
|
|
}),
|
|
hands:this.canvas
|
|
.getObjects()
|
|
.filter((item) => {
|
|
if (item.type === "image" && item.itype === "hand" && item.selectable ) return item;
|
|
})
|
|
.map((item) => {
|
|
let obj=item.getCenterPoint();
|
|
return Object.assign(obj,{left:item.left,top:item.top,
|
|
scaleX:item.scaleX,scaleY:item.scaleY,
|
|
flipX:item.flipX,flipY:item.flipY,
|
|
name:item.name,angle:item.angle
|
|
});
|
|
}),
|
|
|
|
};
|
|
if(json.keypoints.length==1&&!Array.isArray(json.keypoints[0])){
|
|
json.keypoints=json.keypoints[0];
|
|
}
|
|
return json;
|
|
}
|
|
|
|
loadPreset(obj) {
|
|
this.groups=obj['groups']
|
|
let index=obj['index']
|
|
this.getWidgetIndex().value=index;
|
|
this.getWidgetIndex().options['max']=this.groups.length;
|
|
this.node.setDirtyCanvas(true);
|
|
this.setIndexPose(index,false)
|
|
}
|
|
}
|
|
|
|
// Create OpenPose widget
|
|
function createOpenPose(node, inputName, inputData, app) {
|
|
node.name = inputName;
|
|
const widget = {
|
|
type: "openpose",
|
|
name: `w${inputName}`,
|
|
value:'',
|
|
|
|
draw: function (ctx, _, widgetWidth, y, widgetHeight) {
|
|
const top=0;//app.menu.element.offsetHeight;
|
|
const left=0; //app.bodyLeft.offsetWidth;
|
|
const margin = 10,
|
|
visible = app.canvas.ds.scale > 0.5 && this.type === "openpose",
|
|
clientRectBound = ctx.canvas.getBoundingClientRect(),
|
|
transform = new DOMMatrix()
|
|
.scaleSelf(
|
|
clientRectBound.width / ctx.canvas.width,
|
|
clientRectBound.height / ctx.canvas.height
|
|
)
|
|
.multiplySelf(ctx.getTransform())
|
|
.translateSelf(margin, margin + y),
|
|
w = (widgetWidth - margin * 2 - 3) * transform.a;
|
|
|
|
let h=w/this.openpose.children[0].width*this.openpose.children[0].height
|
|
|
|
Object.assign(this.openpose.style, {
|
|
left: `${transform.a * margin+left + transform.e}px`,
|
|
top: `${ top + transform.d + transform.f}px`,
|
|
width: w + "px",
|
|
height: h + "px",
|
|
position: "absolute",
|
|
zIndex: app.graph._nodes.indexOf(node),
|
|
});
|
|
|
|
Object.assign(this.openpose.children[0].style, {
|
|
width: w + "px",
|
|
height: h + "px",
|
|
});
|
|
|
|
Object.assign(this.openpose.children[1].style, {
|
|
width: w + "px",
|
|
height: h + "px",
|
|
});
|
|
|
|
Array.from(this.openpose.children[2].children).forEach((element) => {
|
|
Object.assign(element.style, {
|
|
width: `${28.0 * transform.a}px`,
|
|
height: `${22.0 * transform.d}px`,
|
|
fontSize: `${transform.d * 10.0}px`,
|
|
});
|
|
element.hidden = !visible;
|
|
});
|
|
Array.from(
|
|
this.openpose.querySelectorAll(
|
|
"textarea,div.prompt_div"
|
|
)
|
|
).forEach((element) => {
|
|
if (element.tagName == "TEXTAREA") {
|
|
Object.assign(element.style, {
|
|
width: `${300 * transform.a}px`,
|
|
height: `${190 * transform.d}px`,
|
|
fontSize: `${transform.d * 10.0}px`,
|
|
});
|
|
}else if (element.tagName == "DIV") {
|
|
Object.assign(element.style, {
|
|
top: `${-340 * transform.a}px`
|
|
});
|
|
}
|
|
|
|
});
|
|
},
|
|
};
|
|
|
|
// Fabric canvas
|
|
let canvasOpenPose = document.createElement("canvas");
|
|
let canvasPainter = document.createElement("canvas");
|
|
node.openPose = new OpenPose(node, canvasOpenPose,canvasPainter);
|
|
node.openPose.setCanvasWidth(512)
|
|
node.openPose.setCanvasHeight(512)
|
|
node.openPose.cacheWidgets();
|
|
|
|
let index = node.openPose.getWidgetIndex();
|
|
let width = node.openPose.getWidgetWidth();
|
|
let height = node.openPose.getWidgetHeight();
|
|
index.callback = function (v) {
|
|
node.openPose.setIndexPose(v)
|
|
}
|
|
width.callback = function (v) {
|
|
node.openPose.setCanvasWidth(v)
|
|
node.painter.setCanvasSize(v,node.painter.canvas.height)
|
|
node.openPose.updateDomLayout?.()
|
|
}
|
|
height.callback = function (v) {
|
|
node.openPose.setCanvasHeight(v)
|
|
node.painter.setCanvasSize(node.painter.canvas.width,v)
|
|
node.openPose.updateDomLayout?.()
|
|
}
|
|
|
|
widget.openpose = node.openPose.canvas.wrapperEl;
|
|
widget.parent = node;
|
|
const setPoseValue = (x) => {
|
|
if (!x) return;
|
|
node.openPose.setCanvasWidth(width.value)
|
|
node.openPose.setCanvasHeight(height.value)
|
|
node.painter.setCanvasSize(width.value,height.value)
|
|
let data=JSON.parse(x)
|
|
node.openPose.groups=data.groups;
|
|
node.openPose.hands=data.hands;
|
|
node.openPose.subsets=data.subsets;
|
|
index.value =0;
|
|
index.options['max']=this.openPose.groups.length-1;
|
|
if('backgImg' in data && data.backgImg){
|
|
node.openPose.backgImg=data.backgImg;
|
|
node.openPose.setBackgroundImage(node.openPose.backgImg)
|
|
}
|
|
if('prompts' in data && data.prompts){
|
|
node.openPose.prompts=data.prompts;
|
|
}
|
|
if('negatives' in data && data.negatives){
|
|
node.openPose.negatives=data.negatives;
|
|
}
|
|
node.openPose.setIndexPose(0,false)
|
|
};
|
|
const getPoseValue = () => {
|
|
return JSON.stringify({
|
|
groups: node.openPose.groups,
|
|
subsets: node.openPose.subsets,
|
|
hands: node.openPose.hands,
|
|
backgImg:node.openPose.backgImg?node.openPose.backgImg:null,
|
|
prompts:node.openPose.prompts?node.openPose.prompts:null,
|
|
negatives:node.openPose.negatives?node.openPose.negatives:null
|
|
});
|
|
};
|
|
Object.defineProperty(widget, "value", {
|
|
set: setPoseValue,
|
|
get: getPoseValue,
|
|
});
|
|
|
|
// Create elements undo, redo, clear history
|
|
const panelButtons = document.createElement("div");
|
|
const buttonDefs = [
|
|
{ text: "Ref", title: "背景图片", action: () => node.openPose.backgroundInput.click() },
|
|
{ text: "+", title: "添加骨骼", action: () => node.openPose.addPose() },
|
|
{ text: "-", title: "删除骨骼", action: () => node.openPose.delIndexPose() },
|
|
{ text: "重", title: "重置", action: () => node.openPose.resetCanvas() },
|
|
{ text: "img", title: "添加人物示例", action: () => node.openPose.addPoseDemoInput.click() },
|
|
{ text: "<-", title: "上一步", action: () => node.openPose.undo() },
|
|
{ text: "->", title: "下一步", action: () => node.openPose.redo() },
|
|
{ text: "←", title: "左右翻转", action: () => node.openPose.fliplf() },
|
|
{ text: "↑", title: "上下翻转", action: () => node.openPose.fliptd() },
|
|
{ text: "□", title: "矩形遮罩", action: () => node.openPose.addRect() },
|
|
];
|
|
|
|
panelButtons.className = "panelButtons comfy-menu-btns";
|
|
for (const def of buttonDefs) {
|
|
const btn = document.createElement("button");
|
|
btn.textContent = def.text;
|
|
btn.title = def.title;
|
|
btn.addEventListener("click", def.action);
|
|
panelButtons.appendChild(btn);
|
|
}
|
|
|
|
// 清除历史按钮(需确认,单独处理)
|
|
const historyClearButton = document.createElement("button");
|
|
historyClearButton.textContent = "\u2716";
|
|
historyClearButton.title = "清除历史";
|
|
historyClearButton.addEventListener("click", () => {
|
|
if (confirm(`删除节点"${node.name}"的所有姿态历史记录?`)) {
|
|
node.openPose.undo_history = [];
|
|
node.openPose.redo_history = [];
|
|
if (node.openPose.groups.length > 0) {
|
|
node.openPose.pushUndoState();
|
|
}
|
|
node.openPose.history_change = true;
|
|
node.openPose.updateHistoryData();
|
|
}
|
|
});
|
|
panelButtons.appendChild(historyClearButton);
|
|
node.openPose.canvas.wrapperEl.appendChild(panelButtons);
|
|
|
|
//添加提示词输入框
|
|
let promptDiv = document.createElement("div");
|
|
promptDiv.className = "prompt_div";
|
|
let promptInput = document.createElement("textarea");
|
|
promptInput.className = "comfy-multiline-input promptInput";
|
|
promptInput.placeholder = "正向提示词";
|
|
promptInput.addEventListener("input", () => {
|
|
node.openPose.prompts[node.openPose.history_index]=promptInput.value;
|
|
});
|
|
promptDiv.appendChild(promptInput);
|
|
node.openPose.promptInput=promptInput
|
|
//反向提示词
|
|
let negativeInput = document.createElement("textarea");
|
|
negativeInput.className = "comfy-multiline-input promptInput";
|
|
negativeInput.placeholder = "反向提示词";
|
|
negativeInput.addEventListener("input", () => {
|
|
node.openPose.negatives[node.openPose.history_index]=negativeInput.value;
|
|
});
|
|
promptDiv.appendChild(negativeInput);
|
|
node.openPose.negativeInput=negativeInput
|
|
node.openPose.canvas.wrapperEl.appendChild(promptDiv);
|
|
|
|
const usesDomWidget = typeof node.addDOMWidget === "function";
|
|
let registeredWidget = widget;
|
|
let domHost = null;
|
|
if (usesDomWidget) {
|
|
domHost = document.createElement("div");
|
|
domHost.className = "lam-openpose-dom-widget";
|
|
Object.assign(domHost.style, {
|
|
position: "relative",
|
|
width: "100%",
|
|
overflow: "visible",
|
|
});
|
|
Object.assign(widget.openpose.style, {
|
|
position: "relative",
|
|
left: "auto",
|
|
top: "auto",
|
|
margin: "0 auto",
|
|
zIndex: "1",
|
|
});
|
|
domHost.appendChild(widget.openpose);
|
|
node.openPoseDomContainer = domHost;
|
|
node.openPose.domExtraHeight = 220;
|
|
Object.assign(promptDiv.style, {
|
|
position: "absolute",
|
|
width: "90%",
|
|
height: "100px",
|
|
left: "5%",
|
|
display: "flex",
|
|
gap: "6px",
|
|
zIndex: "4",
|
|
boxSizing: "border-box",
|
|
});
|
|
promptDiv.querySelectorAll("textarea").forEach((textarea) => {
|
|
Object.assign(textarea.style, {
|
|
width: "calc(50% - 3px)",
|
|
height: "100%",
|
|
margin: "0",
|
|
boxSizing: "border-box",
|
|
});
|
|
});
|
|
node.openPose.updateDomLayout = () => {
|
|
const canvasWidth = Number(node.openPose.canvas.width) || 512;
|
|
const canvasHeight = Number(node.openPose.canvas.height) || 512;
|
|
const extraHeight = Number(node.openPose.domExtraHeight) || 220;
|
|
Object.assign(widget.openpose.style, {
|
|
width: `${canvasWidth}px`,
|
|
height: `${canvasHeight}px`,
|
|
});
|
|
promptDiv.style.top = `${canvasHeight + 8}px`;
|
|
domHost.style.height = `${canvasHeight + extraHeight}px`;
|
|
domHost.style.minHeight = `${canvasHeight + extraHeight}px`;
|
|
node.updatePainterDomLayout?.();
|
|
};
|
|
node.openPose.updateDomLayout();
|
|
} else {
|
|
const parentNode = document.createElement("div");
|
|
parentNode.appendChild(widget.openpose)
|
|
app.canvasContainer.appendChild(parentNode)
|
|
}
|
|
document.addEventListener('keydown', function(event) {
|
|
if(node.openPose.disabled){
|
|
return;
|
|
}
|
|
if(Object.keys(app.canvas.selected_nodes).length>1 || Object.keys(app.canvas.selected_nodes).length==0){
|
|
return;
|
|
}
|
|
if('LAM.OpenPoseEditorPlus'!=app.canvas.selected_nodes[Object.keys(app.canvas.selected_nodes)[0]].type){
|
|
return;
|
|
}
|
|
if(event.key=='Delete'||event.key=='Backspace'){
|
|
let activeObject=node.openPose.canvas.getActiveObject()
|
|
if (activeObject) {
|
|
node.openPose.canvas.remove(activeObject);
|
|
}
|
|
node.openPose.setIndexPose(node.openPose.history_index,true)
|
|
}
|
|
if(event.key=='ArrowLeft'){
|
|
node.widgets[node.openPose.index].value=node.widgets[node.openPose.index].value!=0?node.widgets[node.openPose.index].value-1:node.widgets[node.openPose.index].options['max']
|
|
node.openPose.setIndexPose(node.widgets[node.openPose.index].value)
|
|
}
|
|
if(event.key=='ArrowRight'){
|
|
node.widgets[node.openPose.index].value=node.widgets[node.openPose.index].value!=node.widgets[node.openPose.index].options['max']?node.widgets[node.openPose.index].value+1:0
|
|
node.openPose.setIndexPose(node.widgets[node.openPose.index].value)
|
|
}
|
|
|
|
});
|
|
|
|
node.openPose.getHands()
|
|
node.addWidget("button", "显/隐画板", "ShowPainter", () => {
|
|
const previousExtraHeight = Number(node.openPose.domExtraHeight) || 220;
|
|
node.painter.disabled=!node.painter.disabled
|
|
node.openPose.disabled=!node.openPose.disabled
|
|
|
|
if(node.openPose.disabled){
|
|
panelButtons.style.display="none"
|
|
const rightPanel = node.openPose.canvas.wrapperEl.querySelector('.panelRightButtons');
|
|
if (rightPanel) rightPanel.style.display="none"
|
|
}else{
|
|
panelButtons.style.display="flex"
|
|
const rightPanel = node.openPose.canvas.wrapperEl.querySelector('.panelRightButtons');
|
|
if (rightPanel) rightPanel.style.display="flex"
|
|
}
|
|
if (node.painterDomLayer) {
|
|
// Keep the pose canvas visible below the transparent painter canvas so
|
|
// users can trace or paint directly against the skeleton reference.
|
|
node.openPose.canvas.wrapperEl.style.display = "block";
|
|
node.painterDomLayer.style.display = node.painter.disabled ? "none" : "block";
|
|
} else {
|
|
node.openPose.canvas.wrapperEl.style.display = node.openPose.disabled ? "none" : "block";
|
|
node.painter.canvas.wrapperEl.style.display = node.painter.disabled ? "none" : "block";
|
|
}
|
|
if (node.painterDomLayer) {
|
|
const nextExtraHeight = node.painter.disabled ? 220 : 340;
|
|
node.openPose.domExtraHeight = nextExtraHeight;
|
|
node.openPose.updateDomLayout?.();
|
|
const heightDelta = nextExtraHeight - previousExtraHeight;
|
|
if (heightDelta && typeof node.setSize === "function") {
|
|
node.setSize([node.size[0], node.size[1] + heightDelta]);
|
|
}
|
|
node.setDirtyCanvas?.(true, true);
|
|
}
|
|
});
|
|
|
|
// Keep the pose data widget after ShowPainter so existing workflow widget
|
|
// value ordering remains compatible with the legacy implementation.
|
|
if (usesDomWidget) {
|
|
registeredWidget = node.addDOMWidget(widget.name, widget.type, domHost, {
|
|
hideOnZoom: false,
|
|
getValue: getPoseValue,
|
|
setValue: setPoseValue,
|
|
getMinHeight: () => (Number(node.openPose.canvas.height) || 512) + (Number(node.openPose.domExtraHeight) || 220),
|
|
getHeight: () => (Number(node.openPose.canvas.height) || 512) + (Number(node.openPose.domExtraHeight) || 220),
|
|
});
|
|
registeredWidget.openpose = widget.openpose;
|
|
registeredWidget.parent = node;
|
|
} else {
|
|
node.addCustomWidget(widget);
|
|
}
|
|
|
|
node.onRemoved = () => {
|
|
if (Object.hasOwn(LS_Poses, node.name)) {
|
|
delete LS_Poses[node.name];
|
|
LS_Save();
|
|
}
|
|
|
|
// When removing this node we need to remove the input from the DOM
|
|
for (let y in node.widgets) {
|
|
if (node.widgets[y].openpose) {
|
|
node.widgets[y].openpose.remove();
|
|
}
|
|
if (node.widgets[y].painter_wrap) {
|
|
node.widgets[y].painter_wrap.remove();
|
|
}
|
|
}
|
|
};
|
|
|
|
if (!usesDomWidget) {
|
|
widget.onRemove = () => {
|
|
widget.openpose?.remove();
|
|
widget.painter_wrap?.remove();
|
|
};
|
|
}
|
|
|
|
if (!usesDomWidget) app.canvas.onDrawBackground = function () {
|
|
// Draw node isnt fired once the node is off the screen
|
|
// if it goes off screen quickly, the input may not be removed
|
|
// this shifts it off screen so it can be moved back if the node is visible.
|
|
for (let n in app.graph._nodes) {
|
|
n = graph._nodes[n];
|
|
for (let w in n.widgets) {
|
|
let wid = n.widgets[w];
|
|
if (Object.hasOwn(wid, "openpose")) {
|
|
wid.openpose.style.left = -8000 + "px";
|
|
wid.openpose.style.position = "absolute";
|
|
}
|
|
}
|
|
}
|
|
};
|
|
return { widget: registeredWidget };
|
|
}
|
|
|
|
window.LS_Poses = {};
|
|
function LS_Save() {
|
|
///console.log("Save:", LS_Poses);
|
|
localStorage.setItem("ComfyUI_Poses", JSON.stringify(LS_Poses));
|
|
}
|
|
|
|
app.registerExtension({
|
|
name: "LAM.OpenPoseEditorPlus",
|
|
async init(app) {
|
|
// Any initial setup to run as soon as the page loads
|
|
let style = document.createElement("style");
|
|
style.innerText = `
|
|
.panelButtons{
|
|
position: absolute;
|
|
padding: 4px;
|
|
display: flex;
|
|
gap: 4px;
|
|
flex-direction: column;
|
|
width: fit-content;
|
|
}
|
|
.panelButtons button:last-child{
|
|
border-color: var(--error-text);
|
|
color: var(--error-text) !important;
|
|
}
|
|
.panelRightButtons{
|
|
right: 0px;
|
|
overflow: auto;
|
|
height: 95%;
|
|
position: absolute;
|
|
padding: 4px;
|
|
display: flex;
|
|
gap: 4px;
|
|
flex-direction: column;
|
|
width: fit-content;
|
|
}
|
|
.panelRightButtons button:last-child{
|
|
border-color: var(--error-text);
|
|
color: var(--error-text) !important;
|
|
}
|
|
.panelPaintBox {
|
|
position: absolute;
|
|
width: 100%;
|
|
}
|
|
.comfy-menu-btns button.active {
|
|
color: var(--error-text) !important;
|
|
font-weight: bold;
|
|
border: 1px solid;
|
|
}
|
|
.painter_manipulation_box {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: -40px;
|
|
transform: translateX(-50%);
|
|
}
|
|
.painter_manipulation_box > div {
|
|
display: grid;
|
|
gap: 2px;
|
|
grid-template-columns: 0.1fr 0.1fr 0.1fr 0.1fr 0.1fr;
|
|
justify-content: center;
|
|
margin-bottom: 2px;
|
|
}
|
|
.painter_manipulation_box > div button {
|
|
font-size: 0.5rem;
|
|
}
|
|
.painter_manipulation_box > div button[id^=zpos]:active {
|
|
background-color: #484848;
|
|
}
|
|
.painter_drawning_box {
|
|
position: absolute;
|
|
top: -40px;
|
|
width: 88px;
|
|
}
|
|
.painter_drawning_box button {
|
|
width: 24px;
|
|
}
|
|
.painter_drawning_box_property {
|
|
position: absolute;
|
|
top: -36px;
|
|
}
|
|
.painter_drawning_box_property select {
|
|
color: var(--input-text);
|
|
background-color: var(--comfy-input-bg);
|
|
border-radius: 4px;
|
|
border-color: var(--border-color);
|
|
border-style: solid;
|
|
}
|
|
.separator {
|
|
width: 1px;
|
|
height: 25px;
|
|
background-color: var(--border-color);
|
|
display: inline-block;
|
|
vertical-align: middle;
|
|
margin: 0 4px 0 4px;
|
|
}
|
|
.painter_drawning_box > div {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
align-items: center;
|
|
}
|
|
.painter_drawning_box input[type="number"] {
|
|
width: 2.6rem;
|
|
color: var(--input-text);
|
|
background-color: var(--comfy-input-bg);
|
|
border-radius: 8px;
|
|
border-color: var(--border-color);
|
|
border-style: solid;
|
|
font-size: inherit;
|
|
}
|
|
.painter_colors_box input[type="color"], .painter_bg_setting input[type="color"] {
|
|
width: 1.5rem;
|
|
height: 1.5rem;
|
|
padding: 0;
|
|
margin-bottom: 5px;
|
|
color: var(--input-text);
|
|
background-color: var(--comfy-input-bg);
|
|
border-radius: 8px;
|
|
border-color: var(--border-color);
|
|
border-style: solid;
|
|
font-size: inherit;
|
|
}
|
|
|
|
.painter_bg_setting button{
|
|
width: 40px;
|
|
height: 20px;
|
|
}
|
|
.painter_bg_setting #bgColor:after {
|
|
content: attr(data-label);
|
|
position: absolute;
|
|
color: var(--input-text);
|
|
left: 70%;
|
|
font-size: 0.5rem;
|
|
margin-top: -18%;
|
|
}
|
|
.painter_colors_box input[type="color"]::-webkit-color-swatch-wrapper, .painter_bg_setting input[type="color"]::-webkit-color-swatch-wrapper {
|
|
padding: 1px !important;
|
|
}
|
|
.painter_grid_style {
|
|
display: grid;
|
|
gap: 3px;
|
|
font-size: 0.55rem;
|
|
text-align: center;
|
|
align-items: start;
|
|
justify-items: center;
|
|
}
|
|
.painter_shapes_box {
|
|
grid-template-columns: 1fr 1fr 1fr;
|
|
}
|
|
.painter_colors_alpha {
|
|
grid-template-columns: 0.7fr 0.7fr;
|
|
}
|
|
.fieldset_box {
|
|
padding: 2px;
|
|
margin: 15px 0 2px 0;
|
|
position: relative;
|
|
text-align: center;
|
|
}
|
|
.fieldset_box:before {
|
|
content: attr(f_name) ":";
|
|
font-size: 0.6rem;
|
|
position: absolute;
|
|
top: -10px;
|
|
color: yellow;
|
|
left: 0;
|
|
right: 0;
|
|
}
|
|
.list_objects_panel {
|
|
width: 90%;
|
|
font-size: 0.7rem;
|
|
}
|
|
.list_objects_panel__items {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
overflow-y: auto;
|
|
max-height: 350px;
|
|
}
|
|
.list_objects_panel__items button {
|
|
width: 80% !important;
|
|
}
|
|
.list_objects_panel__items::-webkit-scrollbar {
|
|
width: 8px;
|
|
}
|
|
.list_objects_panel__items::-webkit-scrollbar-track {
|
|
background-color: var(--descrip-text);
|
|
}
|
|
.list_objects_panel__items::-webkit-scrollbar-thumb {
|
|
background-color: var(--fg-color);
|
|
}
|
|
.list_objects_align {
|
|
display: flex;
|
|
flex-direction: column;
|
|
text-align: center;
|
|
justify-content: space-between;
|
|
height: 430px;
|
|
}
|
|
.list_objects_align > div{
|
|
flex: 1;
|
|
}
|
|
.painter_history_panel {
|
|
position: absolute;
|
|
padding: 4px;
|
|
display: flex;
|
|
gap: 4px;
|
|
right: 0;
|
|
opacity: 0.8;
|
|
flex-direction: row;
|
|
width: fit-content;
|
|
}
|
|
.painter_history_panel > button {
|
|
background: transparent;
|
|
}
|
|
.painter_history_panel > button:hover:enabled {
|
|
opacity: 1;
|
|
border-color: var(--error-text);
|
|
color: var(--error-text) !important;
|
|
}
|
|
.painter_history_panel > button:disabled {
|
|
opacity: .5;
|
|
}
|
|
.painter_stroke_box {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
.painter_stroke_box > label span {
|
|
margin-right: 9px;
|
|
vertical-align: middle;
|
|
font-size: 10px;
|
|
color: var(--input-text);
|
|
}
|
|
.property_brushesBox, .property_brushesSecondBox {
|
|
display: flex;
|
|
gap: 5px;
|
|
}
|
|
.property_brushesSecondBox > button {
|
|
min-width: 30px;
|
|
font-size: 0.5rem;
|
|
}
|
|
.property_brushesSecondBox svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
.active svg > .cls-2 {
|
|
fill: var(--error-text);
|
|
}
|
|
.active svg > * {
|
|
stroke: var(--error-text);
|
|
}
|
|
.prompt_div{
|
|
position: absolute;
|
|
width: 90%;
|
|
height: 100px;
|
|
}
|
|
.prompt_div .promptInput {
|
|
margin: 0 3px;
|
|
width: 45%;
|
|
}
|
|
|
|
`;
|
|
document.head.appendChild(style);
|
|
},
|
|
async setup(app) {
|
|
let openPoseNode = app.graph._nodes.filter((wi) => wi.type === "LAM.OpenPoseEditorPlus");
|
|
|
|
if (openPoseNode.length) {
|
|
for (const n of openPoseNode) {
|
|
console.log(`Setup PoseNode: ${n.name}`);
|
|
let widgetImage = n.widgets.find((w) => w.name == "image");
|
|
if (widgetImage && Object.hasOwn(LS_Poses, n.name)) {
|
|
let pose_ls = LS_Poses[n.name].undo_history;
|
|
if(pose_ls.length>0){
|
|
n.openPose.loadPreset(pose_ls[pose_ls.length - 1]);
|
|
}else{
|
|
n.openPose.groups=[]
|
|
n.openPose.subsets = [];
|
|
n.openPose.hands = [];
|
|
n.openPose.prompts = [];
|
|
n.openPose.negatives = [];
|
|
n.openPose.addPose();
|
|
n.openPose.pushUndoState();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
|
if (nodeData.name === "LAM.OpenPoseEditorPlus") {
|
|
if (painters_settings_json) {
|
|
const message = createMessage(
|
|
"Loading",
|
|
"Please wait, <span style='font-weight: bold; color: orange'>Painter node</span> settings are loading. Loading times may take a long time if large images have been added to the canvas!"
|
|
);
|
|
LS_Painters = await loadData();
|
|
document.body.removeChild(message);
|
|
} else {
|
|
LS_Painters =
|
|
localStorage.getItem("ComfyUI_Painter") &&
|
|
JSON.parse(localStorage.getItem("ComfyUI_Painter"));
|
|
|
|
if (!LS_Painters || LS_Painters === undefined) {
|
|
localStorage.setItem("ComfyUI_Painter", JSON.stringify({}));
|
|
LS_Painters = JSON.parse(localStorage.getItem("ComfyUI_Painter"));
|
|
}
|
|
}
|
|
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
|
nodeType.prototype.onNodeCreated = function () {
|
|
const r = onNodeCreated
|
|
? onNodeCreated.apply(this, arguments)
|
|
: undefined;
|
|
let openPoseNode = app.graph._nodes.filter((wi) => wi.type === "LAM.OpenPoseEditorPlus");
|
|
let nodeName = `Pose_${openPoseNode.length}`;
|
|
let nodeNamePNG = `${nodeName}`;
|
|
|
|
console.log(`Create PoseNode: ${nodeName}`);
|
|
LS_Poses =
|
|
localStorage.getItem("ComfyUI_Poses") &&
|
|
JSON.parse(localStorage.getItem("ComfyUI_Poses"));
|
|
if (!LS_Poses) {
|
|
localStorage.setItem("ComfyUI_Poses", JSON.stringify({}));
|
|
LS_Poses = JSON.parse(localStorage.getItem("ComfyUI_Poses"));
|
|
}
|
|
|
|
if (!Object.hasOwn(LS_Poses, nodeNamePNG)) {
|
|
LS_Poses[nodeNamePNG] = {
|
|
undo_history: [],
|
|
redo_history: [],
|
|
};
|
|
LS_Save();
|
|
}
|
|
if (LS_Painters && !Object.hasOwn(LS_Painters, nodeNamePNG)) {
|
|
LS_Painters[nodeNamePNG] = {
|
|
undo_history: [],
|
|
redo_history: [],
|
|
canvas_settings: { background: "#000000" },
|
|
settings: {},
|
|
};
|
|
LS_PSave();
|
|
}
|
|
|
|
createOpenPose.apply(this, [this, nodeNamePNG, {}, app]);
|
|
PainterWidget.apply(this, [this, nodeNamePNG, {}, app]);
|
|
this.setSize([750, 950]);
|
|
return r;
|
|
};
|
|
}
|
|
},
|
|
});
|