merge tianlang0704/multiuser
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
import time
|
||||
|
||||
|
||||
class PhotoshopInstance:
|
||||
SPECIAL_LAYER_NEW_LAYER = '### New Layer ###'
|
||||
SPECIAL_LAYER_USE_CANVAS = '### Use Canvas ###'
|
||||
SPECIAL_LAYER_USE_SELECTION = '### Use Selection ###'
|
||||
SPECIAL_LAYER_SAME_AS_LAYER = '### Same as Layer ###'
|
||||
SPECIAL_LAYER_NAME_TO_ID = {
|
||||
SPECIAL_LAYER_USE_CANVAS: 0,
|
||||
SPECIAL_LAYER_USE_SELECTION: -1,
|
||||
SPECIAL_LAYER_NEW_LAYER: -2,
|
||||
SPECIAL_LAYER_SAME_AS_LAYER: -3
|
||||
}
|
||||
|
||||
def __init__(self, sdppp, sid, uid):
|
||||
self.sdppp = sdppp
|
||||
self.sid = sid
|
||||
self.uid = uid
|
||||
self.layers = []
|
||||
self.on_destroy = None
|
||||
self.reset_change_tracker()
|
||||
|
||||
def destroy(self):
|
||||
if self.on_destroy:
|
||||
self.on_destroy(self)
|
||||
|
||||
def get_layers(self):
|
||||
return list(map(lambda layer: f"{layer['name']} (id:{layer['id']})", self.layers))
|
||||
|
||||
def get_base_layers(self):
|
||||
raw_layer_strs = self.get_layers()
|
||||
layer_strs = list(raw_layer_strs)
|
||||
layer_strs.insert(0, self.SPECIAL_LAYER_USE_CANVAS)
|
||||
return layer_strs
|
||||
|
||||
def get_bounds_layers(self):
|
||||
bounds_strs = list(self.get_layers())
|
||||
bounds_strs.insert(0, self.SPECIAL_LAYER_USE_CANVAS)
|
||||
bounds_strs.insert(1, self.SPECIAL_LAYER_USE_SELECTION)
|
||||
bounds_strs.insert(1, self.SPECIAL_LAYER_SAME_AS_LAYER)
|
||||
return bounds_strs
|
||||
|
||||
def get_set_layers(self):
|
||||
raw_layer_strs = self.get_layers()
|
||||
set_layer_strs = list(raw_layer_strs)
|
||||
set_layer_strs.insert(0, self.SPECIAL_LAYER_NEW_LAYER)
|
||||
return set_layer_strs
|
||||
|
||||
def layer_name_to_id(self, layer_name, refrence_id=None):
|
||||
id = 0
|
||||
if layer_name == self.SPECIAL_LAYER_SAME_AS_LAYER:
|
||||
id = refrence_id
|
||||
elif self.SPECIAL_LAYER_NAME_TO_ID.get(layer_name, None) is not None:
|
||||
id = self.SPECIAL_LAYER_NAME_TO_ID[layer_name]
|
||||
else:
|
||||
layer_name_and_id_split = layer_name.split('(id:')
|
||||
id = int(layer_name_and_id_split.pop().strip()[:-1])
|
||||
return id
|
||||
|
||||
async def get_image(self, layer_id, bounds_id=False):
|
||||
result = await self.sdppp.sio.call('get_image', data={'layer_id': layer_id, 'use_layer_bounds': bounds_id}, to=self.sid)
|
||||
if not result:
|
||||
return None, None
|
||||
history_state_id = await self._update_layer_bounds_history_state_id(layer_id, bounds_id)
|
||||
await self._update_history_state_id_after_internal_change(history_state_id)
|
||||
|
||||
return result['upload_name'], result['layer_opacity']
|
||||
|
||||
async def send_images(self, image_ids, layer_id=""):
|
||||
result = await self.sdppp.sio.call('send_images', data={'image_ids': image_ids, 'layer_id': layer_id}, to=self.sid)
|
||||
await self._update_history_state_id_after_internal_change()
|
||||
return result
|
||||
|
||||
async def get_active_history_state_id(self):
|
||||
result = await self.sdppp.sio.call('get_active_history_state_id', data={}, to=self.sid)
|
||||
if result is None:
|
||||
return 0
|
||||
id = result.get('history_state_id', None)
|
||||
return id
|
||||
|
||||
|
||||
# ===
|
||||
def reset_change_tracker(self):
|
||||
self.get_img_state_id = 0
|
||||
self.change_tracker = {}
|
||||
self.comfyui_last_value_tracker = {}
|
||||
|
||||
# for apis calls
|
||||
async def is_ps_history_changed(self):
|
||||
current_id = await self.get_active_history_state_id()
|
||||
if self.get_img_state_id is None or current_id is None:
|
||||
return False
|
||||
return self.get_img_state_id != current_id and self.get_img_state_id < current_id
|
||||
|
||||
async def check_layer_bounds_combo_changed(self, layer, use_layer_bounds):
|
||||
layer_bounds_combo = f"{layer}{use_layer_bounds}"
|
||||
layer_bounds_history_state_id = self.change_tracker.get(layer_bounds_combo, None)
|
||||
if layer_bounds_history_state_id is None:
|
||||
return True
|
||||
latest_state_id = await self.get_active_history_state_id()
|
||||
if latest_state_id > layer_bounds_history_state_id:
|
||||
return True, latest_state_id
|
||||
#didn't change, use last value
|
||||
comfyui_last_value = self.comfyui_last_value_tracker.get(layer_bounds_combo, None) or layer_bounds_history_state_id
|
||||
return False, comfyui_last_value
|
||||
|
||||
# have to track this value because comfyui determines if the value is changed by comparing it with the last value.
|
||||
# can't use the real history id because sending images changes the history id, and comfyui will think the value is changed when it's not
|
||||
def update_comfyui_last_value(self, layer, use_layer_bounds, value):
|
||||
layer_bounds_combo = f"{layer}{use_layer_bounds}"
|
||||
self.comfyui_last_value_tracker[layer_bounds_combo] = value
|
||||
return value
|
||||
|
||||
# need to update history id after internal operation otherwise it might cause infinite change loop
|
||||
async def _update_history_state_id_after_internal_change(self, history_state_id=None):
|
||||
if history_state_id is None: # need to get new id after operation, it causes history change
|
||||
history_state_id = await self.get_active_history_state_id()
|
||||
if self.get_img_state_id is not None: # udpate all the change trackers with the new history state id
|
||||
self.change_tracker = {k: history_state_id for k, v in self.change_tracker.items() if v == self.get_img_state_id}
|
||||
self.get_img_state_id = history_state_id # it gets into a loop if get img state is not updated
|
||||
|
||||
async def _update_layer_bounds_history_state_id(self, layer, use_layer_bounds):
|
||||
layer_bounds_combo = f"{layer}{use_layer_bounds}"
|
||||
latest_state_id = await self.get_active_history_state_id()
|
||||
self.change_tracker[layer_bounds_combo] = latest_state_id
|
||||
return latest_state_id
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import socketio
|
||||
import json
|
||||
import asyncio
|
||||
from socketio import exceptions
|
||||
from .photoshop_manager import PhotoshopManager
|
||||
|
||||
class SDPPP:
|
||||
def __init__(self, PromptServer):
|
||||
self.photoshop_instances = dict()
|
||||
self.comfyui_instances = dict()
|
||||
self.sid_to_ip = dict()
|
||||
|
||||
self.sio = socketio.AsyncServer(async_mode='aiohttp', cors_allowed_origins="*")
|
||||
self.sio.attach(PromptServer.instance.app, socketio_path='/sd-ppp/')
|
||||
self.registerSocketListeners()
|
||||
|
||||
self.loop = PromptServer.instance.loop
|
||||
|
||||
def has_ps_instance(self):
|
||||
return len(self.photoshop_instances) > 0
|
||||
|
||||
def get_ps_instance(self, sid = None):
|
||||
if len(self.photoshop_instances) == 0:
|
||||
return None
|
||||
if sid is None:
|
||||
sid = list(self.photoshop_instances.keys())[0]
|
||||
return self.photoshop_instances[sid]
|
||||
|
||||
def get_client_info_from_event(self, sid, data):
|
||||
ip = self.sid_to_ip[sid]
|
||||
if not ip:
|
||||
return
|
||||
client_id = data.get('client_id', None)
|
||||
if not client_id:
|
||||
client_id = 0
|
||||
user_id = data.get('user_id', None)
|
||||
if not user_id:
|
||||
user_id = 0
|
||||
return ip, client_id, user_id
|
||||
|
||||
def registerSocketListeners(self):
|
||||
sio = self.sio
|
||||
|
||||
@sio.event
|
||||
async def connect(sid, environ):
|
||||
ip = self.sid_to_ip[sid] = environ['REMOTE_ADDR']
|
||||
qs = environ['QUERY_STRING']
|
||||
|
||||
qsobj = dict(x.split('=') for x in qs.split('&'))
|
||||
if 'type' not in qsobj:
|
||||
raise exceptions.ConnectionRefusedError('instance type missed in query')
|
||||
|
||||
elif qsobj['type'] == 'photoshop':
|
||||
user_id = qsobj.get('user_id', None)
|
||||
if not user_id:
|
||||
user_id = 0
|
||||
self.photoshop_instances[sid] = await PhotoshopManager.instance().new_ps_instance(self, sid, ip, user_id)
|
||||
|
||||
elif qsobj['type'] == 'comfyui':
|
||||
client_id = qsobj.get('client_id', None)
|
||||
if not client_id:
|
||||
client_id = 0
|
||||
user_id = qsobj.get('user_id', None)
|
||||
if not user_id:
|
||||
user_id = 0
|
||||
self.comfyui_instances[sid] = client_id
|
||||
PhotoshopManager.instance().instance_from_client_info(ip, client_id, user_id)
|
||||
|
||||
else:
|
||||
raise exceptions.ConnectionRefusedError('unknown instance type ' + qsobj['type'])
|
||||
|
||||
|
||||
# only emit by photoshop instance
|
||||
@sio.event
|
||||
async def disconnect(sid):
|
||||
if sid in self.photoshop_instances:
|
||||
instance = self.photoshop_instances.pop(sid, None)
|
||||
instance.destroy()
|
||||
elif sid in self.comfyui_instances:
|
||||
self.comfyui_instances.pop(sid, None)
|
||||
self.sid_to_ip.pop(sid, None)
|
||||
|
||||
# only emit by photoshop instance
|
||||
@sio.event
|
||||
async def sync_layers(sid, data):
|
||||
instance = self.get_ps_instance(sid)
|
||||
if instance is None:
|
||||
return
|
||||
instance.layers = data['layers']
|
||||
|
||||
layer_strs = instance.get_base_layers()
|
||||
bounds_strs = instance.get_bounds_layers()
|
||||
set_layer_strs = instance.get_set_layers()
|
||||
for sid, client_id in self.comfyui_instances.items():
|
||||
check_instance = PhotoshopManager.instance().instance_from_client_id(client_id)
|
||||
if check_instance != instance:
|
||||
continue
|
||||
await sio.emit('sync_layers', {
|
||||
'layer_strs': layer_strs,
|
||||
'bound_strs': bounds_strs,
|
||||
'set_layer_strs': set_layer_strs,
|
||||
}, to=sid)
|
||||
|
||||
@sio.event
|
||||
async def check_changes(sid, data):
|
||||
ip, client_id, user_id = self.get_client_info_from_event(sid, data)
|
||||
instance = PhotoshopManager.instance().instance_from_client_info(ip, client_id, user_id)
|
||||
if instance is None:
|
||||
return
|
||||
if not await instance.is_ps_history_changed():
|
||||
return
|
||||
await sio.emit('trigger_graph_change', to=sid)
|
||||
|
||||
@sio.event
|
||||
async def init(sid, data):
|
||||
ip, client_id, user_id = self.get_client_info_from_event(sid, data)
|
||||
instance = PhotoshopManager.instance().instance_from_client_info(ip, client_id, user_id)
|
||||
if instance is None:
|
||||
return
|
||||
instance.reset_change_tracker()
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { api } from "../../../scripts/api.js"
|
||||
import socketio from './socket.io.js'
|
||||
|
||||
let layerStrs = [];
|
||||
let boundsStrs = [];
|
||||
let setLayerStrs = [];
|
||||
let socket = null;
|
||||
|
||||
console.log("[sd-ppp]", "Loading js extension");
|
||||
|
||||
const DEFAULT_USER_ID = "Change if sharing remote server"
|
||||
|
||||
function getUserId() {
|
||||
let userId = app.ui.settings.getSettingValue("SD-PPP.userId");
|
||||
if (!userId || userId == DEFAULT_USER_ID) {
|
||||
userId = "";
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.SD-PPP",
|
||||
init() {
|
||||
},
|
||||
async setup() {
|
||||
// set change query loop
|
||||
setInterval(checkChanges, 1000);
|
||||
// add setting for using remote server
|
||||
const userName = localStorage["Comfy.userName"];
|
||||
const emptyValue = app.multiUserServer ? userName : DEFAULT_USER_ID
|
||||
app.ui.settings.addSetting({
|
||||
id: "SD-PPP.userId",
|
||||
name: "SD-PPP: User ID",
|
||||
defaultValue: emptyValue,
|
||||
onChange: async (value, oldValue) => {
|
||||
if (!value) {
|
||||
setTimeout(() => {
|
||||
app.ui.settings.setSettingValue("SD-PPP.userId", emptyValue);
|
||||
if (!app.multiUserServer) return;
|
||||
// Change empty text box to user name immediately when in multi-user mode, it's recommanded in multi-user environment
|
||||
const input = document.querySelector("#SD-PPP-userId")
|
||||
if (input) input.value = emptyValue;
|
||||
}, 0.01);;
|
||||
}
|
||||
},
|
||||
type: "text",
|
||||
});
|
||||
|
||||
socket = socketio(location.origin, {
|
||||
transports: ["websocket"],
|
||||
path: '/sd-ppp/',
|
||||
query: {
|
||||
version: 1,
|
||||
type: 'comfyui',
|
||||
client_id: api.clientId,
|
||||
user_id: getUserId(),
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
socket.emit('init', {
|
||||
client_id: api.clientId,
|
||||
user_id: getUserId(),
|
||||
});
|
||||
});
|
||||
socket.on('disconnect', () => {
|
||||
});
|
||||
socket.on('connect_error', () => {
|
||||
console.error('sdppp socket connect_error')
|
||||
});
|
||||
socket.on('sync_layers', (data) => {
|
||||
layerStrs = data.layer_strs;
|
||||
boundsStrs = data.bound_strs;
|
||||
setLayerStrs = data.set_layer_strs;
|
||||
});
|
||||
socket.on('trigger_graph_change', () => {
|
||||
api.dispatchEvent(new CustomEvent("graphChanged"));
|
||||
});
|
||||
},
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeType.comfyClass === 'Get Image From Photoshop Layer') {
|
||||
const onSelected = nodeType.prototype.onSelected;
|
||||
const onMouseEnter = nodeType.prototype.onMouseEnter;
|
||||
let this_handler = function() {
|
||||
this.widgets[0].options.values = layerStrs;
|
||||
this.widgets[1].options.values = boundsStrs;
|
||||
}
|
||||
nodeType.prototype.onSelected = async function(...args) {
|
||||
if(onSelected) await onSelected.call(this, ...args);
|
||||
this_handler.call(this);
|
||||
}
|
||||
nodeType.prototype.onMouseEnter = async function(...args) {
|
||||
if(onMouseEnter) await onMouseEnter.call(this, ...args);
|
||||
this_handler.call(this);
|
||||
}
|
||||
} else if (nodeType.comfyClass === 'Send Images To Photoshop') {
|
||||
const onSelected = nodeType.prototype.onSelected;
|
||||
const onMouseEnter = nodeType.prototype.onMouseEnter;
|
||||
let this_handler = function() {
|
||||
this.widgets[0].options.values = setLayerStrs;
|
||||
}
|
||||
nodeType.prototype.onSelected = async function(...args) {
|
||||
if(onSelected) await onSelected.call(this, ...args);
|
||||
this_handler.call(this);
|
||||
}
|
||||
nodeType.prototype.onMouseEnter = async function(...args) {
|
||||
if(onMouseEnter) await onMouseEnter.call(this, ...args);
|
||||
this_handler.call(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const SDPPPNodes = [
|
||||
'Get Image From Photoshop Layer',
|
||||
'Send Images To Photoshop',
|
||||
]
|
||||
async function checkChanges() {
|
||||
await checkHistoryChanges();
|
||||
}
|
||||
|
||||
async function checkHistoryChanges() {
|
||||
try {
|
||||
const currentState = app.graph.serialize();
|
||||
const mode0NodeTypes = currentState.nodes.filter(node => node.mode == 0).map(node => node.type);
|
||||
const containsSDPPPNodes = mode0NodeTypes.some(nodeType => SDPPPNodes.includes(nodeType));
|
||||
if (!containsSDPPPNodes) return;
|
||||
socket.emit('check_changes', {
|
||||
client_id: api.clientId,
|
||||
user_id: getUserId(),
|
||||
})
|
||||
} catch (e) {
|
||||
console.error("[sd-ppp]", "Failed to check changes", e);
|
||||
}
|
||||
}
|
||||
Vendored
+1366
-1
File diff suppressed because one or more lines are too long
Vendored
+12
-2
@@ -9,6 +9,7 @@
|
||||
"minVersion": "24.4.0"
|
||||
},
|
||||
"requiredPermissions": {
|
||||
"enableUserInfo": true,
|
||||
"network": {
|
||||
"domains": "all"
|
||||
},
|
||||
@@ -39,19 +40,28 @@
|
||||
},
|
||||
"minimumSize": {
|
||||
"width": 300,
|
||||
"height": 100
|
||||
"height": 160
|
||||
},
|
||||
"maximumSize": {
|
||||
<<<<<<< HEAD
|
||||
"width": 300,
|
||||
"height": 500
|
||||
=======
|
||||
"width": 400,
|
||||
"height": 160
|
||||
>>>>>>> tl/develop_sio_multi_user
|
||||
},
|
||||
"preferredDockedSize": {
|
||||
"width": 300,
|
||||
"height": 100
|
||||
"height": 160
|
||||
},
|
||||
"preferredFloatingSize": {
|
||||
"width": 300,
|
||||
<<<<<<< HEAD
|
||||
"height": 500
|
||||
=======
|
||||
"height": 160
|
||||
>>>>>>> tl/develop_sio_multi_user
|
||||
},
|
||||
"icons": [
|
||||
{
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"minVersion": "24.4.0"
|
||||
},
|
||||
"requiredPermissions": {
|
||||
"enableUserInfo": true,
|
||||
"network": {
|
||||
"domains": "all"
|
||||
},
|
||||
@@ -39,19 +40,28 @@
|
||||
},
|
||||
"minimumSize": {
|
||||
"width": 300,
|
||||
"height": 100
|
||||
"height": 160
|
||||
},
|
||||
"maximumSize": {
|
||||
<<<<<<< HEAD
|
||||
"width": 300,
|
||||
"height": 500
|
||||
=======
|
||||
"width": 400,
|
||||
"height": 160
|
||||
>>>>>>> tl/develop_sio_multi_user
|
||||
},
|
||||
"preferredDockedSize": {
|
||||
"width": 300,
|
||||
"height": 100
|
||||
"height": 160
|
||||
},
|
||||
"preferredFloatingSize": {
|
||||
"width": 300,
|
||||
<<<<<<< HEAD
|
||||
"height": 500
|
||||
=======
|
||||
"height": 160
|
||||
>>>>>>> tl/develop_sio_multi_user
|
||||
},
|
||||
"icons": [
|
||||
{
|
||||
|
||||
@@ -30,7 +30,7 @@ export class PanelController {
|
||||
|
||||
create() {
|
||||
this[_root] = document.createElement("div");
|
||||
this[_root].style.height = "100vh";
|
||||
this[_root].style.height = "130vh";
|
||||
this[_root].style.overflow = "auto";
|
||||
this[_root].style.padding = "8px";
|
||||
|
||||
|
||||
@@ -6,7 +6,11 @@ import Main from "./panels/Main.jsx";
|
||||
|
||||
import { entrypoints } from "uxp";
|
||||
|
||||
<<<<<<< HEAD
|
||||
const demosController = new PanelController(() => <Main/>, {id: "sdppp-connect", menuItems: [
|
||||
=======
|
||||
const demosController = new PanelController(() => <Main/>, {id: "comfy-connect", menuItems: [
|
||||
>>>>>>> tl/develop_sio_multi_user
|
||||
// { id: "reload1", label: "Reload Plugin", enabled: true, checked: false, oninvoke: () => location.reload() },
|
||||
// { id: "dialog1", label: "About this Plugin", enabled: true, checked: false, oninvoke: () => aboutController.run() },
|
||||
] });
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
<<<<<<< HEAD
|
||||
import React from "react";
|
||||
import { storage } from "uxp";
|
||||
import { Model, ComfyConnection, TEXTS } from "../system/system.mjs";
|
||||
@@ -137,4 +138,106 @@ export default class Main extends React.Component {
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
=======
|
||||
import React from "react";
|
||||
import ComfyConnection from "../system/ComfyConnection";
|
||||
import { storage, userInfo } from "uxp";
|
||||
|
||||
export default class Main extends React.Component {
|
||||
state = {
|
||||
comfyURL: '',
|
||||
isConnected: false,
|
||||
userId: userInfo.userId().slice(0, 10),
|
||||
|
||||
ui_ExpandMore: false,
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
ComfyConnection.onConnectStateChange(() => {
|
||||
const instance = ComfyConnection.instance
|
||||
console.log('isConnected:', instance?.isConnected)
|
||||
this.setState({
|
||||
isConnected: instance?.isConnected,
|
||||
comfyURL: instance ? instance.comfyURL : ''
|
||||
})
|
||||
});
|
||||
storage.secureStorage.getItem('userId').then((value) => {
|
||||
if (!value) return
|
||||
this.setState({ userId: Buffer.from(value).toString() })
|
||||
if (this.state.userId) {
|
||||
console.log('userId:', this.state.userId)
|
||||
}
|
||||
})
|
||||
storage.secureStorage.getItem('comfyURL').then((value) => {
|
||||
if (!value) return
|
||||
this.setState({ comfyURL: Buffer.from(value).toString() })
|
||||
if (this.state.comfyURL) {
|
||||
console.log('comfyURL:', this.state.comfyURL)
|
||||
this.doConnectOrDisconnect()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
doConnectOrDisconnect() {
|
||||
if (!ComfyConnection.instance?.isConnected)
|
||||
ComfyConnection.createInstance(this.state.comfyURL, this.state.userId);
|
||||
else
|
||||
ComfyConnection.instance.disconnect();
|
||||
}
|
||||
|
||||
render() {
|
||||
const expandMore = this.state.ui_ExpandMore;
|
||||
return (
|
||||
<>
|
||||
<sp-textfield
|
||||
id="url-bar"
|
||||
label="ComfyURL"
|
||||
onInput={(ev) => { this.state.comfyURL = ev.currentTarget.value }}
|
||||
value={this.state.comfyURL}
|
||||
placeholder="http://127.0.0.1:8188"
|
||||
></sp-textfield>
|
||||
<sp-divider />
|
||||
<div id="connection-more" className={expandMore ? "expand-menu expand" : "expand-menu collapse"}>
|
||||
<div class="expand-menu-title" onClick={() => { this.setState({ui_ExpandMore: !expandMore}) }}>
|
||||
<sp-label>{expandMore ? "▼ more" : "▶ more"}</sp-label>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="input-row" id="user-id-bar">
|
||||
<sp-label>user-name:</sp-label>
|
||||
<div class="input-label">
|
||||
<sp-label>{this.state.userId}</sp-label>
|
||||
<sp-textfield
|
||||
label="USER ID"
|
||||
onInput={(ev) => {
|
||||
const userID = ev.currentTarget.value;
|
||||
if (!userID)
|
||||
storage.secureStorage.removeItem('userId');
|
||||
else {
|
||||
storage.secureStorage.setItem('userId', this.state.userId);
|
||||
this.setState({
|
||||
userId: userID
|
||||
})
|
||||
}
|
||||
}}
|
||||
value={this.state.userId}
|
||||
placeholder="User Name: Change if sharing remote server"
|
||||
></sp-textfield>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="button-box">
|
||||
<sp-button
|
||||
id="connect-btn"
|
||||
variant="primary"
|
||||
onClick={this.doConnectOrDisconnect.bind(this)}
|
||||
>{this.state.isConnected ? 'disconnect' : 'connect'}</sp-button>
|
||||
</div>
|
||||
|
||||
<sp-divider size="small"></sp-divider>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
>>>>>>> tl/develop_sio_multi_user
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#url-bar {
|
||||
width: 100%;
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: left;
|
||||
@@ -36,6 +37,12 @@
|
||||
margin: 0 5px;
|
||||
}
|
||||
.connect-box {
|
||||
=======
|
||||
#user-id-bar {
|
||||
width: 100%;
|
||||
}
|
||||
.button-box {
|
||||
>>>>>>> tl/develop_sio_multi_user
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -44,6 +51,7 @@
|
||||
}
|
||||
#connect-btn {
|
||||
text-align: center;
|
||||
<<<<<<< HEAD
|
||||
margin-right: 10px;
|
||||
}
|
||||
.client-list-item {
|
||||
@@ -81,4 +89,62 @@
|
||||
}
|
||||
.autorun-desc {
|
||||
font-size: 10px;
|
||||
=======
|
||||
}
|
||||
.input-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 30px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
.input-label sp-label {
|
||||
width: calc(100% - 15px);
|
||||
height: 100%;
|
||||
margin-left: 15px;
|
||||
}
|
||||
.input-label sp-textfield {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: -5px;
|
||||
left: 0;
|
||||
visibility: hidden;
|
||||
z-index: 5;
|
||||
}
|
||||
.input-label sp-textfield:focus {
|
||||
visibility: visible;
|
||||
}
|
||||
.input-label:hover sp-textfield{
|
||||
visibility: visible;
|
||||
}
|
||||
.expand-menu .expand-menu-title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 15px;
|
||||
padding: 5px;
|
||||
}
|
||||
.expand-menu .expand-menu-arrow {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
.expand-menu .expand-menu-title sp-icon {
|
||||
height: 15px;
|
||||
width: 20px;
|
||||
color: #ccc;
|
||||
}
|
||||
.expand-menu .content {
|
||||
margin-left: 15px
|
||||
}
|
||||
.expand-menu.collapse .content {
|
||||
display: none;
|
||||
}
|
||||
.expand-menu.expand .content {
|
||||
display: flex;
|
||||
>>>>>>> tl/develop_sio_multi_user
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import { app } from "photoshop";
|
||||
import { storage } from "uxp";
|
||||
import { getAllSubLayer } from "./util";
|
||||
import socketio from './library/socket.io.js'
|
||||
import sendImages from "./events/send_images";
|
||||
import getImage from "./events/get_image";
|
||||
import getActiveHistoryStateId from "./events/get_active_history_state_id";
|
||||
|
||||
class ComfyConnection {
|
||||
static instance = null;
|
||||
|
||||
static _connectStateCallbacks = [];
|
||||
static onConnectStateChange(callback) {
|
||||
ComfyConnection._connectStateCallbacks.push(callback);
|
||||
}
|
||||
static _callConnectStateChange() {
|
||||
ComfyConnection._connectStateCallbacks.forEach(cb => {
|
||||
try {
|
||||
cb(ComfyConnection.instance?.isConnected)
|
||||
} catch (e) { console.error(e) }
|
||||
});
|
||||
}
|
||||
|
||||
static createInstance(comfyURL, userId) {
|
||||
if (ComfyConnection.instance && ComfyConnection.instance.isConnected) {
|
||||
ComfyConnection.instance.disconnect();
|
||||
}
|
||||
ComfyConnection.instance = new ComfyConnection(comfyURL, userId);
|
||||
}
|
||||
|
||||
get isConnected() {
|
||||
return this.socket != null && this.socket.connected === true;
|
||||
}
|
||||
|
||||
comfyURL = '';
|
||||
interval = null;
|
||||
constructor(comfyURL, userId) {
|
||||
ComfyConnection.instance = this;
|
||||
if (!comfyURL) {
|
||||
comfyURL = 'http://127.0.0.1:8188';
|
||||
}
|
||||
this.comfyURL = comfyURL.replace(/\/*$/, '');
|
||||
if (!userId) {
|
||||
userId = '';
|
||||
}
|
||||
this.userId = userId;
|
||||
this.connect();
|
||||
}
|
||||
connect() {
|
||||
console.log('connect')
|
||||
if (!this.socket) {
|
||||
this._createSocket();
|
||||
}
|
||||
this.socket.connect();
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
_createSocket() {
|
||||
console.log('create socket')
|
||||
const socket = this.socket = socketio(this.comfyURL, {
|
||||
autoConnect: false,
|
||||
transports: ["websocket"],
|
||||
path: '/sd-ppp/',
|
||||
query: {
|
||||
version: 1,
|
||||
type: 'photoshop',
|
||||
user_id: this.userId,
|
||||
}
|
||||
});
|
||||
this.interval = setInterval(() => {
|
||||
if (!this.isConnected) return;
|
||||
const allLayers = getAllSubLayer(app.activeDocument);
|
||||
this.socket.emit('sync_layers',
|
||||
{ layers: allLayers }
|
||||
)
|
||||
}, 3000)
|
||||
|
||||
socket.on('connect_error', (error) => {
|
||||
if (socket.active) {
|
||||
console.error('connect_error reconnecting...')
|
||||
} else {
|
||||
console.error('connect_error disconnected')
|
||||
}
|
||||
ComfyConnection._callConnectStateChange();
|
||||
});
|
||||
socket.on('connect', () => {
|
||||
console.log('connect')
|
||||
storage.secureStorage.setItem('comfyURL', this.comfyURL);
|
||||
ComfyConnection._callConnectStateChange();
|
||||
});
|
||||
socket.on('disconnect', () => {
|
||||
console.log('disconnect')
|
||||
ComfyConnection._callConnectStateChange();
|
||||
});
|
||||
|
||||
socket.on('get_image', async (data, callback) => {
|
||||
try {
|
||||
const startTime = Date.now();
|
||||
const result = await getImage(this.comfyURL, data)
|
||||
console.log('get_image cost', Date.now() - startTime, 'ms');
|
||||
callback(result)
|
||||
} catch (e) { console.error(e); callback({ error: e.message }) }
|
||||
})
|
||||
socket.on('send_images', async (data, callback) => {
|
||||
try {
|
||||
const result = await sendImages(this.comfyURL, data)
|
||||
callback(result)
|
||||
} catch (e) { console.error(e); callback({ error: e.message }) }
|
||||
})
|
||||
socket.on('get_active_history_state_id', async (data, callback) => {
|
||||
try {
|
||||
const result = await getActiveHistoryStateId(data)
|
||||
callback(result)
|
||||
} catch (e) { console.error(e); callback({ error: e.message }) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default ComfyConnection;
|
||||
@@ -0,0 +1,205 @@
|
||||
import { app, imaging } from "photoshop";
|
||||
import { executeAsModalUntilSuccess, findInAllSubLayer, unTrimImageData } from '../util.js';
|
||||
import Jimp from "../library/jimp.min";
|
||||
import { SPECIAL_LAYER_NAME_TO_ID, SPECIAL_LAYER_USE_SELECTION } from "../util";
|
||||
|
||||
function isLayerFolder(layer){
|
||||
return layer.layers && layer.layers.length > 0;
|
||||
}
|
||||
|
||||
async function findLayer(layerID) {
|
||||
let layer;
|
||||
let isFolder = false;
|
||||
if (layerID <= 0) return [layer, isFolder];
|
||||
layer = findInAllSubLayer(app.activeDocument, layerID)
|
||||
if (!layer) throw new Error(`Layer(id: ${layerID}) not found`);
|
||||
if (!isLayerFolder(layer)) return [layer, isFolder];
|
||||
// layer is folder
|
||||
const dupLayer = await layer.duplicate();
|
||||
const mergedLayer = await dupLayer.merge()
|
||||
isFolder = true;
|
||||
return [mergedLayer, isFolder];
|
||||
}
|
||||
|
||||
// ps returns trimmed data so need padding
|
||||
function padAndTrimLayerDataToDesireBounds(layer, pixelDataFromAPI, desireBounds) {
|
||||
if (pixelDataFromAPI.length == desireBounds.width * desireBounds.height * 4) {
|
||||
return pixelDataFromAPI;
|
||||
}
|
||||
let pixelDataForReturn = new Uint8Array(desireBounds.width * desireBounds.height * 4);
|
||||
let bounds = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: app.activeDocument.width,
|
||||
bottom: app.activeDocument.height,
|
||||
}
|
||||
if (layer) bounds = layer.bounds;
|
||||
unTrimImageData(
|
||||
pixelDataFromAPI,
|
||||
pixelDataForReturn,
|
||||
bounds,
|
||||
desireBounds
|
||||
)
|
||||
return pixelDataForReturn;
|
||||
}
|
||||
|
||||
async function getPixelsDataHelper(layer, desireBounds) {
|
||||
let options = {
|
||||
documentID: app.activeDocument.id,
|
||||
applyAlpha: false,
|
||||
sourceBounds: desireBounds,
|
||||
}
|
||||
if (layer) options.layerID = layer.id
|
||||
let pixels;
|
||||
try {
|
||||
pixels = await imaging.getPixels(options)
|
||||
} catch (e) {
|
||||
console.error(e.message)
|
||||
return new Uint8Array(desireBounds.width * desireBounds.height * 4);
|
||||
}
|
||||
let psImageData = pixels.imageData
|
||||
const pixelDataFromAPI = await psImageData.getData()
|
||||
Promise.resolve().then(() => { psImageData.dispose() })
|
||||
return pixelDataFromAPI
|
||||
}
|
||||
|
||||
async function getPixelsData(layer, desireBounds) {
|
||||
// layer null = document data
|
||||
if (!layer) {
|
||||
return await getPixelsDataHelper(null, desireBounds);
|
||||
}
|
||||
// normal layer
|
||||
return await getPixelsDataHelper(layer, desireBounds);
|
||||
}
|
||||
|
||||
|
||||
function getDesiredBounds(boundsLayerID) {
|
||||
const docBounds = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: app.activeDocument.width,
|
||||
bottom: app.activeDocument.height,
|
||||
width: app.activeDocument.width,
|
||||
height: app.activeDocument.height
|
||||
};
|
||||
// use selection bounds
|
||||
if (boundsLayerID == SPECIAL_LAYER_NAME_TO_ID[SPECIAL_LAYER_USE_SELECTION]) {
|
||||
// if no selection use document bounds
|
||||
const selectionBounds = app.activeDocument.selection?.bounds;
|
||||
if (!selectionBounds) return docBounds;
|
||||
return {
|
||||
left: selectionBounds.left,
|
||||
top: selectionBounds.top,
|
||||
right: selectionBounds.right,
|
||||
bottom: selectionBounds.bottom,
|
||||
width: selectionBounds.width,
|
||||
height: selectionBounds.height
|
||||
}
|
||||
}
|
||||
let boundsLayer;
|
||||
if (boundsLayerID > 0) {
|
||||
boundsLayer = findInAllSubLayer(app.activeDocument, boundsLayerID)
|
||||
if (!boundsLayer) throw new Error(`Bounds layer(id: ${boundsLayerID}) not found`);
|
||||
}
|
||||
// null boundsLayer = document bounds
|
||||
if (!boundsLayer) return docBounds;
|
||||
// empty boundsLayer = document bounds
|
||||
const boundsLayerBounds = boundsLayer.bounds
|
||||
const isEmptyBoundsLayer = boundsLayerBounds.left == 0 && boundsLayerBounds.top == 0 && boundsLayerBounds.right == 0 && boundsLayerBounds.bottom == 0;
|
||||
if (isEmptyBoundsLayer) return docBounds;
|
||||
// normal layer
|
||||
let desireBounds = {
|
||||
left: boundsLayerBounds.left,
|
||||
top: boundsLayerBounds.top,
|
||||
right: boundsLayerBounds.right,
|
||||
bottom: boundsLayerBounds.bottom,
|
||||
width: boundsLayerBounds.width,
|
||||
height: boundsLayerBounds.height
|
||||
};
|
||||
return desireBounds
|
||||
}
|
||||
|
||||
export default async function getImage(comfyURL, params) {
|
||||
const layerID = params.layer_id
|
||||
const boundsLayerID = params.use_layer_bounds
|
||||
let uploadName = 0;
|
||||
let layerOpacity = 100;
|
||||
|
||||
await executeAsModalUntilSuccess(async (executionContext) => {
|
||||
let hostControl;
|
||||
let suspensionID;
|
||||
const startTime = Date.now();
|
||||
let layer;
|
||||
let isFolder = false;
|
||||
const activeLayers = app.activeDocument?.activeLayers;
|
||||
try {
|
||||
hostControl = executionContext.hostControl;
|
||||
suspensionID = await hostControl.suspendHistory({
|
||||
"documentID": app.activeDocument.id,
|
||||
"name": "Image To ComfyUI"
|
||||
});
|
||||
[layer, isFolder] = await findLayer(layerID);
|
||||
layerOpacity = layer?.opacity ?? 100;
|
||||
const desireBounds = getDesiredBounds(boundsLayerID);
|
||||
const pixelDataFromAPI = await getPixelsData(layer, desireBounds)
|
||||
const pixelDataForReturn = padAndTrimLayerDataToDesireBounds(layer, pixelDataFromAPI, desireBounds)
|
||||
console.log('getPixels', Date.now() - startTime, 'ms');
|
||||
// log desire size
|
||||
const image = await new Promise((resolve, reject) => {
|
||||
new Jimp({
|
||||
data: pixelDataForReturn,
|
||||
width: desireBounds.width,
|
||||
height: desireBounds.height
|
||||
}, (err, image) => {
|
||||
err ? reject(err) : resolve(image);
|
||||
})
|
||||
})
|
||||
console.log('new Jimp', Date.now() - startTime, 'ms');
|
||||
image.quality(100);
|
||||
console.log('quality', Date.now() - startTime, 'ms');
|
||||
const file = await image.getBufferAsync(Jimp.MIME_PNG);
|
||||
console.log('create pngfile', Date.now() - startTime, 'ms');
|
||||
|
||||
const fd = new FormData();
|
||||
const PhotoshopBlob = new Blob([file], { type: "image/png" });
|
||||
fd.append('image', PhotoshopBlob, "PhotoshopBlob.png") ;
|
||||
fd.append('overwrite', "true");
|
||||
console.log('start upload', Date.now() - startTime, 'ms');
|
||||
const promise = fetch(comfyURL + '/upload/image', {
|
||||
method: 'POST',
|
||||
body: fd,
|
||||
}).then(res => {
|
||||
if (res.status == 200)
|
||||
return res.json()
|
||||
else
|
||||
throw new Error('HTTP ' + res.status)
|
||||
})
|
||||
console.log('finish upload', Date.now() - startTime, 'ms')
|
||||
const result = await promise
|
||||
console.log('upload resulted', Date.now() - startTime, 'ms')
|
||||
|
||||
if (result.error) throw new Error(result.error);
|
||||
if (!result.name) throw new Error('No upload_name')
|
||||
uploadName = result.name
|
||||
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw e
|
||||
|
||||
} finally {
|
||||
if (layer && isFolder) layer.delete();
|
||||
if (activeLayers && activeLayers.length > 0) {
|
||||
for (let i = 0; i < activeLayers.length; i++) {
|
||||
activeLayers[i].selected = true;
|
||||
}
|
||||
}
|
||||
if (hostControl && suspensionID) hostControl.resumeHistory(suspensionID);
|
||||
}
|
||||
|
||||
}, { commandName: "get content of layer " + layerID })
|
||||
|
||||
return {
|
||||
upload_name: uploadName,
|
||||
layer_opacity: layerOpacity,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import { app, imaging } from "photoshop";
|
||||
import { executeAsModalUntilSuccess} from '../util.js';
|
||||
import Jimp from "../library/jimp.min";
|
||||
|
||||
import { SPECIAL_LAYER_NAME_TO_ID, SPECIAL_LAYER_NEW_LAYER } from '../util.js';
|
||||
|
||||
function autocrop(jimp) {
|
||||
let minX = jimp.bitmap.width - 1;
|
||||
let minY = jimp.bitmap.height - 1;
|
||||
let maxX = 0;
|
||||
let maxY = 0;
|
||||
|
||||
jimp.scan(0, 0, jimp.bitmap.width, jimp.bitmap.height, function(x, y, idx) {
|
||||
const alpha = this.bitmap.data[idx + 3];
|
||||
if (alpha !== 0) {
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
}
|
||||
});
|
||||
|
||||
const width = maxX - minX + 1;
|
||||
const height = maxY - minY + 1;
|
||||
jimp.crop(minX, minY, width, height);
|
||||
return jimp
|
||||
}
|
||||
|
||||
export default async function sendImages(comfyURL, params) {
|
||||
const imageIds = params.image_ids
|
||||
const layerId = params.layer_id
|
||||
await Promise.all(
|
||||
imageIds.map(async imageId => {
|
||||
let layer;
|
||||
let existingLayerName;
|
||||
let newLayerName;
|
||||
await executeAsModalUntilSuccess(async () => {
|
||||
try {
|
||||
if (layerId && layerId != SPECIAL_LAYER_NAME_TO_ID[SPECIAL_LAYER_NEW_LAYER]) {
|
||||
layer = await app.activeDocument.layers.find(l => l.id == layerId)
|
||||
// deal with multiple images
|
||||
let imageIndexSuffix = ""
|
||||
console.log("imageIds.length: ", imageIds.length)
|
||||
if (imageIds.length > 1){
|
||||
index = imageIds.indexOf(imageId)
|
||||
console.log("imageIds.index: ", index)
|
||||
if (index > 0)
|
||||
imageIndexSuffix = ` ${index}`
|
||||
}
|
||||
if (imageIndexSuffix != "" && layer != null){
|
||||
const layerName = layer?.name;
|
||||
existingLayerName = layerName + imageIndexSuffix
|
||||
console.log("existingLayerName: ", existingLayerName)
|
||||
layer = await app.activeDocument.layers.find(l => l.name == existingLayerName)
|
||||
}
|
||||
}
|
||||
// deal with new layer or id/name not found layer
|
||||
if (!layer) {
|
||||
newLayerName = existingLayerName ?? 'Comfy Images ' + imageId
|
||||
console.log("newLayerName: ", newLayerName)
|
||||
layer = await app.activeDocument.createLayer("pixel", {
|
||||
name: newLayerName
|
||||
})
|
||||
}
|
||||
const jimp = (await Jimp.read(comfyURL + '/finished_images?id=' + imageId))
|
||||
autocrop(jimp)
|
||||
console.log("layer name ", layer.name)
|
||||
let putPixelsOptions = {
|
||||
layerID: layer.id,
|
||||
imageData: await imaging.createImageDataFromBuffer(
|
||||
jimp.bitmap.data,
|
||||
{
|
||||
width: jimp.bitmap.width,
|
||||
height: jimp.bitmap.height,
|
||||
components: 4,
|
||||
colorSpace: "RGB"
|
||||
}
|
||||
),
|
||||
replace: true,
|
||||
}
|
||||
if (!newLayerName) {
|
||||
let bounds = layer.bounds
|
||||
if (bounds.width != jimp.bitmap.width || bounds.height != jimp.bitmap.height) {
|
||||
if (bounds.width <= 1 && bounds.height <= 1) {
|
||||
bounds.left = jimp.bitmap.width / 2
|
||||
bounds.top = jimp.bitmap.height / 2
|
||||
}
|
||||
let centerBounds = {}
|
||||
centerBounds.left = bounds.left + (bounds.width - jimp.bitmap.width) / 2
|
||||
centerBounds.top = bounds.top + (bounds.height - jimp.bitmap.height) / 2
|
||||
centerBounds.right = bounds.left + jimp.bitmap.width
|
||||
centerBounds.bottom = bounds.top + jimp.bitmap.height
|
||||
centerBounds.width = jimp.bitmap.width
|
||||
centerBounds.height = jimp.bitmap.height
|
||||
bounds = centerBounds
|
||||
}
|
||||
putPixelsOptions.targetBounds = bounds
|
||||
}
|
||||
await imaging.putPixels(putPixelsOptions)
|
||||
} catch(e) {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
return {}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { core } from "photoshop";
|
||||
|
||||
export const SPECIAL_LAYER_USE_CANVAS = '### Use Canvas ###'
|
||||
export const SPECIAL_LAYER_USE_SELECTION = '### Use Selection ###'
|
||||
export const SPECIAL_LAYER_NEW_LAYER = '### New Layer ###'
|
||||
export const SPECIAL_LAYER_SAME_AS_LAYER = '### Same as Layer ###'
|
||||
export const SPECIAL_LAYER_NAME_TO_ID = {
|
||||
[SPECIAL_LAYER_USE_CANVAS]: 0,
|
||||
[SPECIAL_LAYER_USE_SELECTION]: -1,
|
||||
[SPECIAL_LAYER_NEW_LAYER]: -2,
|
||||
[SPECIAL_LAYER_SAME_AS_LAYER]: -3
|
||||
}
|
||||
|
||||
export function unTrimImageData(
|
||||
intersectImageDataArray,
|
||||
toImageDataArray,
|
||||
fromImageBounds,
|
||||
toImageBounds
|
||||
) {
|
||||
const fromLeft = fromImageBounds.left;
|
||||
const fromTop = fromImageBounds.top;
|
||||
const fromRight = fromImageBounds.right;
|
||||
const fromBottom = fromImageBounds.bottom;
|
||||
const fromWidth = fromRight - fromLeft;
|
||||
const fromHeight = fromBottom - fromTop;
|
||||
|
||||
const toLeft = toImageBounds.left;
|
||||
const toTop = toImageBounds.top;
|
||||
const toRight = toImageBounds.right;
|
||||
const toBottom = toImageBounds.bottom;
|
||||
const toWidth = toRight - toLeft;
|
||||
const toHeight = toBottom - toTop;
|
||||
|
||||
const intersectLeft = Math.max(fromLeft, toLeft);
|
||||
const intersectTop = Math.max(fromTop, toTop);
|
||||
const intersectRight = Math.min(fromRight, toRight);
|
||||
const intersectBottom = Math.min(fromBottom, toBottom);
|
||||
const intersectWidth = intersectRight - intersectLeft;
|
||||
const intersectHeight = intersectBottom - intersectTop;
|
||||
|
||||
const toLength = toWidth * toHeight * 4;
|
||||
if (toImageDataArray.length !== toLength) {
|
||||
throw new Error(`toImageDataArray.length(${toImageDataArray.length}) !== toLength(${toLength})`);
|
||||
}
|
||||
const intersectLength = intersectWidth * intersectHeight * 4;
|
||||
if (intersectImageDataArray.length !== intersectLength) {
|
||||
throw new Error(`fromImageDataArray.length(${intersectImageDataArray.length}) !== fromLength(${intersectLength})`);
|
||||
}
|
||||
|
||||
for (let i = 0; i < toLength; i += 4) {
|
||||
const currentLeft = (i / 4) % toWidth + toLeft;
|
||||
const currentTop = Math.floor((i / 4) / toWidth) + toTop;
|
||||
if (
|
||||
currentLeft >= fromLeft &&
|
||||
currentLeft < fromRight &&
|
||||
currentTop >= fromTop &&
|
||||
currentTop < fromBottom
|
||||
) {
|
||||
const fromIndex = ((currentTop - intersectTop) * intersectWidth + (currentLeft - intersectLeft)) * 4;
|
||||
const alpha = intersectImageDataArray[fromIndex + 3];
|
||||
toImageDataArray[i] = alpha == 0 ? 0 : intersectImageDataArray[fromIndex];
|
||||
toImageDataArray[i + 1] = alpha == 0 ? 0 : intersectImageDataArray[fromIndex + 1];
|
||||
toImageDataArray[i + 2] = alpha == 0 ? 0 : intersectImageDataArray[fromIndex + 2];
|
||||
toImageDataArray[i + 3] = alpha;
|
||||
}
|
||||
}
|
||||
return toImageDataArray;
|
||||
}
|
||||
|
||||
export function getAllSubLayer(layer, level = 0) {
|
||||
if (!layer?.layers) return [];
|
||||
return layer.layers.reduce((ret, layer) => {
|
||||
ret.push({
|
||||
name: '-'.repeat(level) + layer.name,
|
||||
id: layer.id
|
||||
});
|
||||
return ret.concat(getAllSubLayer(layer, level + 1));
|
||||
}, []);
|
||||
}
|
||||
|
||||
export function findInAllSubLayer(layer, layerid) {
|
||||
if (!layer?.layers) return null;
|
||||
for (let i = 0; i < layer.layers.length; i++) {
|
||||
if (layer.layers[i].id === layerid) return layer.layers[i];
|
||||
|
||||
const result = findInAllSubLayer(layer.layers[i], layerid)
|
||||
if (result) return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const executeAsModal = core.executeAsModal;
|
||||
|
||||
export async function executeAsModalUntilSuccess(...args) {
|
||||
let result;
|
||||
let failed = true;
|
||||
while(failed) {
|
||||
try {
|
||||
result = await executeAsModal(...args);
|
||||
failed = false;
|
||||
} catch (e) {
|
||||
if (e.number != 9) {
|
||||
failed = false; // This case is hit if the targetFunction throws an exception
|
||||
}
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
<<<<<<< HEAD:sdppp_python/comfy_nodes_legacy.py
|
||||
import numpy as np
|
||||
from nodes import LoadImage
|
||||
from PIL import Image
|
||||
@@ -198,3 +199,168 @@ def define_comfyui_nodes_legacy(sdppp):
|
||||
'ImageTimesOpacity': ImageTimesOpacity,
|
||||
'MaskTimesOpacity': MaskTimesOpacity,
|
||||
}
|
||||
=======
|
||||
import numpy as np
|
||||
import time
|
||||
from nodes import LoadImage
|
||||
from .apis import cache_images
|
||||
from .photoshop_manager import PhotoshopManager
|
||||
from server import PromptServer
|
||||
prompt_server = PromptServer.instance
|
||||
|
||||
def define_comfyui_nodes(sdppp):
|
||||
def call_async_func_in_server_thread(coro, dontwait = False):
|
||||
handle = {
|
||||
'done': False,
|
||||
'result': None
|
||||
}
|
||||
loop = sdppp.loop
|
||||
async def do_call():
|
||||
handle['result'] = await coro
|
||||
handle['done'] = True
|
||||
loop.call_soon(lambda: loop.create_task(do_call()))
|
||||
|
||||
if not dontwait:
|
||||
while not handle['done']:
|
||||
pass
|
||||
return handle['result']
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
class GetImageFromPhotoshopLayerNode:
|
||||
# !!!!do not validate because there's no client_id when validating, so we validate manually before execution!!!!
|
||||
# @classmethod
|
||||
# def VALIDATE_INPUTS():
|
||||
# pass
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(self, layer, use_layer_bounds):
|
||||
photoshopInstance = PhotoshopManager.instance().instance_from_client_id(prompt_server.client_id)
|
||||
if not photoshopInstance:
|
||||
return np.random.rand()
|
||||
else:
|
||||
id = photoshopInstance.layer_name_to_id(layer)
|
||||
bounds_id = photoshopInstance.layer_name_to_id(use_layer_bounds, id)
|
||||
start = time.time()
|
||||
is_changed, history_state_id = call_async_func_in_server_thread(photoshopInstance.check_layer_bounds_combo_changed(id, bounds_id))
|
||||
if is_changed and history_state_id is None:
|
||||
return photoshopInstance.update_comfyui_last_value(id, bounds_id, np.random.rand())
|
||||
return photoshopInstance.update_comfyui_last_value(id, bounds_id, history_state_id)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
layer_strs = []
|
||||
bounds_strs = []
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"layer": (layer_strs, {"default": layer_strs[0] if len(layer_strs) > 0 else None}),
|
||||
"use_layer_bounds": (bounds_strs, {"default": bounds_strs[0] if len(bounds_strs) > 0 else None}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK", "FLOAT")
|
||||
RETURN_NAMES = ("image_out", "mask_out", "layer_opacity")
|
||||
FUNCTION = "get_image"
|
||||
CATEGORY = "Photoshop"
|
||||
|
||||
def get_image(self, layer, use_layer_bounds):
|
||||
photoshopInstance = PhotoshopManager.instance().instance_from_client_id(prompt_server.client_id)
|
||||
if not photoshopInstance:
|
||||
raise ValueError('Photoshop is not connected')
|
||||
layer_strs = photoshopInstance.get_base_layers()
|
||||
if layer not in layer_strs:
|
||||
raise ValueError(f"Layer {layer} not found in Photoshop")
|
||||
bounds_strs = photoshopInstance.get_bounds_layers()
|
||||
if use_layer_bounds not in bounds_strs:
|
||||
raise ValueError(f"Layer {use_layer_bounds} not found in Photoshop")
|
||||
|
||||
id = photoshopInstance.layer_name_to_id(layer)
|
||||
bounds_id = photoshopInstance.layer_name_to_id(use_layer_bounds, id)
|
||||
|
||||
start = time.time()
|
||||
image_id, layer_opacity = call_async_func_in_server_thread(photoshopInstance.get_image(layer_id=id, bounds_id=bounds_id))
|
||||
|
||||
loadImage = LoadImage()
|
||||
(output_image, output_mask) = loadImage.load_image(image_id)
|
||||
return (output_image, output_mask, layer_opacity / 100)
|
||||
|
||||
|
||||
class SendImageToPhotoshopLayerNode:
|
||||
# !!!!do not validate because there's no client_id when validating, so we validate manually before execution!!!!
|
||||
# @classmethod
|
||||
# def VALIDATE_INPUTS():
|
||||
# pass
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
layer_strs = []
|
||||
|
||||
return {
|
||||
"required": {},
|
||||
"optional": {
|
||||
"images": ("IMAGE", ),
|
||||
"layer": (layer_strs, {"default": layer_strs[0] if len(layer_strs) > 0 else None}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "send_image"
|
||||
CATEGORY = "Photoshop"
|
||||
OUTPUT_NODE = True
|
||||
|
||||
def send_image(self, images, layer):
|
||||
photoshopInstance = PhotoshopManager.instance().instance_from_client_id(prompt_server.client_id)
|
||||
if not photoshopInstance:
|
||||
raise ValueError('Photoshop is not connected')
|
||||
|
||||
ret = cache_images(images)
|
||||
layer_id = photoshopInstance.layer_name_to_id(layer)
|
||||
call_async_func_in_server_thread(photoshopInstance.send_images(image_ids=ret, layer_id=layer_id), True)
|
||||
|
||||
return (None,)
|
||||
|
||||
class ImageTimesOpacity:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE", ),
|
||||
"opacity": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 1.0, "step": 0.01}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
FUNCTION = "image_times_opacity"
|
||||
CATEGORY = "Photoshop"
|
||||
|
||||
def image_times_opacity(self, images, opacity):
|
||||
image_out = images * opacity
|
||||
return (image_out,)
|
||||
|
||||
class MaskTimesOpacity:
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"masks": ("MASK", ),
|
||||
"opacity": ("FLOAT", {"default": 1.0, "min": 0.01, "max": 1.0, "step": 0.01}),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MASK",)
|
||||
FUNCTION = "mask_times_opacity"
|
||||
CATEGORY = "Photoshop"
|
||||
|
||||
def mask_times_opacity(self, masks, opacity):
|
||||
mask_out = masks * opacity
|
||||
return (mask_out,)
|
||||
|
||||
return {
|
||||
'GetImageFromPhotoshopLayerNode': GetImageFromPhotoshopLayerNode,
|
||||
'SendImageToPhotoshopLayerNode': SendImageToPhotoshopLayerNode,
|
||||
'ImageTimesOpacity': ImageTimesOpacity,
|
||||
'MaskTimesOpacity': MaskTimesOpacity,
|
||||
}
|
||||
>>>>>>> tl/develop_sio_multi_user:comfy/nodes.py
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import asyncio
|
||||
from .photoshop_instance import PhotoshopInstance
|
||||
DEFAULT_ID = 0
|
||||
|
||||
class PhotoshopManager:
|
||||
_instance = None
|
||||
|
||||
@classmethod
|
||||
def instance(cls) -> 'PhotoshopManager':
|
||||
if cls._instance is None:
|
||||
cls._instance = PhotoshopManager()
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
# client id info
|
||||
self.ip_to_client_id_list = {}
|
||||
self.client_id_to_userid = {}
|
||||
# ps instance info
|
||||
self.ip_to_ps_instance_list = {}
|
||||
# final match
|
||||
self.client_id_to_ps_instance = {}
|
||||
|
||||
# get instance from client information, match new instance if not exist
|
||||
def instance_from_client_info(self, ip, client_id, user_id) -> PhotoshopInstance:
|
||||
if not ip: return None
|
||||
if not client_id: user_id = 0
|
||||
if not user_id: user_id = 0
|
||||
instance = self.instance_from_client_id(client_id)
|
||||
if instance is not None:
|
||||
return instance
|
||||
self._add_new_client(ip, client_id, user_id)
|
||||
self._client_match_ps(ip, client_id, user_id)
|
||||
return self.instance_from_client_id(client_id)
|
||||
|
||||
# get instance from client id
|
||||
def instance_from_client_id(self, client_id) -> PhotoshopInstance:
|
||||
return self.client_id_to_ps_instance.get(client_id, None)
|
||||
|
||||
# create new instance and record
|
||||
async def new_ps_instance(self, sdppp, sid, ip, user_id) -> PhotoshopInstance:
|
||||
if not ip: return None
|
||||
if not user_id: user_id = 0
|
||||
# create new instance
|
||||
instance = PhotoshopInstance(sdppp, sid, user_id)
|
||||
instance.on_destroy = self._on_instance_destroy
|
||||
# record new instance
|
||||
await self._add_new_ps_instance(ip, instance)
|
||||
return instance
|
||||
|
||||
#------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
# callback when instance is destroyed and remove from record
|
||||
def _on_instance_destroy(self, instance):
|
||||
self._remove_instance(instance)
|
||||
|
||||
# record new ps instance for matching
|
||||
async def _add_new_ps_instance(self, ip, instance):
|
||||
# find old one and destroy
|
||||
same_ip_list = self.ip_to_ps_instance_list.get(ip, [])
|
||||
for check_instance in same_ip_list:
|
||||
if check_instance.uid == instance.uid:
|
||||
await check_instance.destroy()
|
||||
# record ip to instance
|
||||
ps_instance_list = self.ip_to_ps_instance_list.get(ip, [])
|
||||
if instance not in ps_instance_list:
|
||||
ps_instance_list.append(instance)
|
||||
self.ip_to_ps_instance_list[ip] = ps_instance_list
|
||||
|
||||
# record new client id
|
||||
def _add_new_client(self, ip, client_id, user_id):
|
||||
# record client id, if exist, do nothing
|
||||
client_id_list = self.ip_to_client_id_list.get(ip, [])
|
||||
if client_id in client_id_list:
|
||||
return
|
||||
client_id_list.append(client_id)
|
||||
self.ip_to_client_id_list[ip] = client_id_list
|
||||
# record user_id
|
||||
self.client_id_to_userid[client_id] = user_id
|
||||
|
||||
# match client id to ps instance for prompt use
|
||||
# there are several ways to match:
|
||||
# 1. one to one match(local network most likely): same ip client and ps instance, assign to it
|
||||
# 2. multiple ps instance under the same ip(remote service most likely), check user id
|
||||
def _client_match_ps(self, ip, client_id, user_id):
|
||||
# check existing pair
|
||||
instance = self.client_id_to_ps_instance.get(client_id, None)
|
||||
if instance is not None:
|
||||
return
|
||||
# check same ip ps instance
|
||||
ps_instance_list = self.ip_to_ps_instance_list.get(ip, [])
|
||||
count = len(ps_instance_list)
|
||||
if count <= 0:
|
||||
return
|
||||
# if only one same ip ps instance, assign client id
|
||||
if count == 1:
|
||||
self.client_id_to_ps_instance[client_id] = ps_instance_list[0]
|
||||
return
|
||||
# if more than one same ip ps instance, check same user_id, same ip + same user id = same ps instance
|
||||
ps_user_id_list = [instance.uid for instance in ps_instance_list]
|
||||
try:
|
||||
check_index = ps_user_id_list.index(user_id)
|
||||
except:
|
||||
check_index = None
|
||||
if check_index is None:
|
||||
return
|
||||
self.client_id_to_ps_instance[client_id] = ps_instance_list[check_index]
|
||||
|
||||
def _remove_instance(self, instance):
|
||||
# remove id from ip to instance list
|
||||
for ip, ps_instance_list in self.ip_to_ps_instance_list.items():
|
||||
if instance in ps_instance_list:
|
||||
ps_instance_list.remove(instance)
|
||||
# remove instance from client id to instance
|
||||
for client_id in list(self.client_id_to_ps_instance.keys()):
|
||||
check_instance = self.client_id_to_ps_instance.get(client_id, None)
|
||||
if check_instance == instance:
|
||||
self.client_id_to_ps_instance.pop(client_id, None)
|
||||
Reference in New Issue
Block a user