Clean: WatchPoint only
This commit is contained in:
+31
-7
@@ -1,12 +1,36 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
|
||||
# IDEs
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
debug_logs/
|
||||
|
||||
# Temporal
|
||||
*.tmp
|
||||
*.temp
|
||||
|
||||
# Settings personales (opcional)
|
||||
watchpoint_debug_config.json
|
||||
|
||||
# State files (no subir el JSON con datos)
|
||||
data/list_state.json
|
||||
beta/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
@@ -1,5 +1,5 @@
|
||||
# 👁️ ComfyUI Watch Point
|
||||
|
||||

|
||||
**Dual preview system for ComfyUI workflows**
|
||||
|
||||
Watch Point provides two simultaneous preview methods:
|
||||
@@ -16,20 +16,40 @@ Watch Point provides two simultaneous preview methods:
|
||||
- 📋 Copy to clipboard
|
||||
- ⚙️ Customizable settings
|
||||
- 📡 **Signal Scout Panel**: Display text from your workflow in a side panel.
|
||||
- 🔒 **Thread Safety**: Windows minimize instead of closing to prevent ComfyUI crashes
|
||||
|
||||
### Floating Preview (JavaScript)
|
||||
- 📌 Uses ComfyUI's native preview system
|
||||
- 🎨 Works with existing preview extensions
|
||||
- 🔄 Auto-updates during workflow execution
|
||||
- 💾 Configurable via browser localStorage (no file editing)
|
||||
- ⌨️ Customizable keyboard shortcuts
|
||||
- 🎯 Snap-to-grid positioning
|
||||
- 👻 Adjustable opacity levels
|
||||
|
||||
## Thread Safety & Window Protection
|
||||
|
||||
WatchPoint includes built-in protection against threading issues that can crash ComfyUI:
|
||||
|
||||
### 🛡️ Window Minimization Protection
|
||||
- **Close Button Behavior**: Clicking the Windows close button minimizes the window instead of closing it
|
||||
- **Thread Safety**: Prevents `Tcl_AsyncDelete` errors and threading conflicts
|
||||
- **ComfyUI Stability**: Ensures ComfyUI continues running even if windows are "minimized"
|
||||
- **Window Recovery**: Minimized windows can be restored from the taskbar
|
||||
|
||||
### 🔧 Technical Details
|
||||
- Uses thread-safe Tkinter operations
|
||||
- Implements proper cleanup procedures
|
||||
- Prevents memory leaks and threading deadlocks
|
||||
- Global window management for consistent behavior
|
||||
|
||||
## Installation
|
||||
|
||||
1. Clone into your ComfyUI custom_nodes directory:
|
||||
```bash
|
||||
cd ComfyUI/custom_nodes
|
||||
git clone https://github.com/yourusername/ComfyUI-WatchPoint.git
|
||||
git clone https://github.com/gmorks/ComfyUI-WatchPoint.git
|
||||
```
|
||||
|
||||
2. Install dependencies (if needed):
|
||||
```bash
|
||||
pip install pillow screeninfo
|
||||
@@ -96,6 +116,12 @@ A utility node to restore minimized WatchPoint windows.
|
||||
- **1**: Zoom 1:1 (100% actual size)
|
||||
- **ESC**: Close window
|
||||
|
||||
### Window Close Behavior
|
||||
**Important**: The Windows close button (❌) minimizes the window instead of closing it. This prevents threading errors and keeps ComfyUI stable. To "close" the window:
|
||||
- Click the close button → Window minimizes to taskbar
|
||||
- Use **ESC** key to properly close the window
|
||||
- Right-click taskbar icon → "Close window" for forced closure
|
||||
|
||||
### Toolbar Buttons
|
||||
- **↻ Reset**: Reset zoom and pan
|
||||
- **⊕ Zoom In**: Increase zoom level
|
||||
@@ -151,9 +177,10 @@ except ImportError as e:
|
||||
### Disabling Utility Nodes
|
||||
Comment out or remove the import block in `__init__.py` to disable utility nodes.
|
||||
|
||||
## Configuration File
|
||||
## Configuration Files
|
||||
|
||||
Edit `watchpoint_settings.json` to customize defaults:
|
||||
### Monitor Preview Settings
|
||||
Edit `watchpoint_settings.json` to customize defaults for the external Tkinter window:
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -168,13 +195,149 @@ Edit `watchpoint_settings.json` to customize defaults:
|
||||
}
|
||||
```
|
||||
|
||||
### Settings Explained
|
||||
#### Settings Explained
|
||||
- `window_x`, `window_y`: Window position (null = auto)
|
||||
- `window_size_mode`: "fixed", "Half Vertical", "Half Horizontal", or "Quarter"
|
||||
- `show_toolbar`: Show toolbar on startup
|
||||
- `save_format`: Default save format ("png", "jpeg", "webp")
|
||||
- `jpeg_quality`: JPEG compression quality (10-100)
|
||||
|
||||
### Floating Preview Configuration
|
||||
The floating preview window is configured via browser localStorage. No file editing required!
|
||||
|
||||
#### Quick Start
|
||||
|
||||
**View Current Configuration**
|
||||
|
||||
Open browser console (F12) and run:
|
||||
```javascript
|
||||
WatchPointFloating.getConfig()
|
||||
```
|
||||
|
||||
**Change Configuration**
|
||||
```javascript
|
||||
WatchPointFloating.setConfig({
|
||||
window: {
|
||||
defaultWidth: 500,
|
||||
defaultHeight: 500,
|
||||
defaultPosition: "top-right",
|
||||
defaultOpacity: 80
|
||||
},
|
||||
shortcuts: {
|
||||
toggle: "Ctrl+Alt+KeyP" // Change toggle shortcut
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Reset to Defaults**
|
||||
```javascript
|
||||
WatchPointFloating.resetConfig()
|
||||
```
|
||||
|
||||
**Show Help**
|
||||
```javascript
|
||||
WatchPointFloating.showHelp()
|
||||
```
|
||||
|
||||
**Note**: Reload the page after changing config to apply changes.
|
||||
|
||||
#### Available Configuration Options
|
||||
|
||||
##### Window Settings
|
||||
```javascript
|
||||
{
|
||||
window: {
|
||||
defaultWidth: 320, // Width in pixels
|
||||
defaultHeight: 320, // Height in pixels
|
||||
defaultPosition: "middle-right", // Initial position
|
||||
defaultOpacity: 100 // 0-100 (percentage)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Available Positions**:
|
||||
- `"top-left"`, `"top-center"`, `"top-right"`
|
||||
- `"middle-left"`, `"middle-center"`, `"middle-right"`
|
||||
- `"bottom-left"`, `"bottom-center"`, `"bottom-right"`
|
||||
|
||||
##### Opacity Settings
|
||||
```javascript
|
||||
{
|
||||
opacity: {
|
||||
levels: [20, 40, 60, 80, 100], // Available opacity levels (%)
|
||||
showIndicator: true, // Show opacity % when changing
|
||||
indicatorDuration: 1000 // How long indicator shows (ms)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### UI Settings
|
||||
```javascript
|
||||
{
|
||||
ui: {
|
||||
showHeader: true, // Show title bar
|
||||
showTitle: true, // Show "👁️ Watch Point" title
|
||||
showCloseButton: true, // Show X button
|
||||
dragFromAnywhere: false // Drag from image area (only if header=false)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Keyboard Shortcuts
|
||||
```javascript
|
||||
{
|
||||
shortcuts: {
|
||||
// Main controls
|
||||
toggle: "Ctrl+Alt+KeyW",
|
||||
increaseOpacity: "Ctrl+Alt+Equal", // or Ctrl+Alt++
|
||||
decreaseOpacity: "Ctrl+Alt+Minus", // or Ctrl+Alt+-
|
||||
|
||||
// Arrow navigation (relative)
|
||||
snapUp: "Ctrl+Alt+ArrowUp",
|
||||
snapDown: "Ctrl+Alt+ArrowDown",
|
||||
snapLeft: "Ctrl+Alt+ArrowLeft",
|
||||
snapRight: "Ctrl+Alt+ArrowRight",
|
||||
|
||||
// Numpad navigation (absolute positions)
|
||||
snapToPosition7: "Ctrl+Alt+Numpad7", // Top-left
|
||||
snapToPosition8: "Ctrl+Alt+Numpad8", // Top-center
|
||||
snapToPosition9: "Ctrl+Alt+Numpad9", // Top-right
|
||||
snapToPosition4: "Ctrl+Alt+Numpad4", // Middle-left
|
||||
snapToPosition5: "Ctrl+Alt+Numpad5", // Middle-center
|
||||
snapToPosition6: "Ctrl+Alt+Numpad6", // Middle-right
|
||||
snapToPosition1: "Ctrl+Alt+Numpad1", // Bottom-left
|
||||
snapToPosition2: "Ctrl+Alt+Numpad2", // Bottom-center
|
||||
snapToPosition3: "Ctrl+Alt+Numpad3" // Bottom-right
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Default Keyboard Shortcuts
|
||||
|
||||
| Action | Shortcut | Description |
|
||||
|--------|----------|-------------|
|
||||
| **Toggle window** | `Ctrl+Alt+W` | Show/hide floating preview |
|
||||
| **Increase opacity** | `Ctrl+Alt++` | Make more opaque |
|
||||
| **Decrease opacity** | `Ctrl+Alt+-` | Make more transparent |
|
||||
| **Move up** | `Ctrl+Alt+↑` | Snap to position above |
|
||||
| **Move down** | `Ctrl+Alt+↓` | Snap to position below |
|
||||
| **Move left** | `Ctrl+Alt+←` | Snap to position left |
|
||||
| **Move right** | `Ctrl+Alt+→` | Snap to position right |
|
||||
| **Snap to corner** | `Ctrl+Alt+Numpad[1-9]` | Jump to specific position |
|
||||
|
||||
#### Storage Location
|
||||
|
||||
Configuration is stored in browser localStorage under the key:
|
||||
```
|
||||
watchpoint-floating-config
|
||||
```
|
||||
|
||||
This means:
|
||||
- ✅ Configuration persists between sessions
|
||||
- ✅ Each browser/profile has independent settings
|
||||
- ✅ No server-side storage needed
|
||||
- ⚠️ Clearing browser data will reset to defaults
|
||||
|
||||
## Use Cases
|
||||
|
||||
### Dual Monitor Setup
|
||||
@@ -232,8 +395,22 @@ pip install pywin32
|
||||
|
||||
## Credits
|
||||
|
||||
Created for ComfyUI by [Your Name]
|
||||
Vibe Coded for ComfyUI by gmorks using AI.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - Feel free to use and modify!
|
||||
MIT License - Feel free to use and modify!
|
||||
|
||||
## 🚧 Development Status
|
||||
|
||||
**Note**: This extension is not yet compatible with ComfyUI Nodes 2.0. Support for the new node system is on our to-do list and will be implemented in a future update.
|
||||
|
||||
Stay tuned for updates!
|
||||
|
||||
## Recent Changes
|
||||
|
||||
### v1.0.0 - Initial Release
|
||||
- Dual preview system (Monitor + Floating)
|
||||
- Signal Scout text display
|
||||
- Basic window controls and settings
|
||||
- Multi-monitor support
|
||||
@@ -1,167 +0,0 @@
|
||||
# 🛡️ WatchPoint Conservation Guide
|
||||
|
||||
## 📋 Document Purpose
|
||||
This document serves as a "contra PRD" - a conservation guide for maintaining WatchPoint functionality while allowing future modifications. It outlines what MUST be preserved, what can be modified, and critical implementation details.
|
||||
|
||||
## 🔒 CRITICAL: Must Preserve
|
||||
|
||||
### Core Stability Features
|
||||
1. **Window Minimization Protection**
|
||||
- `safe_close()` function in `watch_point.py`
|
||||
- Prevents accidental window closure from crashing ComfyUI
|
||||
- Uses `iconify()` as primary method, `withdraw()` as fallback
|
||||
- Updates window state tracking (`minimized`, `running` flags)
|
||||
|
||||
2. **Thread Safety**
|
||||
- All Tkinter operations must run on main thread
|
||||
- Use `self.window_manager.master.after()` for thread-safe GUI updates
|
||||
- Prevents `Tcl_AsyncDelete` crashes
|
||||
|
||||
3. **Debug Mode Persistence**
|
||||
- `watchpoint_debug_config.json` file-based configuration
|
||||
- `load_debug_config()` and `save_debug_config()` functions
|
||||
- Debug state survives ComfyUI restarts
|
||||
|
||||
4. **Recursion Prevention**
|
||||
- `_saving_dump` flag in `save_debug_dump()`
|
||||
- Prevents infinite loops during debug dump saving
|
||||
|
||||
### File Structure
|
||||
```
|
||||
ComfyUI-WatchPoint/
|
||||
├── watch_point.py # Core functionality (preserve stability features)
|
||||
├── __init__.py # Optional utility imports (modify carefully)
|
||||
├── watchpoint_debug_config.json # Debug persistence (auto-generated)
|
||||
├── watchpoint_settings.json # User settings (auto-generated)
|
||||
├── nodes/
|
||||
│ ├── __init__.py # Node package init
|
||||
│ ├── list_cycler.py # Template for utility nodes
|
||||
│ └── watchpoint_utils.py # Optional utility nodes (safe to modify)
|
||||
└── WATCHPOINT_CONSERVATION.md # This file
|
||||
```
|
||||
|
||||
## 📝 Implementation Rules
|
||||
|
||||
### When Adding New Features
|
||||
1. **Utility Nodes**: Add to `nodes/watchpoint_utils.py`, not `watch_point.py`
|
||||
2. **Core Modifications**: Only modify `watch_point.py` for critical fixes
|
||||
3. **Optional Imports**: Use try/except blocks in `__init__.py`
|
||||
4. **State Management**: Use file-based persistence for critical state
|
||||
|
||||
### Window Management
|
||||
```python
|
||||
# Correct pattern for window operations
|
||||
def safe_window_operation(self, display_idx, operation):
|
||||
try:
|
||||
self.window_manager.master.after(0, lambda: self._perform_operation(display_idx, operation))
|
||||
except Exception as e:
|
||||
wp_logger.warning(f"Window operation failed: {e}", "WindowManager")
|
||||
```
|
||||
|
||||
### Debug Mode Handling
|
||||
```python
|
||||
# Always check debug state before operations
|
||||
if self.debug_mode or load_debug_config().get("debug_enabled", False):
|
||||
# Perform debug operations
|
||||
pass
|
||||
```
|
||||
|
||||
## 🔄 Utility Node Management
|
||||
|
||||
### Enabling/Disabling Utilities
|
||||
Located in `__init__.py`:
|
||||
```python
|
||||
try:
|
||||
from .nodes.watchpoint_utils import NODE_CLASS_MAPPINGS as UTILS_CLASS, NODE_DISPLAY_NAME_MAPPINGS as UTILS_DISPLAY
|
||||
NODE_CLASS_MAPPINGS.update(UTILS_CLASS)
|
||||
NODE_DISPLAY_NAME_MAPPINGS.update(UTILS_DISPLAY)
|
||||
print("WatchPoint Utils loaded: Debug Toggle and Restore Window available")
|
||||
except ImportError as e:
|
||||
print(f"WatchPoint Utils not available: {e}")
|
||||
pass
|
||||
```
|
||||
|
||||
### Safe Modifications
|
||||
- ✅ Add new utility nodes to `watchpoint_utils.py`
|
||||
- ✅ Modify utility node functionality
|
||||
- ✅ Add new optional features
|
||||
- ❌ Remove core stability features from `watch_point.py`
|
||||
- ❌ Modify thread safety mechanisms
|
||||
- ❌ Remove file-based state management
|
||||
|
||||
## 🚨 Common Pitfalls to Avoid
|
||||
|
||||
1. **Don't Create Direct Tkinter Windows**
|
||||
```python
|
||||
# ❌ WRONG - Causes thread issues
|
||||
window = tk.Toplevel()
|
||||
|
||||
# ✅ CORRECT - Use existing window manager
|
||||
self.window_manager.create_window(...)
|
||||
```
|
||||
|
||||
2. **Don't Skip Error Handling**
|
||||
```python
|
||||
# ❌ WRONG - No error handling
|
||||
root.protocol("WM_DELETE_WINDOW", root.iconify)
|
||||
|
||||
# ✅ CORRECT - With error handling
|
||||
def safe_close():
|
||||
try:
|
||||
root.iconify()
|
||||
except:
|
||||
root.withdraw()
|
||||
root.protocol("WM_DELETE_WINDOW", safe_close)
|
||||
```
|
||||
|
||||
3. **Don't Forget State Persistence**
|
||||
```python
|
||||
# ❌ WRONG - State lost on restart
|
||||
self.debug_mode = True
|
||||
|
||||
# ✅ CORRECT - Persistent state
|
||||
save_debug_config({"debug_enabled": True})
|
||||
```
|
||||
|
||||
## 🔧 Testing Checklist
|
||||
|
||||
Before committing changes:
|
||||
- [ ] Test window minimization (click X button)
|
||||
- [ ] Test debug mode persistence (restart ComfyUI)
|
||||
- [ ] Test with utility nodes enabled/disabled
|
||||
- [ ] Test multi-monitor setup
|
||||
- [ ] Check for `Tcl_AsyncDelete` errors
|
||||
- [ ] Verify no infinite recursion in debug dumps
|
||||
|
||||
## 📚 Key Functions Reference
|
||||
|
||||
### Window Management
|
||||
- `safe_close()` - Window close protection
|
||||
- `window_manager.create_window()` - Safe window creation
|
||||
- `window_manager.restore_window()` - Window restoration
|
||||
|
||||
### Debug System
|
||||
- `load_debug_config()` - Load persistent debug state
|
||||
- `save_debug_config()` - Save debug state
|
||||
- `save_debug_dump()` - Safe debug dump (with recursion protection)
|
||||
|
||||
### State Management
|
||||
- File: `watchpoint_debug_config.json`
|
||||
- Keys: `debug_enabled`, `debug_data`
|
||||
- Auto-created on first use
|
||||
|
||||
## 🎯 Philosophy
|
||||
|
||||
**Conservative Approach**: This project prioritizes stability over features. Every new feature must:
|
||||
1. Not compromise core stability
|
||||
2. Be optional when possible
|
||||
3. Include proper error handling
|
||||
4. Maintain backward compatibility
|
||||
|
||||
**Modular Design**: Separate core functionality from utilities to allow safe experimentation without risking core stability.
|
||||
|
||||
---
|
||||
|
||||
*Last Updated: 2026-01-23*
|
||||
*Version: 1.0*
|
||||
*Purpose: Preserve WatchPoint stability while enabling safe evolution*
|
||||
+2
-3
@@ -1,9 +1,8 @@
|
||||
from .watch_point import NODE_CLASS_MAPPINGS as WP_CLASS, NODE_DISPLAY_NAME_MAPPINGS as WP_DISPLAY, cleanup_all_watchpoints
|
||||
from .nodes.list_cycler import NODE_CLASS_MAPPINGS as LC_CLASS, NODE_DISPLAY_NAME_MAPPINGS as LC_DISPLAY
|
||||
|
||||
# Combinamos lo básico
|
||||
NODE_CLASS_MAPPINGS = {**WP_CLASS, **LC_CLASS}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {**WP_DISPLAY, **LC_DISPLAY}
|
||||
NODE_CLASS_MAPPINGS = {**WP_CLASS}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {**WP_DISPLAY}
|
||||
|
||||
# Importar nodos utils de forma opcional (pueden ser desactivados si no se necesitan)
|
||||
try:
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,199 @@
|
||||
{
|
||||
"id": "d744fb0e-f3c7-4705-9960-73b30103a194",
|
||||
"revision": 0,
|
||||
"last_node_id": 6,
|
||||
"last_link_id": 7,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 1,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
17300,
|
||||
-9670
|
||||
],
|
||||
"size": [
|
||||
222.83333587646484,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.10.0",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"example.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"type": "PrimitiveString",
|
||||
"pos": [
|
||||
17100,
|
||||
-9270
|
||||
],
|
||||
"size": [
|
||||
500,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "STRING",
|
||||
"type": "STRING",
|
||||
"links": [
|
||||
5
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.10.0",
|
||||
"Node name for S&R": "PrimitiveString"
|
||||
},
|
||||
"widgets_values": [
|
||||
"\"opt_signal_text\" can be used to send any string, to the tkinter window"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"type": "WatchPoint",
|
||||
"pos": [
|
||||
17600,
|
||||
-9670
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
338
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 4
|
||||
},
|
||||
{
|
||||
"name": "opt_signal_text",
|
||||
"shape": 7,
|
||||
"type": "STRING",
|
||||
"link": 5
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
7
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "gmorks/ComfyUI-WatchPoint",
|
||||
"ver": "9c383314405e0767edb67ac208b87b4bac7a2448",
|
||||
"Node name for S&R": "WatchPoint"
|
||||
},
|
||||
"widgets_values": [
|
||||
true,
|
||||
true,
|
||||
"Monitor 0 (1920x1080)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
17900,
|
||||
-9670
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 7
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.10.0",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
4,
|
||||
1,
|
||||
0,
|
||||
5,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
5,
|
||||
4,
|
||||
0,
|
||||
5,
|
||||
1,
|
||||
"STRING"
|
||||
],
|
||||
[
|
||||
7,
|
||||
5,
|
||||
0,
|
||||
6,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.725,
|
||||
"offset": [
|
||||
-16829.321120689645,
|
||||
9914.394612068972
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.37.11",
|
||||
"VHS_latentpreview": false,
|
||||
"VHS_latentpreviewrate": 0,
|
||||
"VHS_MetadataImage": true,
|
||||
"VHS_KeepIntermediate": true
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
+106
-9
@@ -1,6 +1,7 @@
|
||||
// watchpoint_floating.js
|
||||
// Watch Point Floating Preview Integration
|
||||
// Only captures images from Watch Point nodes
|
||||
// Configuration via localStorage
|
||||
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
@@ -64,21 +65,58 @@ const DEFAULT_CONFIG = {
|
||||
}
|
||||
};
|
||||
|
||||
// Configuration loader
|
||||
async function loadConfig() {
|
||||
// Configuration loader with localStorage support
|
||||
function loadConfig() {
|
||||
try {
|
||||
const response = await fetch('/extensions/ComfyUI-WatchPoint/watchpoint_config.json');
|
||||
if (response.ok) {
|
||||
const userConfig = await response.json();
|
||||
console.log("✅ Watch Point Floating: Loaded user config");
|
||||
return { ...DEFAULT_CONFIG, ...userConfig };
|
||||
const saved = localStorage.getItem('watchpoint-floating-config');
|
||||
if (saved) {
|
||||
const userConfig = JSON.parse(saved);
|
||||
console.log("✅ Watch Point Floating: Loaded config from localStorage");
|
||||
// Deep merge user config with defaults
|
||||
return deepMerge(DEFAULT_CONFIG, userConfig);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("ℹ️ Watch Point Floating: No custom config found, using defaults");
|
||||
console.warn("⚠️ Watch Point Floating: Error loading localStorage config:", e);
|
||||
}
|
||||
|
||||
console.log("ℹ️ Watch Point Floating: Using default config");
|
||||
return DEFAULT_CONFIG;
|
||||
}
|
||||
|
||||
// Save configuration to localStorage
|
||||
function saveConfig(config) {
|
||||
try {
|
||||
localStorage.setItem('watchpoint-floating-config', JSON.stringify(config));
|
||||
console.log("✅ Watch Point Floating: Config saved to localStorage");
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("❌ Watch Point Floating: Could not save config:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Deep merge helper
|
||||
function deepMerge(target, source) {
|
||||
const output = Object.assign({}, target);
|
||||
if (isObject(target) && isObject(source)) {
|
||||
Object.keys(source).forEach(key => {
|
||||
if (isObject(source[key])) {
|
||||
if (!(key in target))
|
||||
Object.assign(output, { [key]: source[key] });
|
||||
else
|
||||
output[key] = deepMerge(target[key], source[key]);
|
||||
} else {
|
||||
Object.assign(output, { [key]: source[key] });
|
||||
}
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function isObject(item) {
|
||||
return item && typeof item === 'object' && !Array.isArray(item);
|
||||
}
|
||||
|
||||
// Main extension
|
||||
app.registerExtension({
|
||||
name: "WatchPoint.FloatingPreview",
|
||||
@@ -87,7 +125,7 @@ app.registerExtension({
|
||||
console.log("👁️ Watch Point Floating: Initializing...");
|
||||
|
||||
// Load configuration
|
||||
const config = await loadConfig();
|
||||
const config = loadConfig();
|
||||
|
||||
// Create the floating preview window
|
||||
const pip = new WatchPointFloatingWindow(config);
|
||||
@@ -213,6 +251,65 @@ app.registerExtension({
|
||||
console.log(" - Toggle:", config.shortcuts.toggle);
|
||||
console.log(" - Opacity: Ctrl+Alt+[+/-]");
|
||||
console.log(" - Position: Ctrl+Alt+[Arrows] or Ctrl+Alt+[Numpad1-9]");
|
||||
|
||||
// Expose global API for configuration
|
||||
window.WatchPointFloating = {
|
||||
getConfig: () => loadConfig(),
|
||||
setConfig: (newConfig) => {
|
||||
const merged = deepMerge(DEFAULT_CONFIG, newConfig);
|
||||
if (saveConfig(merged)) {
|
||||
console.log("✅ Configuration saved. Reload page to apply changes.");
|
||||
return merged;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
resetConfig: () => {
|
||||
try {
|
||||
localStorage.removeItem('watchpoint-floating-config');
|
||||
console.log("✅ Watch Point Floating: Config reset to defaults. Reload page to apply.");
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("❌ Could not reset config:", e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
showHelp: () => {
|
||||
console.log(`
|
||||
🎨 Watch Point Floating Configuration Help
|
||||
==========================================
|
||||
|
||||
View current config:
|
||||
WatchPointFloating.getConfig()
|
||||
|
||||
Change settings:
|
||||
WatchPointFloating.setConfig({
|
||||
window: {
|
||||
defaultWidth: 500,
|
||||
defaultHeight: 500,
|
||||
defaultPosition: "top-right",
|
||||
defaultOpacity: 80
|
||||
},
|
||||
shortcuts: {
|
||||
toggle: "Ctrl+Alt+KeyP"
|
||||
}
|
||||
})
|
||||
|
||||
Reset to defaults:
|
||||
WatchPointFloating.resetConfig()
|
||||
|
||||
Available positions:
|
||||
top-left, top-center, top-right
|
||||
middle-left, middle-center, middle-right
|
||||
bottom-left, bottom-center, bottom-right
|
||||
|
||||
Note: Reload page after changing config to apply changes.
|
||||
`);
|
||||
},
|
||||
instance: pip
|
||||
};
|
||||
|
||||
console.log("🔧 Watch Point Floating: Global API exposed as window.WatchPointFloating");
|
||||
console.log(" Run WatchPointFloating.showHelp() for configuration help");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
app.registerExtension({
|
||||
name: "WatchPoint.ListCyclerButtons",
|
||||
async beforeRegisterNodeDef(nodeType, nodeData, app) {
|
||||
if (nodeData.name === "WPSmartListCycler") {
|
||||
|
||||
const onNodeCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function() {
|
||||
onNodeCreated?.apply(this, arguments);
|
||||
|
||||
// Crear el botón de Reset
|
||||
const btn = this.addWidget("button", "RESET STATE", "reset", () => {
|
||||
const node_id = this.id;
|
||||
|
||||
// Llamar a nuestra API personalizada en el servidor
|
||||
api.fetchApi("/wp/list_cycler/reset", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ node_id }),
|
||||
}).then(response => {
|
||||
if (response.ok) {
|
||||
if (DEBUG) {
|
||||
//alert(`Nodo ${node_id}: Estado del JSON limpiado. En la próxima ejecución se regenerará la lista.`);
|
||||
this.color = "#225522"; // Feedback visual momentáneo
|
||||
setTimeout(() => { this.color = ""; }, 1000);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
btn.serialize = false; // No necesitamos guardar el estado del botón
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 3.2 MiB |
@@ -1,115 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
from server import PromptServer
|
||||
from aiohttp import web
|
||||
|
||||
DEBUG = True
|
||||
|
||||
class WPSmartListCycler:
|
||||
def __init__(self):
|
||||
self.data_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "list_state.json")
|
||||
os.makedirs(os.path.dirname(self.data_path), exist_ok=True)
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"list_string": ("STRING", {"default": "0,1,2,3,4,5"}),
|
||||
"mode": (["increment-wrap", "random-shuffled", "fixed"],),
|
||||
"fixed_index": ("INT", {"default": 0, "min": 0, "max": 1000}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
},
|
||||
"hidden": {"unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT", "STRING")
|
||||
RETURN_NAMES = ("index", "index_as_string")
|
||||
FUNCTION = "get_next_value"
|
||||
CATEGORY = "WatchPoint/Logic"
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(s, **kwargs):
|
||||
"""
|
||||
Forzamos a ComfyUI a que ignore la caché de este nodo devolviendo
|
||||
un valor distinto en cada consulta.
|
||||
"""
|
||||
return random.random()
|
||||
|
||||
def load_state(self):
|
||||
try:
|
||||
if os.path.exists(self.data_path):
|
||||
with open(self.data_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
except Exception: pass
|
||||
return {}
|
||||
|
||||
def save_state(self, state):
|
||||
try:
|
||||
with open(self.data_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(state, f, indent=4)
|
||||
except Exception as e:
|
||||
if DEBUG:
|
||||
print(f"WP_Error: Fallo al guardar JSON: {e}")
|
||||
|
||||
def get_next_value(self, list_string, mode, fixed_index, seed, unique_id):
|
||||
# 1. Preparar items
|
||||
items = [int(x.strip()) for x in list_string.split(",") if x.strip()] or [0]
|
||||
state = self.load_state()
|
||||
|
||||
if unique_id not in state:
|
||||
state[unique_id] = {"last_idx": -1, "shuffled_list": []}
|
||||
|
||||
node_state = state[unique_id]
|
||||
out_value = items[0]
|
||||
|
||||
# 2. Lógica de selección
|
||||
if mode == "fixed":
|
||||
idx = fixed_index % len(items)
|
||||
out_value = items[idx]
|
||||
node_state["last_idx"] = idx
|
||||
|
||||
elif mode == "increment-wrap":
|
||||
new_idx = (node_state.get("last_idx", -1) + 1) % len(items)
|
||||
out_value = items[new_idx]
|
||||
node_state["last_idx"] = new_idx
|
||||
|
||||
elif mode == "random-shuffled":
|
||||
shuffled = node_state.get("shuffled_list", [])
|
||||
|
||||
# Regenerar bolsa si está vacía o el tamaño de la lista cambió
|
||||
if DEBUG:
|
||||
print(f"WP_Log: Nodo {unique_id} estado actual: {node_state}")
|
||||
if not shuffled or max(shuffled, default=-1) >= len(items):
|
||||
shuffled = list(range(len(items)))
|
||||
random.shuffle(shuffled)
|
||||
if DEBUG:
|
||||
print(f"WP_Log: Nodo {unique_id} barajando nueva lista: {shuffled}" )
|
||||
|
||||
idx_to_use = shuffled.pop(0)
|
||||
out_value = items[idx_to_use]
|
||||
node_state["shuffled_list"] = shuffled
|
||||
node_state["last_idx"] = idx_to_use
|
||||
|
||||
# 3. Guardado inmediato
|
||||
state[unique_id] = node_state
|
||||
self.save_state(state)
|
||||
if DEBUG:
|
||||
print(f"WP_Log: Nodo {unique_id} ejecutado. Resultado: {out_value}")
|
||||
return (int(out_value), str(out_value))
|
||||
|
||||
# --- API ENDPOINT (Mismo de antes) ---
|
||||
@PromptServer.instance.routes.post("/wp/list_cycler/reset")
|
||||
async def reset_node_state(request):
|
||||
json_data = await request.json()
|
||||
node_id = str(json_data.get("node_id"))
|
||||
temp_instance = WPSmartListCycler()
|
||||
state = temp_instance.load_state()
|
||||
if node_id in state:
|
||||
del state[node_id]
|
||||
temp_instance.save_state(state)
|
||||
return web.json_response({"status": "success"})
|
||||
return web.json_response({"status": "not_found"})
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"WPSmartListCycler": WPSmartListCycler}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"WPSmartListCycler": "🔄 Smart List Cycler (WP)"}
|
||||
+51
-26
@@ -1,6 +1,6 @@
|
||||
import os
|
||||
import json
|
||||
import sys # NUEVO: Importar sys para debug dumps
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox
|
||||
from PIL import Image, ImageTk
|
||||
@@ -55,11 +55,11 @@ class WatchPointLogger:
|
||||
self.log_level = "INFO" # DEBUG, INFO, WARNING, ERROR
|
||||
self.logs = []
|
||||
self.max_logs = 100
|
||||
self.debug_mode = False # NUEVO: Modo debug
|
||||
self.debug_log_dir = "debug_logs" # NUEVO: Directorio para logs de debug
|
||||
self.debug_session_id = None # NUEVO: ID única para sesión de debug
|
||||
self.debug_mode = False
|
||||
self.debug_log_dir = "debug_logs"
|
||||
self.debug_session_id = None
|
||||
self.config_file = os.path.join(os.path.dirname(__file__), "watchpoint_debug_config.json")
|
||||
self.load_debug_config() # Cargar configuración persistente
|
||||
self.load_debug_config()
|
||||
|
||||
def log(self, level, message, component="WatchPoint"):
|
||||
"""Log un mensaje con nivel y componente"""
|
||||
@@ -124,13 +124,13 @@ class WatchPointLogger:
|
||||
self.debug_mode = enabled
|
||||
if enabled:
|
||||
self.debug_session_id = time.strftime("%Y%m%d_%H%M%S")
|
||||
# Crear directorio de debug si no existe
|
||||
|
||||
os.makedirs(self.debug_log_dir, exist_ok=True)
|
||||
self.info(f"Modo debug activado - Sesión: {self.debug_session_id}", "Debug")
|
||||
else:
|
||||
self.info("Modo debug desactivado", "Debug")
|
||||
|
||||
# Guardar configuración persistente
|
||||
|
||||
self.save_debug_config()
|
||||
|
||||
def load_debug_config(self):
|
||||
@@ -183,16 +183,16 @@ class WatchPointLogger:
|
||||
}
|
||||
}
|
||||
|
||||
# Agregar stats del WatchPoint si está disponible
|
||||
|
||||
if watch_point_instance and hasattr(watch_point_instance, 'get_health_stats'):
|
||||
debug_data["health_stats"] = watch_point_instance.get_health_stats()
|
||||
|
||||
# Agregar info de threads SI ESTÁ DISPONIBLE Y NO ESTAMOS EN RECURSIÓN
|
||||
|
||||
if watch_point_instance and hasattr(watch_point_instance, 'debug_threads'):
|
||||
# Llamar directamente a la función sin pasar por el logger para evitar recursión
|
||||
debug_data["thread_info"] = watch_point_instance.debug_threads()
|
||||
|
||||
# Agregar logs recientes
|
||||
|
||||
debug_data["recent_logs"] = self.get_logs()[-50:] # Últimos 50 logs
|
||||
|
||||
# Guardar a archivo
|
||||
@@ -207,7 +207,7 @@ class WatchPointLogger:
|
||||
self.error(f"Error guardando debug dump: {e}", "Debug")
|
||||
return None
|
||||
finally:
|
||||
# Limpiar bandera de recursión
|
||||
# Clear recursion flag
|
||||
self._saving_dump = False
|
||||
|
||||
# Crear logger global
|
||||
@@ -283,7 +283,7 @@ class WindowManager:
|
||||
os.path.join(os.path.dirname(__file__), "watchpoint_settings.json")
|
||||
)
|
||||
self.shutdown_event = threading.Event()
|
||||
self._start_time = time.time() # Tiempo de inicio para estadísticas
|
||||
self._start_time = time.time() # Start time for statistics
|
||||
self.initialized = True
|
||||
|
||||
# Iniciar watchdog
|
||||
@@ -293,23 +293,53 @@ class WindowManager:
|
||||
wp_logger.info("WindowManager inicializado", "WindowManager")
|
||||
|
||||
def show_image(self, display_idx, pil_img, text=None):
|
||||
"""Creates or updates a window to display an image and optional text."""
|
||||
if display_idx in self.windows:
|
||||
win_data = self.windows[display_idx]
|
||||
"""Creates or updates THE SINGLE global window to display an image and optional text."""
|
||||
|
||||
# Check if any window exists, reuse it
|
||||
if len(self.windows) > 0:
|
||||
# Obtener el índice de la ventana existente (solo debería haber 1)
|
||||
existing_idx = list(self.windows.keys())[0]
|
||||
win_data = self.windows[existing_idx]
|
||||
|
||||
# Verificar que la ventana esté corriendo
|
||||
if not win_data.get("running", False):
|
||||
self._cleanup_window(display_idx)
|
||||
# Window exists but is dead, clean up
|
||||
wp_logger.warning(f"Ventana {existing_idx} existe pero no está running, limpiando", "ShowImage")
|
||||
self._cleanup_window(existing_idx)
|
||||
# Continuar a creación de nueva ventana (abajo)
|
||||
else:
|
||||
# Window is alive, REUSE
|
||||
wp_logger.debug(f"Reutilizando ventana existente {existing_idx} para nueva imagen", "ShowImage")
|
||||
|
||||
# Actualizar imagen
|
||||
with win_data["lock"]:
|
||||
win_data["image"] = pil_img
|
||||
|
||||
# Actualizar texto si existe
|
||||
if text and win_data.get("instance"):
|
||||
win_data["instance"].update_signal_text(text)
|
||||
|
||||
# Update title if monitor changed
|
||||
if existing_idx != display_idx:
|
||||
wp_logger.info(f"Monitor cambiado de {existing_idx} a {display_idx} (solo actualiza título)", "ShowImage")
|
||||
instance = win_data.get("instance")
|
||||
if instance and hasattr(instance, 'root'):
|
||||
try:
|
||||
# Update simplified title
|
||||
instance.root.title("Watch Point")
|
||||
except Exception as e:
|
||||
wp_logger.warning(f"No se pudo actualizar título: {e}", "ShowImage")
|
||||
|
||||
return
|
||||
|
||||
|
||||
# If we reach here, NO window exists, create a new one
|
||||
wp_logger.info(f"Creando nueva ventana global en Monitor {display_idx}", "ShowImage")
|
||||
|
||||
lock = Lock()
|
||||
win_data = {
|
||||
"image": pil_img, "lock": lock, "running": True,
|
||||
"instance": None, "pending_text": text,
|
||||
"minimized": False # NUEVO: Estado de minimización
|
||||
"minimized": False
|
||||
}
|
||||
self.windows[display_idx] = win_data
|
||||
|
||||
@@ -324,7 +354,7 @@ class WindowManager:
|
||||
self.windows[display_idx]["closing"] = True
|
||||
self.windows[display_idx]["close_started"] = time.time()
|
||||
|
||||
# MEJORADO: Esperar hasta 3 segundos con timeout
|
||||
|
||||
try:
|
||||
thread = self.windows[display_idx].get("thread")
|
||||
if thread and thread.is_alive():
|
||||
@@ -349,9 +379,9 @@ class WindowManager:
|
||||
root = None
|
||||
win_instance = None
|
||||
try:
|
||||
# === CÓDIGO EXISTENTE SE MANTIENE IGUAL ===
|
||||
# Create main window
|
||||
root = tk.Tk()
|
||||
root.title(f"Watch Point - Monitor {display_idx}")
|
||||
root.title("Watch Point")
|
||||
|
||||
self._apply_icon(root)
|
||||
self._apply_geometry(root, display_idx)
|
||||
@@ -685,11 +715,6 @@ class WatchPoint:
|
||||
except (ValueError, IndexError):
|
||||
display_idx = 0
|
||||
|
||||
# Close the previous window if the monitor has changed
|
||||
if self.last_display_idx != -1 and self.last_display_idx != display_idx:
|
||||
wp_logger.info(f"Cambiando de monitor {self.last_display_idx} a {display_idx}", "WatchPoint")
|
||||
self.window_manager.hide_window(self.last_display_idx)
|
||||
|
||||
self.last_display_idx = display_idx
|
||||
|
||||
if monitor_preview:
|
||||
|
||||
Reference in New Issue
Block a user