The root cause was a coordinate space mismatch between the floating panel
canvas and the Python backend:
Before (broken):
- Canvas mask window: fixed ~300px (arbitrary, unrelated to actual mask)
- Preview image: drawn at downscaled pixel size (e.g. 768x512)
- offsetX/Y: interpreted in preview image pixel space
- Python output: interprets offsetX/Y in ORIGINAL image pixel space
- Result: 100px offset in preview = 200px in output (2x mismatch at 50% scale)
After (fixed):
- Python sends orig_h/w (original image dims) + bbox_h/w (mask bbox) via mce_info
- Canvas computes zoom-to-fit so the full original image is visible
- Everything rendered in ORIGINAL image pixel space (zoomed for display)
- Mask window size = bbox_w * zoom × bbox_h * zoom (accurate proportional size)
- DrawImage stretches the preview to fill original dimensions (consistent coords)
- Mouse drag sensitivity = 1 / (zoom * scale) for correct image-pixel mapping
- offsetX/Y values in JS == same values applied by Python (no conversion needed)
This ensures WYSIWYG: what you see in the mask window IS what you get in output.
- Node body is now small with only a button widget
- Click button opens a floating DOM panel with full interactive canvas
- Panel: header (flip H/V, reset, close) + canvas + sidebar (sliders) + footer
- Canvas shows actual input image with transforms applied in real-time
- Mouse drag to pan, scroll to zoom, shift+scroll to rotate
- Apply button writes state back to hidden bridge widget and re-executes
- onDrawForeground shows a thumbnail preview on the compact node
- Move onDrawForeground, onMouseDown, onMouseMove, onMouseUp to
beforeRegisterNodeDef prototype pattern (not instance-level)
- Use __mce_ prefixed properties to avoid name collisions
- Move toolbar/grid drawing to standalone functions for clarity
- Ensure setDirtyCanvas fires after all state changes
- Fallback text 'Queue workflow to load preview' when no image loaded
- Preserve image preview across redraws via __mce_img on instance
Python:
- Save input image preview to ComfyUI temp dir after execution
- Use unique_id to generate stable filename per node instance
- Return UI dict with preview filename for frontend consumption
JS:
- Listen for onExecuted to receive preview image filename
- Load actual image from ComfyUI /view API
- Draw image centered at transforms origin — all transforms
(scale, rotation, flip, offset) apply naturally to the image
- Fall back to checkerboard grid when no preview loaded yet
- Fixed corner marker drawing bug
- Python: remove visible FLOAT/INT/BOOLEAN param widgets, add hidden transform_state JSON input
- JS: complete rewrite — node body IS the interactive canvas, no modal, no sliders
- Drag to pan, scroll to zoom, shift+scroll to rotate
- Toolbar with flip/reset buttons drawn directly on node
- Hidden bridge widget for state serialization
- Global wheel listener for reliable zoom/rotate across all ComfyUI versions
- README: update docs to reflect purely graphical design