Files
rubenvillarreal cf264d7018 Simplify PoseAlign node to manual-only mode
- Remove all automatic alignment logic and keypoint detection
- Convert to pure manual transformation node using widget sliders
- Remove manual/auto mode toggle and Live UI toggle
- Ensure canvas preview exactly matches output transformations
- Simplify codebase by removing 400+ lines of complex auto-alignment code

Key changes:
- Python: Always use widget values (tx_A, ty_A, scale_A, angle_deg_A, etc.)
- JavaScript: Remove automatic mode detection and Live toggle UI
- Canvas interactions directly update widget values for immediate effect
- Output transformations now perfectly match canvas preview

Fixes issue where manual canvas adjustments weren't reflected in final output.
2025-08-02 09:34:43 +00:00

379 lines
14 KiB
JavaScript

/* modules/canvas_renderer.js - Fixed dimension handling for poses A and B */
export class CanvasRenderer {
constructor(canvas, ctx, imageManager, transformManager) {
this.canvas = canvas;
this.ctx = ctx;
this.imageManager = imageManager;
this.transformManager = transformManager;
this.state = null;
}
setState(state) {
this.state = state;
}
// Calculate proper coordinate system for drawing
calculateCoordinateSystem() {
const actualCanvasWidth = this.canvas.width;
const actualCanvasHeight = this.canvas.height;
const refSize = this.imageManager.getReferenceImageSize();
const refW = refSize.width;
const refH = refSize.height;
const canvasAspect = actualCanvasWidth / actualCanvasHeight;
const refAspect = refW / refH;
let canvasScale, offsetX, offsetY;
if (refAspect > canvasAspect) {
canvasScale = actualCanvasWidth / refW * 0.9;
offsetX = actualCanvasWidth * 0.05;
offsetY = (actualCanvasHeight - refH * canvasScale) / 2;
} else {
canvasScale = actualCanvasHeight / refH * 0.9;
offsetY = actualCanvasHeight * 0.05;
offsetX = (actualCanvasWidth - refW * canvasScale) / 2;
}
return { canvasScale, offsetX, offsetY, refW, refH, actualCanvasWidth, actualCanvasHeight };
}
// Draw grid
drawGrid(coordSys) {
const { canvasScale, offsetX, offsetY, actualCanvasWidth, actualCanvasHeight } = coordSys;
this.ctx.strokeStyle = "#333";
this.ctx.lineWidth = 1;
this.ctx.setLineDash([2, 4]);
const gridStep = 32 * canvasScale;
for (let i = offsetX; i < actualCanvasWidth; i += gridStep) {
this.ctx.beginPath();
this.ctx.moveTo(i, 0);
this.ctx.lineTo(i, actualCanvasHeight);
this.ctx.stroke();
}
for (let i = offsetY; i < actualCanvasHeight; i += gridStep) {
this.ctx.beginPath();
this.ctx.moveTo(0, i);
this.ctx.lineTo(actualCanvasWidth, i);
this.ctx.stroke();
}
this.ctx.setLineDash([]);
}
// Generate placeholder
generatePoseVisualization(type, width = 384, height = 384) {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#1a1a1a';
ctx.fillRect(0, 0, width, height);
ctx.strokeStyle = type === 'ref' ? '#666' : type === 'A' ? '#ff4a4a' : '#4a9eff';
ctx.lineWidth = 3;
ctx.lineCap = 'round';
const centerX = width / 2;
const centerY = height / 2;
const scale = 50;
// Simple stick figure
ctx.beginPath();
ctx.arc(centerX, centerY - scale, 15, 0, Math.PI * 2);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(centerX, centerY - scale + 15);
ctx.lineTo(centerX, centerY + scale);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(centerX - scale * 0.8, centerY - scale * 0.3);
ctx.lineTo(centerX + scale * 0.8, centerY - scale * 0.3);
ctx.stroke();
ctx.beginPath();
ctx.moveTo(centerX, centerY + scale);
ctx.lineTo(centerX - scale * 0.5, centerY + scale * 1.5);
ctx.moveTo(centerX, centerY + scale);
ctx.lineTo(centerX + scale * 0.5, centerY + scale * 1.5);
ctx.stroke();
ctx.fillStyle = ctx.strokeStyle;
ctx.font = '12px monospace';
ctx.textAlign = 'center';
const label = type === 'ref' ? 'REF (placeholder)' :
type === 'A' ? 'POSE A (placeholder)' : 'POSE B (placeholder)';
ctx.fillText(label, centerX, height - 20);
return canvas;
}
// CRITICAL FIX: Better dimension detection for poses A and B
getPoseDimensions(img, keyPrefix, inputDims) {
// Priority 1: Use API input dimensions if available
if (inputDims && inputDims[keyPrefix]) {
const [w, h] = inputDims[keyPrefix];
console.log(`Using API dimensions for ${keyPrefix}: ${w}x${h}`);
return { width: w, height: h };
}
// Priority 2: Use actual image dimensions
if (img) {
console.log(`Using image dimensions for ${keyPrefix}: ${img.width}x${img.height}`);
return { width: img.width, height: img.height };
}
// Priority 3: Use reference dimensions as fallback
const refSize = this.imageManager.getReferenceImageSize();
console.log(`FALLBACK: Using reference dimensions for ${keyPrefix}: ${refSize.width}x${refSize.height}`);
return refSize;
}
// FIXED: Draw pose layer with proper dimension handling
drawPoseLayer(img, keyPrefix, color, isActive, coordSys, hasValidImages) {
const { canvasScale, offsetX, offsetY, refW, refH } = coordSys;
if (!img) {
if (!hasValidImages) {
const type = keyPrefix === "REF" ? "ref" : keyPrefix === "A" ? "A" : "B";
img = this.generatePoseVisualization(type, refW, refH);
} else {
console.log(`No image for ${keyPrefix}`);
return;
}
}
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
if (keyPrefix === "REF") {
// Reference image: draw at reference size and position
this.ctx.globalAlpha = 0.8;
this.ctx.drawImage(img, offsetX, offsetY, refW * canvasScale, refH * canvasScale);
console.log(`Drew reference pose at: ${offsetX}, ${offsetY}, ${refW * canvasScale}x${refH * canvasScale}`);
} else {
// For poses A and B, get current transform values
const transform = this.transformManager.getCurrentTransform(keyPrefix, refW, refH);
const { tx, ty, scale, rotD, matrix } = transform;
console.log(`Pose ${keyPrefix} transform:`, { tx, ty, scale, rotD, matrix });
// CRITICAL FIX: Get the correct dimensions for this pose
const inputDims = this.transformManager.getInputDimensions();
const poseDims = this.getPoseDimensions(img, keyPrefix, inputDims);
const drawW = poseDims.width;
const drawH = poseDims.height;
console.log(`Pose ${keyPrefix} will be drawn at dimensions: ${drawW}x${drawH}`);
// Apply transformation matrix with proper scaling
this.ctx.setTransform(
matrix.a * canvasScale, // Scale X transformation
matrix.b * canvasScale, // Scale Y shear
matrix.c * canvasScale, // Scale X shear
matrix.d * canvasScale, // Scale Y transformation
matrix.e * canvasScale + offsetX, // Translate X to canvas coords
matrix.f * canvasScale + offsetY // Translate Y to canvas coords
);
// Set appropriate transparency
if (keyPrefix === "A") {
this.ctx.globalAlpha = 0.6; // More transparent so B shows through
} else {
this.ctx.globalAlpha = 0.8; // Less transparent but still allows ref to show
}
// CRITICAL FIX: Draw the image using the CORRECT dimensions for this pose
this.ctx.drawImage(img, 0, 0, drawW, drawH);
console.log(`Drew pose ${keyPrefix} at correct dimensions: ${drawW}x${drawH}`);
// Reset transform to draw outline in canvas coordinates
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
// Calculate transformed corners using the correct drawing dimensions
const corners = [
[0, 0], [drawW, 0], [drawW, drawH], [0, drawH]
].map(([x, y]) => [
(matrix.a * x + matrix.c * y + matrix.e) * canvasScale + offsetX,
(matrix.b * x + matrix.d * y + matrix.f) * canvasScale + offsetY
]);
// Draw colored outline showing the transformed boundary
this.ctx.globalAlpha = isActive ? 1.0 : 0.6;
this.ctx.strokeStyle = color;
this.ctx.lineWidth = isActive ? 3 : 2;
this.ctx.setLineDash(isActive ? [] : [5, 5]);
this.ctx.beginPath();
this.ctx.moveTo(corners[0][0], corners[0][1]);
for (let i = 1; i < corners.length; i++) {
this.ctx.lineTo(corners[i][0], corners[i][1]);
}
this.ctx.closePath();
this.ctx.stroke();
this.ctx.setLineDash([]);
console.log(`Drew bounding box for ${keyPrefix} using ${drawW}x${drawH} at corners:`, corners);
}
}
// Draw UI overlay
drawUIOverlay(coordSys, hasValidImages) {
const { actualCanvasWidth, actualCanvasHeight } = coordSys;
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
this.ctx.globalAlpha = 1.0;
if (!hasValidImages) {
this.ctx.fillStyle = "#888";
this.ctx.font = "14px monospace";
this.ctx.textAlign = "center";
this.ctx.fillText("Run workflow to generate images", actualCanvasWidth/2, actualCanvasHeight/2 - 20);
this.ctx.fillStyle = "#666";
this.ctx.font = "12px monospace";
this.ctx.fillText("Images will appear after node execution", actualCanvasWidth/2, actualCanvasHeight/2 + 5);
this.ctx.font = "10px monospace";
const nodeImgCount = this.imageManager.node.imgs?.length || 0;
this.ctx.fillText(`node.imgs found: ${nodeImgCount}`, actualCanvasWidth/2, actualCanvasHeight/2 + 25);
}
// Active pose indicator
this.ctx.fillStyle = this.state.which === "A" ? "#ff4a4a" : "#4a9eff";
this.ctx.fillRect(10, 10, 20, 20);
this.ctx.strokeStyle = "#fff";
this.ctx.lineWidth = 2;
this.ctx.strokeRect(10, 10, 20, 20);
this.ctx.fillStyle = "#fff";
this.ctx.font = "12px monospace";
this.ctx.textAlign = "left";
this.ctx.fillText(`Active: Pose ${this.state.which}`, 40, 25);
// Instructions
this.ctx.fillStyle = "#aaa";
this.ctx.font = "10px monospace";
this.ctx.fillText("Left-click: Move Pose A", 10, actualCanvasHeight - 70);
this.ctx.fillText("Right-click: Move Pose B", 10, actualCanvasHeight - 55);
this.ctx.fillText("Wheel: Scale | Shift+Wheel: Rotate", 10, actualCanvasHeight - 40);
this.ctx.fillText("Arrow keys: Fine movement | R: Reset pose", 10, actualCanvasHeight - 25);
// Enhanced debug info with dimension details
const refSize = this.imageManager.getReferenceImageSize();
const canvasScale = coordSys.canvasScale;
const inputDims = this.transformManager.getInputDimensions();
const transform = this.transformManager.getCurrentTransform(this.state.which, refSize.width, refSize.height);
let dimInfo = `Ref: ${refSize.width}x${refSize.height}`;
if (inputDims) {
if (inputDims.A) dimInfo += ` | A: ${inputDims.A[0]}x${inputDims.A[1]}`;
if (inputDims.B) dimInfo += ` | B: ${inputDims.B[0]}x${inputDims.B[1]}`;
} else {
dimInfo += ` | API dims: NULL`;
}
dimInfo += ` | Scale: ${canvasScale.toFixed(3)}`;
// Safe handling of potentially undefined transform values
const tx = (transform.tx !== undefined && !isNaN(transform.tx)) ? transform.tx.toFixed(1) : '0.0';
const ty = (transform.ty !== undefined && !isNaN(transform.ty)) ? transform.ty.toFixed(1) : '0.0';
const scale = (transform.scale !== undefined && !isNaN(transform.scale)) ? transform.scale.toFixed(2) : '1.00';
const angle = (transform.rotD !== undefined && !isNaN(transform.rotD)) ? transform.rotD.toFixed(1) : '0.0';
dimInfo += ` | ${this.state.which}: tx=${tx}, ty=${ty}, s=${scale}, r=${angle}°`;
this.ctx.fillText(dimInfo, 10, actualCanvasHeight - 10);
}
// Main drawing function with enhanced debugging
async draw() {
try {
console.log("=== DRAW FUNCTION START ===");
const dataUpdated = await this.transformManager.updateFromNode();
if (dataUpdated) {
console.log("Transform data updated from Python node");
}
const images = await this.imageManager.getImagesFromNode(this.transformManager);
const hasValidImages = !!(images.ref || images.A || images.B);
const coordSys = this.calculateCoordinateSystem();
const { actualCanvasWidth, actualCanvasHeight } = coordSys;
// Enhanced debug logging with dimension info
const inputDims = this.transformManager.getInputDimensions();
const refSize = this.imageManager.getReferenceImageSize();
const transformA = this.transformManager.getCurrentTransform('A', refSize.width, refSize.height);
const transformB = this.transformManager.getCurrentTransform('B', refSize.width, refSize.height);
console.log("CANVAS DRAW STATE:", {
hasValidImages,
refImageSize: refSize,
inputDimensions: inputDims,
inputDimsStatus: inputDims ? "Available" : "NULL from API",
canvasActualSize: { width: actualCanvasWidth, height: actualCanvasHeight },
canvasScale: coordSys.canvasScale,
activeTransforms: {
A: { tx: transformA.tx, ty: transformA.ty, scale: transformA.scale, angle: transformA.rotD },
B: { tx: transformB.tx, ty: transformB.ty, scale: transformB.scale, angle: transformB.rotD }
},
properties: this.transformManager.getAllProperties()
});
// Clear canvas
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
this.ctx.clearRect(0, 0, actualCanvasWidth, actualCanvasHeight);
// Background
this.ctx.fillStyle = "#1a1a1a";
this.ctx.fillRect(0, 0, actualCanvasWidth, actualCanvasHeight);
// Draw layers in correct order (back to front)
this.drawGrid(coordSys);
this.drawPoseLayer(images.ref, "REF", "#666", false, coordSys, hasValidImages);
// Only draw pose B if it exists (not in single pose mode)
if (images.B) {
this.drawPoseLayer(images.B, "B", "#4a9eff", this.state.which === "B", coordSys, hasValidImages);
}
this.drawPoseLayer(images.A, "A", "#ff4a4a", this.state.which === "A", coordSys, hasValidImages);
this.drawUIOverlay(coordSys, hasValidImages);
console.log("=== DRAW FUNCTION END ===");
} catch (error) {
console.error("Error in draw function:", error);
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.fillStyle = "#1a1a1a";
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
this.ctx.fillStyle = "#ff4444";
this.ctx.font = "14px monospace";
this.ctx.textAlign = "center";
this.ctx.fillText("Error loading images", this.canvas.width/2, this.canvas.height/2);
this.ctx.font = "10px monospace";
this.ctx.fillText("Check console for details", this.canvas.width/2, this.canvas.height/2 + 20);
}
}
// Get canvas coordinates from mouse event
getCanvasCoordinates(e) {
const rect = this.canvas.getBoundingClientRect();
return {
x: (e.clientX - rect.left) * (this.canvas.width / rect.width),
y: (e.clientY - rect.top) * (this.canvas.height / rect.height)
};
}
}