// Canvas操作用JavaScript for dwpose-editor // 🔧 最小限デバッグフラグ(必要時のみONにする) window.poseEditorDebug = window.poseEditorDebug || { rect: false, hands: false, send: false }; // グローバル変数 window.poseEditorGlobals = { canvas: null, ctx: null, poseData: null, // 追加! isUpdating: false, // 🔧 表示・編集設定 enableHands: true, enableFace: false, editMode: "簡易モード", // "簡易モード" or "詳細モード" // 🎨 背景画像機能 backgroundImage: null, // 背景画像オブジェクト // 🔧 矩形編集状態(refs互換) rectEditMode: null, // 'leftHand', 'rightHand', 'face', null rectEditModeActive: false, // 矩形編集モード状態 currentRects: { leftHand: null, rightHand: null, face: null }, draggedRectControl: null, // ドラッグ中のコントロールポイント draggedRect: null, // ドラッグ中の矩形 dragStartPos: { x: 0, y: 0 } }; let canvas = null; let ctx = null; let poseData = null; let isInitialized = false; // DWPose 20キーポイント接続定義(つま先込み)- refs互換 const BODY_CONNECTIONS = [ [1, 2], [1, 5], [2, 3], [3, 4], [5, 6], [6, 7], [1, 8], [8, 9], [9, 10], [1, 11], [11, 12], [12, 13], [1, 0], [0, 14], [14, 16], [0, 15], [15, 17], [13, 18], [10, 19] // 修正:右足首→右つま先、左足首→左つま先 ]; // 色定義(dwpose_modifierから) const POSE_COLORS = { body: '#ff0055', hand: '#ff9500', face: '#00ff00', bodyLine: '#ff0055', handLine: '#ff9500', faceLine: '#00ff00' }; // スケルトン色配列(refs互換) - 構造化された色定義 const SKELETON_COLORS = [ 'rgb(255,0,0)', 'rgb(255,85,0)', 'rgb(255,170,0)', 'rgb(255,255,0)', 'rgb(170,255,0)', 'rgb(85,255,0)', 'rgb(0,255,0)', 'rgb(0,255,85)', 'rgb(0,255,170)', 'rgb(0,255,255)', 'rgb(0,170,255)', 'rgb(0,85,255)', 'rgb(0,0,255)', 'rgb(85,0,255)', 'rgb(170,0,255)', 'rgb(255,0,255)', 'rgb(255,0,170)', 'rgb(255,0,85)', 'rgb(255,255,170)', 'rgb(170,255,255)' ]; // キーポイント半径 const KEYPOINT_RADIUS = 4; // ドラッグ状態(refs互換) let isDragging = false; let draggedKeypoint = -1; let dragOffset = { x: 0, y: 0 }; // デバッグログ機能は削除済み // Canvas初期化関数 function initializePoseEditor() { canvas = document.getElementById('pose_canvas'); if (!canvas) { setTimeout(initializePoseEditor, 100); return; } ctx = canvas.getContext('2d'); if (!ctx) { return; } // グローバル変数に保存(refs互換) window.poseEditorGlobals.canvas = canvas; window.poseEditorGlobals.ctx = ctx; // ローカル変数も更新 window.canvas = canvas; window.ctx = ctx; // Canvas設定 canvas.width = 640; canvas.height = 640; // 初期描画 clearCanvas(); // 🔧 デフォルトカーソル設定 canvas.style.cursor = 'default'; isInitialized = true; notifyCanvasStateChange('initialized'); // ドラッグイベントを設定(refs互換) setupDragEvents(); // 🔧 Gradioチェックボックス監視を開始 setupGradioCheckboxListeners(); } // 後方互換性のために古い関数名も残す function initializeCanvas() { initializePoseEditor(); } // 複数の初期化トリガー(refs互換) document.addEventListener('DOMContentLoaded', initializePoseEditor); window.addEventListener('load', initializePoseEditor); // 後方互換性 document.addEventListener('DOMContentLoaded', initializeCanvas); window.addEventListener('load', initializeCanvas); // Gradio固有の初期化(MutationObserver使用) const observer = new MutationObserver((mutations) => { if (document.getElementById('pose_canvas') && !isInitialized) { initializeCanvas(); } }); // body要素の監視開始 document.addEventListener('DOMContentLoaded', () => { observer.observe(document.body, { childList: true, subtree: true }); }); // Canvas クリア function clearCanvas() { if (!ctx) return; ctx.fillStyle = '#f0f0f0'; ctx.fillRect(0, 0, canvas.width, canvas.height); } // エラー表示 function showCanvasError(message) { if (!ctx) return; clearCanvas(); ctx.fillStyle = '#ff0000'; ctx.font = '16px Arial'; ctx.textAlign = 'center'; ctx.fillText(message, canvas.width / 2, canvas.height / 2); } // ドラッグイベント設定(refs互換) function setupDragEvents() { if (!canvas) { return; } canvas.addEventListener('mousedown', handleMouseDown); canvas.addEventListener('mousemove', handleMouseMove); canvas.addEventListener('mouseup', handleMouseUp); canvas.addEventListener('mouseleave', handleMouseUp); // Canvas外ドラッグ対策 // テスト用クリックイベント canvas.addEventListener('click', function(event) { }); } // 🔧 Gradioチェックボックス監視設定 function setupGradioCheckboxListeners() { // チェックボックスとラジオボタン要素を探す(少し待ってから) setTimeout(() => { // チェックボックス監視 const allCheckboxes = document.querySelectorAll('input[type="checkbox"]'); allCheckboxes.forEach((checkbox, index) => { // 親要素のテキストから手を描画・顔を描画を特定 const parentText = checkbox.parentElement?.textContent?.trim() || ''; if (parentText.includes('手を描画')) { checkbox.addEventListener('change', (e) => { updateDisplaySettingsFromCheckbox(); }); } else if (parentText.includes('顔を描画')) { checkbox.addEventListener('change', (e) => { updateDisplaySettingsFromCheckbox(); }); } }); // ラジオボタン監視(編集モード) const allRadios = document.querySelectorAll('input[type="radio"]'); allRadios.forEach((radio, index) => { // 親要素のテキストから簡易モード・詳細モードを特定 const parentText = radio.parentElement?.textContent?.trim() || ''; const grandParentText = radio.parentElement?.parentElement?.textContent?.trim() || ''; if (parentText.includes('簡易モード') || parentText.includes('詳細モード') || grandParentText.includes('編集モード')) { radio.addEventListener('change', (e) => { updateDisplaySettingsFromCheckbox(); }); } }); }, 1000); } // チェックボックスとラジオボタンから設定を取得して描画更新 function updateDisplaySettingsFromCheckbox() { // チェックボックス処理 const allCheckboxes = document.querySelectorAll('input[type="checkbox"]'); let handEnabled = true; let faceEnabled = true; allCheckboxes.forEach((checkbox) => { const parentText = checkbox.parentElement?.textContent?.trim() || ''; if (parentText.includes('手を描画')) { handEnabled = checkbox.checked; } else if (parentText.includes('顔を描画')) { faceEnabled = checkbox.checked; } }); // ラジオボタン処理(編集モード) const allRadios = document.querySelectorAll('input[type="radio"]'); let editMode = "簡易モード"; // デフォルト allRadios.forEach((radio) => { if (radio.checked) { const parentText = radio.parentElement?.textContent?.trim() || ''; if (parentText.includes('簡易モード')) { editMode = "簡易モード"; } else if (parentText.includes('詳細モード')) { editMode = "詳細モード"; } } }); // グローバル設定を更新 window.poseEditorGlobals.enableHands = handEnabled; window.poseEditorGlobals.enableFace = faceEnabled; window.poseEditorGlobals.editMode = editMode; // 詳細モードに切り替えた時は矩形編集モードを確実に終了 if (editMode === "詳細モード") { window.poseEditorGlobals.rectEditMode = null; window.poseEditorGlobals.rectEditModeActive = false; window.poseEditorGlobals.draggedRectControl = null; window.poseEditorGlobals.draggedRect = null; window.poseEditorGlobals.currentRects = { leftHand: null, rightHand: null, face: null }; } // 強制再描画 const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (currentPoseData && Object.keys(currentPoseData).length > 0) { drawPose(currentPoseData, handEnabled, faceEnabled, editMode); } } // マウス座標取得(refs互換) function getMousePos(event) { const rect = canvas.getBoundingClientRect(); // CSSピクセル → Canvas内部ピクセルへ正規化(非スクエア時のズレ解消) const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; return { x: (event.clientX - rect.left) * scaleX, y: (event.clientY - rect.top) * scaleY }; } // 最寄りのキーポイントを検索(refs互換:戻り値はインデックス) function findNearestKeypoint(mouseX, mouseY, maxDistance = 20) { // グローバルposeDataを参照 const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (!currentPoseData) { return -1; } if (!currentPoseData.bodies) { return -1; } if (!currentPoseData.bodies.candidate) { return -1; } const candidates = currentPoseData.bodies.candidate; let nearestIndex = -1; let minDistance = maxDistance; // refs互換の閾値 // 📐 解像度情報の取得 const originalRes = currentPoseData.resolution || [512, 512]; const fit = getFitParams(originalRes); for (let i = 0; i < Math.min(20, candidates.length); i++) { // つま先込み20個 const point = candidates[i]; if (point && point[0] > 1 && point[1] > 1 && point[0] < originalRes[0] && point[1] < originalRes[1]) { // 座標変換を適用(アスペクト比維持 + オフセット) const scaledX = fit.offsetX + point[0] * fit.scale; const scaledY = fit.offsetY + point[1] * fit.scale; const distance = Math.sqrt((mouseX - scaledX) ** 2 + (mouseY - scaledY) ** 2); if (distance < minDistance) { minDistance = distance; nearestIndex = i; } } } return nearestIndex; // 🔧 refs互換:インデックス数値を返す } // 🎯 詳細モード用:手と顔のキーポイント検索 function findNearestKeypointInDetailMode(mouseX, mouseY, maxDistance = 15) { const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (!currentPoseData) { return null; } const originalRes = currentPoseData.resolution || [512, 512]; const fit = getFitParams(originalRes); let nearestKeypoint = null; let minDistance = maxDistance; // 💖 手のキーポイント検索(people形式統一) if (window.poseEditorGlobals.enableHands && currentPoseData.people && currentPoseData.people[0]) { const person = currentPoseData.people[0]; const handsData = [ person.hand_left_keypoints_2d || [], person.hand_right_keypoints_2d || [] ]; ['left', 'right'].forEach((handType, handIndex) => { const handData = handsData[handIndex]; if (handData && handData.length > 0) { for (let i = 0; i < handData.length; i += 3) { if (i + 2 < handData.length) { const x = fit.offsetX + handData[i] * fit.scale; const y = fit.offsetY + handData[i + 1] * fit.scale; const conf = handData[i + 2]; if (conf > 0.3) { const distance = Math.sqrt((mouseX - x) ** 2 + (mouseY - y) ** 2); if (distance < minDistance) { minDistance = distance; nearestKeypoint = { type: 'hand', handType: handType, handIndex: handIndex, keypointIndex: i / 3, arrayIndex: i }; } } } } } }); } // 😊 顔のキーポイント検索 if (window.poseEditorGlobals.enableFace && currentPoseData.faces) { const facesData = currentPoseData.people && currentPoseData.people[0] && currentPoseData.people[0].face_keypoints_2d ? [currentPoseData.people[0].face_keypoints_2d] : currentPoseData.faces; if (facesData && facesData[0] && facesData[0].length > 0) { const faceData = facesData[0]; for (let i = 0; i < faceData.length; i += 3) { if (i + 2 < faceData.length) { const x = fit.offsetX + faceData[i] * fit.scale; const y = fit.offsetY + faceData[i + 1] * fit.scale; const conf = faceData[i + 2]; if (conf > 0.3) { const distance = Math.sqrt((mouseX - x) ** 2 + (mouseY - y) ** 2); if (distance < minDistance) { minDistance = distance; nearestKeypoint = { type: 'face', keypointIndex: i / 3, arrayIndex: i }; } } } } } } // 👤 ボディのキーポイントも検索(優先度は低く) const bodyKeypointIndex = findNearestKeypoint(mouseX, mouseY, maxDistance * 0.8); if (bodyKeypointIndex >= 0) { const candidates = currentPoseData.bodies.candidate; const point = candidates[bodyKeypointIndex]; if (point) { const fit = getFitParams(currentPoseData.resolution || [512,512]); const scaledX = fit.offsetX + point[0] * fit.scale; const scaledY = fit.offsetY + point[1] * fit.scale; const distance = Math.sqrt((mouseX - scaledX) ** 2 + (mouseY - scaledY) ** 2); if (distance < minDistance) { nearestKeypoint = { type: 'body', keypointIndex: bodyKeypointIndex }; } } } return nearestKeypoint; } // 詳細モード用:最寄りのキーポイント検索(手・顔の個別キーポイント対応) function findNearestKeypointInDetailMode(clickX, clickY) { if (!poseData || !poseData.people || poseData.people.length === 0) { return null; } const person = poseData.people[0]; const threshold = 25; // ピクセル距離閾値(データ座標系) let minDistance = threshold; let nearestIndex = -1; let nearestType = null; // Canvas座標をデータ座標に変換 const currentPoseData = window.poseEditorGlobals.poseData || poseData; let dataClickX = clickX; let dataClickY = clickY; if (currentPoseData && canvas) { const resolution = currentPoseData.resolution || [512, 512]; const d = canvasToDataXY(clickX, clickY, resolution); dataClickX = d.x; dataClickY = d.y; } // 体のキーポイント検索 if (person.pose_keypoints_2d) { for (let i = 0; i < person.pose_keypoints_2d.length; i += 3) { const x = person.pose_keypoints_2d[i]; const y = person.pose_keypoints_2d[i + 1]; const confidence = person.pose_keypoints_2d[i + 2]; if (confidence > 0.3) { const distance = Math.sqrt((x - dataClickX) ** 2 + (y - dataClickY) ** 2); if (distance < threshold && distance < minDistance) { minDistance = distance; nearestIndex = i / 3; nearestType = 'body'; } } } } // 左手のキーポイント検索(詳細モードでは個別編集可能) if (window.poseEditorGlobals.enableHands && person.hand_left_keypoints_2d) { for (let i = 0; i < person.hand_left_keypoints_2d.length; i += 3) { const x = person.hand_left_keypoints_2d[i]; const y = person.hand_left_keypoints_2d[i + 1]; const confidence = person.hand_left_keypoints_2d[i + 2]; if (confidence > 0.3) { const distance = Math.sqrt((x - dataClickX) ** 2 + (y - dataClickY) ** 2); if (distance < threshold && distance < minDistance) { minDistance = distance; nearestIndex = i / 3; nearestType = 'leftHand'; } } } } // 右手のキーポイント検索(詳細モードでは個別編集可能) if (window.poseEditorGlobals.enableHands && person.hand_right_keypoints_2d) { for (let i = 0; i < person.hand_right_keypoints_2d.length; i += 3) { const x = person.hand_right_keypoints_2d[i]; const y = person.hand_right_keypoints_2d[i + 1]; const confidence = person.hand_right_keypoints_2d[i + 2]; if (confidence > 0.3) { const distance = Math.sqrt((x - dataClickX) ** 2 + (y - dataClickY) ** 2); if (distance < threshold && distance < minDistance) { minDistance = distance; nearestIndex = i / 3; nearestType = 'rightHand'; } } } } // 顔のキーポイント検索(詳細モードでは個別編集可能) if (window.poseEditorGlobals.enableFace && person.face_keypoints_2d) { for (let i = 0; i < person.face_keypoints_2d.length; i += 3) { const x = person.face_keypoints_2d[i]; const y = person.face_keypoints_2d[i + 1]; const confidence = person.face_keypoints_2d[i + 2]; if (confidence > 0.3) { const distance = Math.sqrt((x - dataClickX) ** 2 + (y - dataClickY) ** 2); if (distance < threshold && distance < minDistance) { minDistance = distance; nearestIndex = i / 3; nearestType = 'face'; } } } } const result = { index: nearestIndex, type: nearestType, distance: minDistance }; return nearestIndex >= 0 ? result : null; } // マウスダウン処理(refs互換 + 矩形編集対応 + 詳細モード対応) function handleMouseDown(event) { if (!isCanvasReady()) { return; } const mousePos = getMousePos(event); // 🎯 詳細モードでのキーポイント直接編集 if (window.poseEditorGlobals.editMode === "詳細モード") { const keypoint = findNearestKeypointInDetailMode(mousePos.x, mousePos.y); if (keypoint) { // 詳細モードでのキーポイントドラッグ開始 isDragging = true; // 詳細モード用のドラッグオブジェクトを設定 window.poseEditorGlobals.draggedDetailKeypoint = { type: keypoint.type, index: keypoint.index, arrayIndex: keypoint.index * 3 // フラット配列インデックス }; // ドラッグオフセット計算 const person = poseData.people[0]; let keypointArray; switch (keypoint.type) { case 'body': keypointArray = person.pose_keypoints_2d; break; case 'leftHand': keypointArray = person.hand_left_keypoints_2d; break; case 'rightHand': keypointArray = person.hand_right_keypoints_2d; break; case 'face': keypointArray = person.face_keypoints_2d; break; } if (keypointArray) { const keypointX = keypointArray[keypoint.index * 3]; const keypointY = keypointArray[keypoint.index * 3 + 1]; dragOffset.x = mousePos.x - keypointX; dragOffset.y = mousePos.y - keypointY; } } return; } // 🔧 簡易モードでの矩形編集優先処理 if (window.poseEditorGlobals.editMode === "簡易モード") { // 矩形編集モード中の処理 if (window.poseEditorGlobals.rectEditModeActive) { const controlPoint = findNearestRectControlPoint(mousePos.x, mousePos.y); if (controlPoint) { // 🔧 コントロールポイントドラッグ開始時の元座標保存 const rectType = window.poseEditorGlobals.rectEditMode; const currentRect = window.poseEditorGlobals.currentRects[rectType]; if (currentRect) { window.poseEditorGlobals.originalRect = { ...currentRect }; // 🔧 このドラッグ操作の基準となる"元キーポイント"も現在の状態からスナップショット // 以前はセッション開始時のベースを使っていたため、連続リサイズで倍率/方向が狂っていた const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (currentPoseData) { window.poseEditorGlobals.originalKeypoints = JSON.parse(JSON.stringify(currentPoseData)); } } // コントロールポイントドラッグ(リサイズ) window.poseEditorGlobals.draggedRectControl = controlPoint; window.poseEditorGlobals.dragStartPos = { x: mousePos.x, y: mousePos.y }; isDragging = true; return; } // 矩形内ドラッグ(移動) const rectType = findRectContaining(mousePos.x, mousePos.y); if (rectType === window.poseEditorGlobals.rectEditMode) { window.poseEditorGlobals.draggedRect = rectType; window.poseEditorGlobals.dragStartPos = { x: mousePos.x, y: mousePos.y }; isDragging = true; return; } // 他の矩形クリック → 切り替え if (rectType && rectType !== window.poseEditorGlobals.rectEditMode) { window.poseEditorGlobals.rectEditMode = rectType; // 再描画で新しい矩形のコントロールポイントを表示 const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (currentPoseData) { drawPose(currentPoseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace); } return; } // 矩形外クリック → 編集モード終了 // 🚀 currentPoseData定義を先に移動 const currentPoseData = window.poseEditorGlobals.poseData || poseData; // 🚀 編集完了データをGradio送信(データ改変はしない) sendPoseDataToGradio(); // 🔧 矩形編集モード終了時の完全な状態クリア(連続編集対応) window.poseEditorGlobals.rectEditModeActive = false; window.poseEditorGlobals.rectEditMode = null; // 💖 編集用データ状態をクリア(baseOriginalKeypointsは保持!) // window.poseEditorGlobals.baseOriginalKeypoints = null; // ← 💥 これが原因!削除 window.poseEditorGlobals.originalKeypoints = null; window.poseEditorGlobals.originalRect = null; window.poseEditorGlobals.rectEditInfo = null; // 🔧 ドラッグ状態もクリア window.poseEditorGlobals.draggedRectControl = null; window.poseEditorGlobals.draggedRect = null; window.poseEditorGlobals.dragStartPos = { x: 0, y: 0 }; // 🚀 矩形位置を保持したまま再描画(再計算させない) if (currentPoseData) { // 矩形編集モードをfalseにした直後なので、矩形は保持される drawPose(currentPoseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace); } return; } // 💖 矩形編集モードでない場合の矩形クリック → 編集モード開始(refs準拠) const rectType = findRectContaining(mousePos.x, mousePos.y); if (rectType) { // 矩形モード切り替え時の処理 if (window.poseEditorGlobals.rectEditMode !== rectType) { window.poseEditorGlobals.rectEditMode = rectType; } window.poseEditorGlobals.rectEditModeActive = true; // 🔧 rectEditInfo全体を初期化(全矩形タイプ対応) initializeRectEditInfo(); // 再描画でコントロールポイントを表示 const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (currentPoseData) { drawPose(currentPoseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace); } return; } } // 🔧 詳細モードまたは矩形編集モードでない場合:通常のキーポイント編集 if (window.poseEditorGlobals.editMode === "詳細モード" || !window.poseEditorGlobals.rectEditModeActive) { // 🎯 詳細モードでは手・顔・ボディ全て検索 if (window.poseEditorGlobals.editMode === "詳細モード") { const detailKeypoint = findNearestKeypointInDetailMode(mousePos.x, mousePos.y); if (detailKeypoint) { isDragging = true; window.poseEditorGlobals.draggedDetailKeypoint = detailKeypoint; canvas.style.cursor = 'grabbing'; return; } } // 🔧 簡易モードまたは詳細モードで何も見つからない場合:ボディキーポイント検索 else { const keypointIndex = findNearestKeypoint(mousePos.x, mousePos.y); if (keypointIndex >= 0) { isDragging = true; draggedKeypoint = keypointIndex; // 🔧 refs互換:ドラッグオフセット計算(キーポイントの正確な位置からのオフセット) const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (currentPoseData && currentPoseData.bodies && currentPoseData.bodies.candidate) { const candidates = currentPoseData.bodies.candidate; const originalRes = currentPoseData.resolution || [512, 512]; const fit = getFitParams(originalRes); const point = candidates[keypointIndex]; if (point) { const keypointX = fit.offsetX + point[0] * fit.scale; const keypointY = fit.offsetY + point[1] * fit.scale; dragOffset = { x: mousePos.x - keypointX, y: mousePos.y - keypointY }; } } canvas.style.cursor = 'grabbing'; } else { } } } } // 🎯 詳細モード用:手・顔・ボディキーポイント位置更新 function updateDetailKeypointPosition(detailKeypoint, canvasX, canvasY) { const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (!currentPoseData) { return; } const originalRes = currentPoseData.resolution || [512, 512]; // Canvas座標をデータ座標に変換(レターボックス対応) const dataPt = canvasToDataXY(canvasX, canvasY, originalRes); const dataX = dataPt.x; const dataY = dataPt.y; switch (detailKeypoint.type) { case 'leftHand': // 左手のキーポイント更新 if (currentPoseData.people && currentPoseData.people[0]) { if (!currentPoseData.people[0].hand_left_keypoints_2d) { // people形式で編集済みデータが存在しない場合は作成 const originalHandData = currentPoseData.hands && currentPoseData.hands[0]; if (originalHandData) { currentPoseData.people[0].hand_left_keypoints_2d = [...originalHandData]; } } if (currentPoseData.people[0].hand_left_keypoints_2d) { const handData = currentPoseData.people[0].hand_left_keypoints_2d; const arrayIndex = detailKeypoint.arrayIndex; if (arrayIndex < handData.length - 1) { handData[arrayIndex] = dataX; handData[arrayIndex + 1] = dataY; // 信頼度は維持 // 元のhandsデータも同期更新 syncHandsToOriginal(currentPoseData, 'left', handData); } } } break; case 'rightHand': // 右手のキーポイント更新 if (currentPoseData.people && currentPoseData.people[0]) { if (!currentPoseData.people[0].hand_right_keypoints_2d) { // people形式で編集済みデータが存在しない場合は作成 const originalHandData = currentPoseData.hands && currentPoseData.hands[1]; if (originalHandData) { currentPoseData.people[0].hand_right_keypoints_2d = [...originalHandData]; } } if (currentPoseData.people[0].hand_right_keypoints_2d) { const handData = currentPoseData.people[0].hand_right_keypoints_2d; const arrayIndex = detailKeypoint.arrayIndex; if (arrayIndex < handData.length - 1) { handData[arrayIndex] = dataX; handData[arrayIndex + 1] = dataY; // 信頼度は維持 // 元のhandsデータも同期更新 syncHandsToOriginal(currentPoseData, 'right', handData); } } } break; case 'face': // 顔のキーポイント更新 if (currentPoseData.people && currentPoseData.people[0]) { if (!currentPoseData.people[0].face_keypoints_2d) { // 編集済みデータが存在しない場合は作成 const originalFaceData = currentPoseData.faces && currentPoseData.faces[0]; if (originalFaceData) { currentPoseData.people[0].face_keypoints_2d = [...originalFaceData]; } } if (currentPoseData.people[0].face_keypoints_2d) { const faceData = currentPoseData.people[0].face_keypoints_2d; const arrayIndex = detailKeypoint.arrayIndex; if (arrayIndex < faceData.length - 1) { faceData[arrayIndex] = dataX; faceData[arrayIndex + 1] = dataY; // 信頼度は維持 // 🚀 元のfacesデータも同期更新 syncFacesToOriginal(currentPoseData, faceData); } } } break; case 'body': // ボディキーポイント更新(既存の関数を使用) updateKeypointPosition(detailKeypoint.index, canvasX, canvasY); break; } } // 🚀 手のデータ同期関数 function syncHandsToOriginal(poseData, handType, handData) { if (!poseData.hands) return; const handIndex = handType === 'left' ? 0 : 1; if (handIndex < poseData.hands.length) { poseData.hands[handIndex] = [...handData]; } } // 🚀 顔のデータ同期関数 function syncFacesToOriginal(poseData, faceData) { if (!poseData.faces) return; if (poseData.faces.length > 0) { poseData.faces[0] = [...faceData]; } } // 🎯 矩形変形によるキーポイント一括変換機能(refs互換・Issue 028実装) function transformKeypointsInRect(control, newMouseX, newMouseY) { // 1. データとコントロールの存在確認 if (!window.poseEditorGlobals.poseData || !control) { return; } // 2. people形式データの確認 if (!window.poseEditorGlobals.poseData.people || !Array.isArray(window.poseEditorGlobals.poseData.people) || window.poseEditorGlobals.poseData.people.length === 0) { return; } const person = window.poseEditorGlobals.poseData.people[0]; // 3. 対象キーポイントデータを取得 let targetKeypoints; let fieldName; switch (control.type) { case 'face': targetKeypoints = person.face_keypoints_2d; fieldName = 'face_keypoints_2d'; break; case 'leftHand': targetKeypoints = person.hand_left_keypoints_2d; fieldName = 'hand_left_keypoints_2d'; break; case 'rightHand': targetKeypoints = person.hand_right_keypoints_2d; fieldName = 'hand_right_keypoints_2d'; break; default: return; } if (!targetKeypoints || !Array.isArray(targetKeypoints)) { return; } // 4. 元の矩形サイズを取得 const originalRect = control.rect; // 5. 新しい矩形サイズを計算(角に応じて) let newRect = { ...originalRect }; switch (control.corner) { case 'TL': // 左上 newRect.width = originalRect.width + (originalRect.x - newMouseX); newRect.height = originalRect.height + (originalRect.y - newMouseY); newRect.x = newMouseX; newRect.y = newMouseY; break; case 'TR': // 右上 newRect.width = newMouseX - originalRect.x; newRect.height = originalRect.height + (originalRect.y - newMouseY); newRect.y = newMouseY; break; case 'BL': // 左下 newRect.width = originalRect.width + (originalRect.x - newMouseX); newRect.height = newMouseY - originalRect.y; newRect.x = newMouseX; break; case 'BR': // 右下 newRect.width = newMouseX - originalRect.x; newRect.height = newMouseY - originalRect.y; break; } // 6. 最小サイズ制限 const minSize = 20; if (newRect.width < minSize || newRect.height < minSize) return; // 7. 変換比率を計算 const scaleX = newRect.width / originalRect.width; const scaleY = newRect.height / originalRect.height; // 8. 座標変換設定の準備 const canvasWidth = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.width : 512; const canvasHeight = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.height : 512; let dataResolutionWidth = canvasWidth; let dataResolutionHeight = canvasHeight; // 解像度情報の取得(現在のデータ構造に対応) const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (currentPoseData.resolution && Array.isArray(currentPoseData.resolution) && currentPoseData.resolution.length >= 2) { dataResolutionWidth = currentPoseData.resolution[0]; dataResolutionHeight = currentPoseData.resolution[1]; } // レターボックス対応フィット const fit = getFitParams([dataResolutionWidth, dataResolutionHeight]); // 9. 正規化座標かピクセル座標かを判定 let isNormalized = false; if (targetKeypoints.length > 0) { // 最初の有効なキーポイントで判定 for (let i = 0; i < targetKeypoints.length; i += 3) { if (i + 2 < targetKeypoints.length && targetKeypoints[i + 2] > 0) { const x = targetKeypoints[i]; const y = targetKeypoints[i + 1]; isNormalized = (x >= 0 && x <= 1 && y >= 0 && y <= 1); break; } } } // 10. 対象キーポイントを一括変換 for (let i = 0; i < targetKeypoints.length; i += 3) { if (i + 2 < targetKeypoints.length) { const confidence = targetKeypoints[i + 2]; if (confidence > 0.1) { // 閾値を下げて、より多くのポイントを変換 let x = targetKeypoints[i]; let y = targetKeypoints[i + 1]; // データ座標→Canvas座標 let canvasX, canvasY; if (isNormalized) { const dx = x * dataResolutionWidth; const dy = y * dataResolutionHeight; canvasX = fit.offsetX + dx * fit.scale; canvasY = fit.offsetY + dy * fit.scale; } else { canvasX = fit.offsetX + x * fit.scale; canvasY = fit.offsetY + y * fit.scale; } // 元矩形内での相対位置を計算 const relativeX = (canvasX - originalRect.x) / originalRect.width; const relativeY = (canvasY - originalRect.y) / originalRect.height; // 新矩形での新しい位置を計算 const newCanvasX = newRect.x + (relativeX * newRect.width); const newCanvasY = newRect.y + (relativeY * newRect.height); // Canvas座標→データ座標に戻す if (isNormalized) { const dataX = (newCanvasX - fit.offsetX) / fit.scale; const dataY = (newCanvasY - fit.offsetY) / fit.scale; targetKeypoints[i] = dataX / dataResolutionWidth; targetKeypoints[i + 1] = dataY / dataResolutionHeight; } else { targetKeypoints[i] = (newCanvasX - fit.offsetX) / fit.scale; targetKeypoints[i + 1] = (newCanvasY - fit.offsetY) / fit.scale; } } } } // 11. 矩形情報を更新 control.rect = newRect; } // 🔧 直接矩形変換関数(シンプル版) function transformKeypointsDirectly(rectType, originalRect, newRect) { const currentPoseData = window.poseEditorGlobals.poseData || poseData; const originalKeypoints = window.poseEditorGlobals.originalKeypoints; if (!currentPoseData || !currentPoseData.people || !currentPoseData.people[0]) { return; } if (!originalKeypoints || !originalKeypoints.people || !originalKeypoints.people[0]) { return; } const person = currentPoseData.people[0]; const originalPerson = originalKeypoints.people[0]; // 対象キーポイントデータを取得(元データと現在データ両方) let targetKeypoints, originalTargetKeypoints; let fieldName; switch (rectType) { case 'face': targetKeypoints = person.face_keypoints_2d; originalTargetKeypoints = originalPerson.face_keypoints_2d; fieldName = 'face_keypoints_2d'; break; case 'leftHand': targetKeypoints = person.hand_left_keypoints_2d; originalTargetKeypoints = originalPerson.hand_left_keypoints_2d; fieldName = 'hand_left_keypoints_2d'; break; case 'rightHand': targetKeypoints = person.hand_right_keypoints_2d; originalTargetKeypoints = originalPerson.hand_right_keypoints_2d; fieldName = 'hand_right_keypoints_2d'; break; default: return; } if (!targetKeypoints || !Array.isArray(targetKeypoints)) { return; } if (!originalTargetKeypoints || !Array.isArray(originalTargetKeypoints)) { return; } // 座標変換設定 const canvasWidth = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.width : 512; const canvasHeight = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.height : 512; // 解像度情報の取得(metadata.resolution → resolution → 512x512) let dataResolutionWidth = 512; let dataResolutionHeight = 512; if (currentPoseData.metadata && currentPoseData.metadata.resolution && Array.isArray(currentPoseData.metadata.resolution) && currentPoseData.metadata.resolution.length >= 2) { dataResolutionWidth = currentPoseData.metadata.resolution[0]; dataResolutionHeight = currentPoseData.metadata.resolution[1]; } else if (currentPoseData.resolution && Array.isArray(currentPoseData.resolution) && currentPoseData.resolution.length >= 2) { dataResolutionWidth = currentPoseData.resolution[0]; dataResolutionHeight = currentPoseData.resolution[1]; } const fit = getFitParams([dataResolutionWidth, dataResolutionHeight]); // 正規化/ピクセル/Canvas座標を判定(元データ優先で判定) let isNormalized = false; let isCanvasUnit = false; let sampleX = null, sampleY = null; if (originalTargetKeypoints.length > 0) { let overCanvasCount = 0; let validCount = 0; for (let i = 0; i < originalTargetKeypoints.length; i += 3) { if (i + 2 < originalTargetKeypoints.length && originalTargetKeypoints[i + 2] > 0) { const x = originalTargetKeypoints[i]; const y = originalTargetKeypoints[i + 1]; if (sampleX === null) { sampleX = x; sampleY = y; } validCount++; if (x > dataResolutionWidth * 1.01 || y > dataResolutionHeight * 1.01) { overCanvasCount++; } // 正規化判定 if (x >= 0 && x <= 1 && y >= 0 && y <= 1) { isNormalized = true; break; } if (validCount >= 20) break; // サンプル十分 } } if (!isNormalized && validCount > 0) { // 多数がデータ解像度を超える場合はCanvas座標とみなす isCanvasUnit = (overCanvasCount / validCount) > 0.5; } } // 🔍 デバッグログ: 手と顔の座標データ比較 if (window.poseEditorDebug.rect) console.log(`🔍 [DEBUG ${rectType}] 座標データ分析:`, { rectType: rectType, isNormalized: isNormalized, isCanvasUnit: isCanvasUnit, sampleCoord: { x: sampleX, y: sampleY }, originalRect: { x: originalRect.x, y: originalRect.y, width: originalRect.width, height: originalRect.height }, newRect: { x: newRect.x, y: newRect.y, width: newRect.width, height: newRect.height }, fit: { scale: fit.scale, offsetX: fit.offsetX, offsetY: fit.offsetY }, resolution: { data: dataResolutionWidth + 'x' + dataResolutionHeight, canvas: canvasWidth + 'x' + canvasHeight }, keypointsLength: targetKeypoints.length }); // 参照矩形は常にポイント群のBBox(Canvas座標)を使用して相対比を安定化 let refRect = (function () { let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (let i = 0; i < originalTargetKeypoints.length; i += 3) { if (i + 2 >= originalTargetKeypoints.length) break; const conf = originalTargetKeypoints[i + 2]; if (conf > 0.1) { let x = originalTargetKeypoints[i]; let y = originalTargetKeypoints[i + 1]; let cx, cy; if (isNormalized) { cx = fit.offsetX + (x * dataResolutionWidth) * fit.scale; cy = fit.offsetY + (y * dataResolutionHeight) * fit.scale; } else if (isCanvasUnit) { cx = x; cy = y; } else { cx = fit.offsetX + x * fit.scale; cy = fit.offsetY + y * fit.scale; } minX = Math.min(minX, cx); minY = Math.min(minY, cy); maxX = Math.max(maxX, cx); maxY = Math.max(maxY, cy); } } const margin = 8; const bx = isFinite(minX) ? minX - margin : originalRect.x; const by = isFinite(minY) ? minY - margin : originalRect.y; const bw = (isFinite(maxX) && isFinite(minX)) ? (maxX - minX) + margin * 2 : originalRect.width; const bh = (isFinite(maxY) && isFinite(minY)) ? (maxY - minY) + margin * 2 : originalRect.height; const rr = { x: bx, y: by, width: Math.max(1, bw), height: Math.max(1, bh) }; if (window.poseEditorDebug.rect) console.log('🔧 [transformKeypointsDirectly] Using bbox as refRect', { refRect: rr, originalRect }); return rr; })(); // キーポイントを一括変換(元データから毎回変換) let debugSampleIndex = -1; for (let i = 0; i < originalTargetKeypoints.length; i += 3) { if (i + 2 < originalTargetKeypoints.length && i + 2 < targetKeypoints.length) { const confidence = originalTargetKeypoints[i + 2]; if (confidence > 0.1) { // 🎯 元データから取得(累積変形防止) let x = originalTargetKeypoints[i]; let y = originalTargetKeypoints[i + 1]; // データ座標→Canvas座標(入力の座標系に応じて) let canvasX, canvasY; if (isNormalized) { canvasX = fit.offsetX + (x * dataResolutionWidth) * fit.scale; canvasY = fit.offsetY + (y * dataResolutionHeight) * fit.scale; } else if (isCanvasUnit) { // 既にCanvas座標(過去のバグで混入している場合) canvasX = x; canvasY = y; } else { canvasX = fit.offsetX + x * fit.scale; canvasY = fit.offsetY + y * fit.scale; } // 元矩形内での相対位置を計算(参照矩形に対して) let relativeX = (canvasX - refRect.x) / refRect.width; let relativeY = (canvasY - refRect.y) / refRect.height; // 安定化のために0-1へクランプ relativeX = Math.max(0, Math.min(1, relativeX)); relativeY = Math.max(0, Math.min(1, relativeY)); // 新矩形での新しい位置を計算 const newCanvasX = newRect.x + (relativeX * newRect.width); const newCanvasY = newRect.y + (relativeY * newRect.height); // Canvas座標→データ座標に戻す(常にデータ座標で保存) let finalX, finalY; if (isNormalized) { const dataX = (newCanvasX - fit.offsetX) / fit.scale; const dataY = (newCanvasY - fit.offsetY) / fit.scale; finalX = dataX / dataResolutionWidth; finalY = dataY / dataResolutionHeight; targetKeypoints[i] = finalX; targetKeypoints[i + 1] = finalY; } else { finalX = (newCanvasX - fit.offsetX) / fit.scale; finalY = (newCanvasY - fit.offsetY) / fit.scale; targetKeypoints[i] = finalX; targetKeypoints[i + 1] = finalY; } // 🔍 最初のサンプルポイントのみ詳細ログ出力 if (debugSampleIndex < 0) { debugSampleIndex = i / 3; if (window.poseEditorDebug.rect) console.log(`🔍 [DEBUG ${rectType}] 座標変換詳細 (Point ${debugSampleIndex}):`, { originalData: { x: x, y: y }, canvasCoord: { x: canvasX, y: canvasY }, relative: { x: relativeX, y: relativeY }, newCanvas: { x: newCanvasX, y: newCanvasY }, finalData: { x: finalX, y: finalY }, deltaCanvas: { x: newCanvasX - canvasX, y: newCanvasY - canvasY }, deltaData: { x: finalX - x, y: finalY - y } }); } } } } // 🔍 手のデータの場合:変換前後の全データ比較ログ if (rectType === 'leftHand' || rectType === 'rightHand') { if (window.poseEditorDebug.rect) console.log(`🔍 [${rectType}] 変換前後データ比較:`, { rectType: rectType, originalFirstPoint: originalTargetKeypoints.length >= 3 ? { x: originalTargetKeypoints[0], y: originalTargetKeypoints[1], conf: originalTargetKeypoints[2] } : null, transformedFirstPoint: targetKeypoints.length >= 3 ? { x: targetKeypoints[0], y: targetKeypoints[1], conf: targetKeypoints[2] } : null, originalSample3Points: [ originalTargetKeypoints.slice(0, 9), // 最初の3点 originalTargetKeypoints.slice(18, 27), // 中間3点 originalTargetKeypoints.slice(54, 63) // 最後の3点 ], transformedSample3Points: [ targetKeypoints.slice(0, 9), // 最初の3点 targetKeypoints.slice(18, 27), // 中間3点 targetKeypoints.slice(54, 63) // 最後の3点 ], coordinateShift: targetKeypoints.length >= 3 ? { deltaX: targetKeypoints[0] - originalTargetKeypoints[0], deltaY: targetKeypoints[1] - originalTargetKeypoints[1] } : null }); } } // 🔧 元座標からキーポイントを移動(累積移動防止版) function moveKeypointsFromOriginal(rectType, totalDeltaX, totalDeltaY) { console.log('🔍 [moveKeypointsFromOriginal] Called with:', { rectType, totalDeltaX, totalDeltaY, hasOriginalKeypoints: !!window.poseEditorGlobals.originalKeypoints, hasBaseOriginalKeypoints: !!window.poseEditorGlobals.baseOriginalKeypoints }); const currentPoseData = window.poseEditorGlobals.poseData || poseData; const originalKeypoints = window.poseEditorGlobals.originalKeypoints; if (!currentPoseData || !currentPoseData.people || !currentPoseData.people[0]) { return; } if (!originalKeypoints || !originalKeypoints.people || !originalKeypoints.people[0]) { return; } const person = currentPoseData.people[0]; const originalPerson = originalKeypoints.people[0]; // 対象キーポイントデータを取得 let targetKeypoints, originalTargetKeypoints; let fieldName; switch (rectType) { case 'face': targetKeypoints = person.face_keypoints_2d; originalTargetKeypoints = originalPerson.face_keypoints_2d; fieldName = 'face_keypoints_2d'; break; case 'leftHand': targetKeypoints = person.hand_left_keypoints_2d; originalTargetKeypoints = originalPerson.hand_left_keypoints_2d; fieldName = 'hand_left_keypoints_2d'; break; case 'rightHand': targetKeypoints = person.hand_right_keypoints_2d; originalTargetKeypoints = originalPerson.hand_right_keypoints_2d; fieldName = 'hand_right_keypoints_2d'; break; default: return; } if (!targetKeypoints || !Array.isArray(targetKeypoints)) { return; } if (!originalTargetKeypoints || !Array.isArray(originalTargetKeypoints)) { return; } // 座標変換設定 const canvasWidth = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.width : 512; const canvasHeight = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.height : 512; let dataResolutionWidth = canvasWidth; let dataResolutionHeight = canvasHeight; // 解像度情報の取得 if (currentPoseData.resolution && Array.isArray(currentPoseData.resolution) && currentPoseData.resolution.length >= 2) { dataResolutionWidth = currentPoseData.resolution[0]; dataResolutionHeight = currentPoseData.resolution[1]; } const fit = getFitParams([dataResolutionWidth, dataResolutionHeight]); // 正規化座標かピクセル座標かを判定 let isNormalized = false; if (originalTargetKeypoints.length > 0) { for (let i = 0; i < originalTargetKeypoints.length; i += 3) { if (i + 2 < originalTargetKeypoints.length && originalTargetKeypoints[i + 2] > 0) { const x = originalTargetKeypoints[i]; const y = originalTargetKeypoints[i + 1]; isNormalized = (x >= 0 && x <= 1 && y >= 0 && y <= 1); break; } } } // キーポイントを移動(元データから移動量を適用) for (let i = 0; i < originalTargetKeypoints.length; i += 3) { if (i + 2 < originalTargetKeypoints.length && i + 2 < targetKeypoints.length) { const confidence = originalTargetKeypoints[i + 2]; if (confidence > 0.1) { // 元データから取得 let origX = originalTargetKeypoints[i]; let origY = originalTargetKeypoints[i + 1]; // データ座標→Canvas座標 let canvasX, canvasY; if (isNormalized) { canvasX = fit.offsetX + (origX * dataResolutionWidth) * fit.scale; canvasY = fit.offsetY + (origY * dataResolutionHeight) * fit.scale; } else { canvasX = fit.offsetX + origX * fit.scale; canvasY = fit.offsetY + origY * fit.scale; } // 移動量を適用 const newCanvasX = canvasX + totalDeltaX; const newCanvasY = canvasY + totalDeltaY; // Canvas座標→データ座標に戻す if (isNormalized) { const dataX = (newCanvasX - fit.offsetX) / fit.scale; const dataY = (newCanvasY - fit.offsetY) / fit.scale; targetKeypoints[i] = dataX / dataResolutionWidth; targetKeypoints[i + 1] = dataY / dataResolutionHeight; } else { targetKeypoints[i] = (newCanvasX - fit.offsetX) / fit.scale; targetKeypoints[i + 1] = (newCanvasY - fit.offsetY) / fit.scale; } } } } } // 🔧 矩形移動に合わせてキーポイントを移動(refs互換版) function moveKeypointsWithRect(rectType, deltaX, deltaY) { const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (!currentPoseData || !currentPoseData.people || !currentPoseData.people[0]) { return; } const person = currentPoseData.people[0]; // 対応するキーポイントを取得 let keypoints = null; let fieldName = ''; switch (rectType) { case 'face': keypoints = person.face_keypoints_2d; fieldName = 'face_keypoints_2d'; break; case 'leftHand': keypoints = person.hand_left_keypoints_2d; fieldName = 'hand_left_keypoints_2d'; break; case 'rightHand': keypoints = person.hand_right_keypoints_2d; fieldName = 'hand_right_keypoints_2d'; break; default: return; } if (!keypoints || !Array.isArray(keypoints)) { return; } // Canvas→データ座標への変換係数 let dataResolutionWidth = 512; let dataResolutionHeight = 512; if (currentPoseData.metadata && currentPoseData.metadata.resolution && Array.isArray(currentPoseData.metadata.resolution) && currentPoseData.metadata.resolution.length >= 2) { dataResolutionWidth = currentPoseData.metadata.resolution[0]; dataResolutionHeight = currentPoseData.metadata.resolution[1]; } else if (currentPoseData.resolution && Array.isArray(currentPoseData.resolution) && currentPoseData.resolution.length >= 2) { dataResolutionWidth = currentPoseData.resolution[0]; dataResolutionHeight = currentPoseData.resolution[1]; } const canvasWidth = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.width : 512; const canvasHeight = window.poseEditorGlobals.canvas ? window.poseEditorGlobals.canvas.height : 512; // レターボックス対応フィット const fit = getFitParams([dataResolutionWidth, dataResolutionHeight]); // Canvasの移動量→データ座標の移動量へ変換 const dataDeltaX = deltaX / fit.scale; const dataDeltaY = deltaY / fit.scale; // すべてのキーポイントを移動(データ座標系) for (let i = 0; i < keypoints.length; i += 3) { const confidence = keypoints[i + 2]; if (confidence > 0.1) { // 有効なキーポイントのみ移動 keypoints[i] += dataDeltaX; // X座標(データ座標) keypoints[i + 1] += dataDeltaY; // Y座標(データ座標) } } // データを更新 person[fieldName] = keypoints; } // 🚀 全データをエクスポート用フォーマットに同期(refs互換) function syncAllDataToExportFormat(poseData) { if (!poseData) return; // 1. bodies.candidate → people.pose_keypoints_2d 同期 syncBodiesToPeople(poseData); // 2. 手の編集データがあれば元データにも同期 if (poseData.people && poseData.people[0]) { const person = poseData.people[0]; // 左手同期 if (person.hand_left_keypoints_2d) { syncHandsToOriginal(poseData, 'left', person.hand_left_keypoints_2d); } // 右手同期 if (person.hand_right_keypoints_2d) { syncHandsToOriginal(poseData, 'right', person.hand_right_keypoints_2d); } // 顔同期 if (person.face_keypoints_2d) { syncFacesToOriginal(poseData, person.face_keypoints_2d); } } } // マウス移動処理(refs互換 + 矩形編集対応) function handleMouseMove(event) { if (!isCanvasReady()) return; const mousePos = getMousePos(event); if (isDragging) { // 🔧 矩形コントロールポイントドラッグ処理 if (window.poseEditorGlobals.draggedRectControl) { updateRectControlDrag(mousePos.x, mousePos.y); return; } // 🔧 矩形移動ドラッグ処理 if (window.poseEditorGlobals.draggedRect) { updateRectMoveDrag(mousePos.x, mousePos.y); return; } // 🎯 詳細モードの手・顔・ボディキーポイントドラッグ処理 if (window.poseEditorGlobals.draggedDetailKeypoint) { updateDetailKeypointPosition(window.poseEditorGlobals.draggedDetailKeypoint, mousePos.x, mousePos.y); // リアルタイム再描画(エラーハンドリング付き・手顔データ保持強化) try { const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (currentPoseData) { // 🫳😊 手と顔データが存在することを確認してから描画 if (currentPoseData.people && currentPoseData.people[0]) { const person = currentPoseData.people[0]; } drawPose(currentPoseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace); } else { } } catch (error) { console.error("❌ Error in real-time redraw:", error); } } // 🔧 通常のボディキーポイントドラッグ処理 else if (draggedKeypoint >= 0) { // 🔧 refs互換:オフセット考慮の新座標計算 const newX = mousePos.x - dragOffset.x; const newY = mousePos.y - dragOffset.y; // ドラッグ中の座標更新(オフセット適用済み座標で) updateKeypointPosition(draggedKeypoint, newX, newY); // リアルタイム再描画(ハイライト付き)- エラーハンドリング付き・手顔データ保持強化 try { const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (currentPoseData) { // 🫳😊 手と顔データの存在確認 drawPose(currentPoseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace, draggedKeypoint); } else { } } catch (error) { console.error("❌ Error in highlighted redraw:", error); } } } else { // 🔧 ホバー時のカーソル変更(編集モードに応じて) if (window.poseEditorGlobals.editMode === "簡易モード" && window.poseEditorGlobals.rectEditModeActive) { // 矩形編集モード中:コントロールポイントまたは矩形内でカーソル変更 const controlPoint = findNearestRectControlPoint(mousePos.x, mousePos.y); if (controlPoint) { canvas.style.cursor = getControlPointCursor(controlPoint.position); } else { const rectType = findRectContaining(mousePos.x, mousePos.y); canvas.style.cursor = rectType === window.poseEditorGlobals.rectEditMode ? 'move' : 'default'; } } else { // 🎯 詳細モードまたは通常モード:キーポイント近くでカーソル変更 if (window.poseEditorGlobals.editMode === "詳細モード") { const detailKeypoint = findNearestKeypointInDetailMode(mousePos.x, mousePos.y); canvas.style.cursor = detailKeypoint ? 'grab' : 'default'; } else { const keypointIndex = findNearestKeypoint(mousePos.x, mousePos.y); canvas.style.cursor = keypointIndex >= 0 ? 'grab' : 'default'; } } } } // 🔧 コントロールポイント位置に応じたカーソル取得 function getControlPointCursor(position) { const cursors = { 'tl': 'nw-resize', 'tr': 'ne-resize', 'bl': 'sw-resize', 'br': 'se-resize', 't': 'n-resize', 'r': 'e-resize', 'b': 's-resize', 'l': 'w-resize' }; return cursors[position] || 'pointer'; } // マウスアップ処理(refs互換 + 矩形編集対応) function handleMouseUp(event) { if (isDragging) { if (window.poseEditorGlobals.draggedRectControl) { window.poseEditorGlobals.draggedRectControl = null; window.poseEditorGlobals.originalRect = null; // 🔧 元の矩形情報をクリア } else if (window.poseEditorGlobals.draggedRect) { window.poseEditorGlobals.draggedRect = null; } else if (window.poseEditorGlobals.draggedDetailKeypoint) { // 🎯 詳細モードキーポイントドラッグ終了 window.poseEditorGlobals.draggedDetailKeypoint = null; } else if (draggedKeypoint >= 0) { draggedKeypoint = -1; } isDragging = false; canvas.style.cursor = 'default'; // 🚀 編集完了データをGradio送信(データ改変はしない) sendPoseDataToGradio(); } } // キーポイント座標更新(refs互換・people形式同期対応) function updateKeypointPosition(keypointIndex, canvasX, canvasY) { // グローバルposeDataを参照 const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (!currentPoseData || !currentPoseData.bodies || !currentPoseData.bodies.candidate) { return; } // 📐 解像度情報の取得 const originalRes = currentPoseData.resolution || [512, 512]; // Canvas→データ変換(レターボックス対応) const d = canvasToDataXY(canvasX, canvasY, originalRes); const clampedDataX = d.x; const clampedDataY = d.y; // 1. candidateリストを更新 const candidates = currentPoseData.bodies.candidate; if (keypointIndex < candidates.length) { candidates[keypointIndex][0] = clampedDataX; candidates[keypointIndex][1] = clampedDataY; // 🔧 ローカルposeDataも同期更新 if (poseData && poseData.bodies && poseData.bodies.candidate) { poseData.bodies.candidate[keypointIndex][0] = clampedDataX; poseData.bodies.candidate[keypointIndex][1] = clampedDataY; } } // 2. 🚀 people形式にも同期更新(export用) syncBodiesToPeople(currentPoseData); // 3. 🫳😊 手と顔データも強制同期(編集後の表示維持のため) if (currentPoseData.people && currentPoseData.people[0]) { const person = currentPoseData.people[0]; // 💖 手データの同期(people形式のみ) if (person.hand_left_keypoints_2d) { syncHandsToOriginal(currentPoseData, 'left', person.hand_left_keypoints_2d); } if (person.hand_right_keypoints_2d) { syncHandsToOriginal(currentPoseData, 'right', person.hand_right_keypoints_2d); } // 顔データの同期 if (person.face_keypoints_2d && currentPoseData.faces) { syncFacesToOriginal(currentPoseData, person.face_keypoints_2d); } } } // 🚀 bodies.candidateからpeople.pose_keypoints_2dに同期(refs互換) function syncBodiesToPeople(poseData) { if (!poseData || !poseData.bodies || !poseData.bodies.candidate) { return; } // people形式が存在しない場合は作成 if (!poseData.people) { poseData.people = [{}]; } if (!poseData.people[0]) { poseData.people[0] = {}; } const candidates = poseData.bodies.candidate; const person = poseData.people[0]; // candidatesからpose_keypoints_2dフラット配列を生成 const pose_keypoints_2d = []; for (let i = 0; i < candidates.length; i++) { const candidate = candidates[i]; if (candidate && candidate.length >= 2) { pose_keypoints_2d.push(candidate[0]); // x pose_keypoints_2d.push(candidate[1]); // y pose_keypoints_2d.push(candidate[2] || 1.0); // confidence (デフォルト1.0) } else { // 無効なキーポイントは0で埋める pose_keypoints_2d.push(0, 0, 0); } } person.pose_keypoints_2d = pose_keypoints_2d; // 🫳😊 既存の手と顔データを保持(ボディ編集時に消失しないように) if (!person.hand_left_keypoints_2d && poseData.hands && poseData.hands[0]) { person.hand_left_keypoints_2d = poseData.hands[0]; } if (!person.hand_right_keypoints_2d && poseData.hands && poseData.hands[1]) { person.hand_right_keypoints_2d = poseData.hands[1]; } if (!person.face_keypoints_2d && poseData.faces && poseData.faces.length > 0) { person.face_keypoints_2d = poseData.faces[0] || poseData.faces; } } // Gradioにデータ送信(refs互換・強制changeイベント版) function sendPoseDataToGradio() { if (window.poseEditorDebug.send) console.log('🔍 [sendPoseDataToGradio] Called'); // グローバルposeDataを参照 const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (window.poseEditorDebug.send) console.log('🔍 [sendPoseDataToGradio] currentPoseData state:', { exists: !!currentPoseData, hasPeople: !!currentPoseData?.people, peopleCount: currentPoseData?.people?.length || 0, hasHandLeft: !!currentPoseData?.people?.[0]?.hand_left_keypoints_2d, hasHandRight: !!currentPoseData?.people?.[0]?.hand_right_keypoints_2d, hasFace: !!currentPoseData?.people?.[0]?.face_keypoints_2d }); if (!currentPoseData) { return; } // Issue 038: JavaScript側更新フラグチェック(refs issue043準拠) if (window.poseEditorGlobals && window.poseEditorGlobals.isUpdating) { return; } // 処理開始フラグを立てる if (window.poseEditorGlobals) { window.poseEditorGlobals.isUpdating = true; } try { // Issue #038: people形式でPythonに送信(refs互換) const canvasData = { "people": [], "_t": Date.now() // タイムスタンプで強制イベント発火 }; // 常に最新データで構築(people形式があっても再構築) if (currentPoseData.bodies && currentPoseData.bodies.candidate) { // bodies.candidateからpeople形式に変換 const pose_keypoints_2d = []; const candidates = currentPoseData.bodies.candidate; for (let i = 0; i < candidates.length; i++) { const candidate = candidates[i]; if (candidate && candidate.length >= 2) { pose_keypoints_2d.push(candidate[0]); // x pose_keypoints_2d.push(candidate[1]); // y pose_keypoints_2d.push(candidate.length > 2 ? candidate[2] : 1.0); // confidence } } const person = { "pose_keypoints_2d": pose_keypoints_2d }; // 🫳 手のデータを追加(複数のソースから確実に取得) // people形式から取得を試行 let leftHandData = null; let rightHandData = null; let faceData = null; // 💖 people形式からのみデータ取得(フォールバック削除) if (currentPoseData.people && currentPoseData.people[0]) { leftHandData = currentPoseData.people[0].hand_left_keypoints_2d || []; rightHandData = currentPoseData.people[0].hand_right_keypoints_2d || []; faceData = currentPoseData.people[0].face_keypoints_2d || []; } if (!faceData && currentPoseData.faces && currentPoseData.faces.length > 0) { faceData = currentPoseData.faces[0] || currentPoseData.faces; } // 💖 手と顔データを確実に設定(配列コピーで安全に保持) if (leftHandData && leftHandData.length > 0) { person.hand_left_keypoints_2d = [...leftHandData]; // 配列コピー } else { } if (rightHandData && rightHandData.length > 0) { person.hand_right_keypoints_2d = [...rightHandData]; // 配列コピー } else { } if (faceData && faceData.length > 0) { person.face_keypoints_2d = [...faceData]; // 配列コピー } else { } canvasData.people = [person]; } else { // bodies.candidateがない場合でも手と顔データがあれば送信 const person = { "pose_keypoints_2d": [] // 空のボディデータ }; // 手と顔データのみ追加 if (currentPoseData.people && currentPoseData.people[0]) { const existingPerson = currentPoseData.people[0]; if (existingPerson.hand_left_keypoints_2d) { person.hand_left_keypoints_2d = existingPerson.hand_left_keypoints_2d; } if (existingPerson.hand_right_keypoints_2d) { person.hand_right_keypoints_2d = existingPerson.hand_right_keypoints_2d; } if (existingPerson.face_keypoints_2d) { person.face_keypoints_2d = existingPerson.face_keypoints_2d; } } // 💖 people形式で手・顔データ存在確認 const hasHandsOrFace = currentPoseData.people && currentPoseData.people[0] && (currentPoseData.people[0].hand_left_keypoints_2d || currentPoseData.people[0].hand_right_keypoints_2d || currentPoseData.people[0].face_keypoints_2d) || currentPoseData.faces; if (hasHandsOrFace) { canvasData.people = [person]; } } // 解像度情報を保持 if (currentPoseData.resolution) { canvasData.resolution = currentPoseData.resolution; } // 送信前の最終データ確認 if (window.poseEditorDebug.send) console.log('🔍 [sendPoseDataToGradio] Final canvasData being sent:', { hasPeople: !!canvasData.people, peopleCount: canvasData.people?.length || 0, hasHandLeft: !!canvasData.people?.[0]?.hand_left_keypoints_2d, hasHandRight: !!canvasData.people?.[0]?.hand_right_keypoints_2d, hasFace: !!canvasData.people?.[0]?.face_keypoints_2d, handLeftLength: canvasData.people?.[0]?.hand_left_keypoints_2d?.length || 0, handRightLength: canvasData.people?.[0]?.hand_right_keypoints_2d?.length || 0, faceLength: canvasData.people?.[0]?.face_keypoints_2d?.length || 0 }); const jsonString = JSON.stringify(canvasData); if (window.poseEditorDebug.send) console.log('🎯 [sendPoseDataToGradio] People形式でGradioに送信:', canvasData.people.length, 'people'); // 専用の隠しテキストボックスを探して更新 const jsUpdateTextbox = document.querySelector('#js_pose_update textarea'); if (jsUpdateTextbox) { jsUpdateTextbox.value = jsonString; jsUpdateTextbox.dispatchEvent(new Event('input', { bubbles: true })); return; } // フォールバック:従来の方法 const textareas = document.querySelectorAll('textarea'); for (const textarea of textareas) { const currentValue = textarea.value || ''; if (currentValue.includes('bodies') || currentValue.includes('candidate') || currentValue.trim() === '') { textarea.value = jsonString; textarea.dispatchEvent(new Event('input', { bubbles: true })); return; } } } catch (error) { } finally { // Issue 038: 処理完了後は必ずフラグを解除(refs issue043準拠) if (window.poseEditorGlobals) { const oldFlag = window.poseEditorGlobals.isUpdating; window.poseEditorGlobals.isUpdating = false; } } } // Canvas状態チェック(refs互換) function isCanvasReady() { const ready = window.poseEditorGlobals.canvas && window.poseEditorGlobals.ctx && isInitialized; return ready; } // ポーズ全体の描画(ハイライト対応・設定制御) function drawPose(poseData, enableHands = true, enableFace = true, highlightIndex = -1) { if (!isCanvasReady() || !poseData) { return; } // 🚀 refs互換:常に最新の編集済みデータを使用 const currentPoseData = window.poseEditorGlobals.poseData || poseData; const canvas = window.poseEditorGlobals.canvas; const ctx = window.poseEditorGlobals.ctx; // キャンバスクリア ctx.clearRect(0, 0, canvas.width, canvas.height); // 🎨 背景画像描画 drawBackground(); // 🔧 矩形編集モード中でない場合のみ矩形を再計算 if (window.poseEditorGlobals.editMode === "簡易モード" && !window.poseEditorGlobals.rectEditModeActive) { window.poseEditorGlobals.currentRects = { leftHand: null, rightHand: null, face: null }; } // 🎯 詳細モードでは常に矩形をクリア else if (window.poseEditorGlobals.editMode === "詳細モード") { window.poseEditorGlobals.currentRects = { leftHand: null, rightHand: null, face: null }; } // 📐 解像度情報の取得(手と顔描画のため) const originalRes = currentPoseData.resolution || [512, 512]; const fit = getFitParams(originalRes); // ボディの描画(ハイライト対応) drawBody(currentPoseData, highlightIndex); // 手の描画(設定制御・座標変換パラメータ付き・エラー耐性強化) if (enableHands) { try { // 🚀 refs互換:編集済みのhand_keypoints_2dを優先使用、フォールバック付き let handsDataForDrawing = null; if (currentPoseData.people && currentPoseData.people[0]) { const person = currentPoseData.people[0]; handsDataForDrawing = [ person.hand_left_keypoints_2d || [], person.hand_right_keypoints_2d || [] ]; // 🔧 Issue #043: 手データのデバッグ情報追加 if (window.poseEditorDebug.hands) console.log('🫳 Hand data debug:', { enableHands: enableHands, leftHandLength: handsDataForDrawing[0].length, rightHandLength: handsDataForDrawing[1].length, leftHandSample: handsDataForDrawing[0].slice(0, 9), // 最初の3点 rightHandSample: handsDataForDrawing[1].slice(0, 9) // 最初の3点 }); // 💖 people形式のみサポート、古いhands形式は削除 } else { handsDataForDrawing = [[], []]; // 空の手データ if (window.poseEditorDebug.hands) console.log('🚫 No people data available for hands'); } if (handsDataForDrawing && handsDataForDrawing.length >= 2) { if (window.poseEditorDebug.hands) console.log('✅ Calling drawHands function'); drawHands(handsDataForDrawing, originalRes); } else { if (window.poseEditorDebug.hands) console.log('❌ Invalid hands data for drawing'); } } catch (error) { console.error("❌ Error drawing hands:", error); } // 🔧 簡易モード:手の矩形描画(編集モード中は再計算しない) if (window.poseEditorGlobals.editMode === "簡易モード") { if (!window.poseEditorGlobals.rectEditModeActive) { // 🚀 通常時:編集済みキーポイントから矩形を計算 if (currentPoseData.people && currentPoseData.people[0]) { const person = currentPoseData.people[0]; const editedHandsData = [ person.hand_left_keypoints_2d || [], person.hand_right_keypoints_2d || [] ]; drawHandRectangles(editedHandsData, originalRes); } else { // 💖 people形式のみサポート、空データで矩形なし drawHandRectangles([[], []], originalRes); } } else { // 編集モード中:既存の矩形を描画(再計算しない) drawExistingRectangles(['leftHand', 'rightHand']); } } else { } } else if (!enableHands) { } else { } // 顔の描画(設定制御・座標変換パラメータ付き・エラー耐性強化) if (enableFace) { try { // 🚀 refs互換:編集済みのface_keypoints_2dを優先使用、フォールバック付き let facesDataForDrawing = null; if (currentPoseData.people && currentPoseData.people[0] && currentPoseData.people[0].face_keypoints_2d) { facesDataForDrawing = [currentPoseData.people[0].face_keypoints_2d]; } else if (currentPoseData.faces && Array.isArray(currentPoseData.faces)) { facesDataForDrawing = currentPoseData.faces; } else { facesDataForDrawing = [[]]; // 空の顔データ } if (facesDataForDrawing && facesDataForDrawing.length > 0) { drawFaces(facesDataForDrawing, originalRes); } } catch (error) { console.error("❌ Error drawing face:", error); } // 🔧 簡易モード:顔の矩形描画(編集モード中は再計算しない) if (window.poseEditorGlobals.editMode === "簡易モード") { if (!window.poseEditorGlobals.rectEditModeActive) { // 🚀 通常時:編集済みキーポイントから矩形を計算 if (currentPoseData.people && currentPoseData.people[0] && currentPoseData.people[0].face_keypoints_2d) { const editedFacesData = [currentPoseData.people[0].face_keypoints_2d]; drawFaceRectangles(editedFacesData, originalRes); } else { drawFaceRectangles(currentPoseData.faces, originalRes); } } else { // 編集モード中:既存の矩形を描画(再計算しない) drawExistingRectangles(['face']); } } else { } } else if (!enableFace) { } else { } } // 🎨 背景画像描画(refs互換) function drawBackground() { if (!isCanvasReady()) return; const canvas = window.poseEditorGlobals.canvas; const ctx = window.poseEditorGlobals.ctx; // デフォルト背景(白) ctx.fillStyle = '#ffffff'; ctx.fillRect(0, 0, canvas.width, canvas.height); // 背景画像がある場合はアスペクト比維持でフィット(上下黒帯 or 左右黒帯) if (window.poseEditorGlobals.backgroundImage) { const img = window.poseEditorGlobals.backgroundImage; const imgAspect = img.width / img.height; const canvasAspect = canvas.width / canvas.height; let drawWidth, drawHeight, offsetX, offsetY; if (imgAspect > canvasAspect) { // 画像の方が横長 → 幅をCanvasに合わせて上下黒帯 drawWidth = canvas.width; drawHeight = Math.round(canvas.width / imgAspect); offsetX = 0; offsetY = Math.round((canvas.height - drawHeight) / 2); } else { // 画像の方が縦長 → 高さをCanvasに合わせて左右黒帯 drawHeight = canvas.height; drawWidth = Math.round(canvas.height * imgAspect); offsetX = Math.round((canvas.width - drawWidth) / 2); offsetY = 0; } ctx.globalAlpha = 0.3; ctx.drawImage(img, offsetX, offsetY, drawWidth, drawHeight); ctx.globalAlpha = 1.0; } } // ボディ描画(ハイライト対応) function drawBody(poseData, highlightIndex = -1) { // people形式からbodies形式への変換を試行 let candidatesData = null; if (poseData.bodies && poseData.bodies.candidate) { candidatesData = poseData.bodies.candidate; } else if (poseData.people && poseData.people[0] && poseData.people[0].pose_keypoints_2d) { // people形式からcandidate形式に変換 const pose_keypoints_2d = poseData.people[0].pose_keypoints_2d; candidatesData = []; for (let i = 0; i < pose_keypoints_2d.length; i += 3) { if (i + 2 < pose_keypoints_2d.length) { candidatesData.push([pose_keypoints_2d[i], pose_keypoints_2d[i+1], pose_keypoints_2d[i+2]]); } } } else { return; } const canvas = window.poseEditorGlobals.canvas; const ctx = window.poseEditorGlobals.ctx; const candidates = candidatesData; const subset = (poseData.bodies && poseData.bodies.subset) || []; if (subset.length === 0) { // No subset data, using all candidates directly } else { // 最初の人物のみ描画(単一人物想定) const person = subset[0]; const personIndices = person[0]; // インデックス配列を取得 } // 📐 解像度情報の取得 const originalRes = poseData.resolution || [512, 512]; const fit = getFitParams(originalRes); // 接続線の描画(refs互換・配列ベース + 座標変換) ctx.lineWidth = 3; let drawnConnections = 0; for (let i = 0; i < BODY_CONNECTIONS.length; i++) { const [start, end] = BODY_CONNECTIONS[i]; if (start < candidates.length && end < candidates.length) { const startPoint = candidates[start]; const endPoint = candidates[end]; // 🚫 無効座標をフィルタリング(0,0や範囲外も除外) if (startPoint && endPoint && startPoint[0] > 1 && startPoint[1] > 1 && endPoint[0] > 1 && endPoint[1] > 1 && startPoint[0] < originalRes[0] && startPoint[1] < originalRes[1] && endPoint[0] < originalRes[0] && endPoint[1] < originalRes[1]) { // 🔄 座標変換を適用(レターボックス対応) const startX = fit.offsetX + startPoint[0] * fit.scale; const startY = fit.offsetY + startPoint[1] * fit.scale; const endX = fit.offsetX + endPoint[0] * fit.scale; const endY = fit.offsetY + endPoint[1] * fit.scale; // 🔧 refs互換: SKELETON_COLORSの配列ベース色分け ctx.strokeStyle = SKELETON_COLORS[i % SKELETON_COLORS.length]; ctx.beginPath(); ctx.moveTo(startX, startY); ctx.lineTo(endX, endY); ctx.stroke(); drawnConnections++; } else { } } } // console.log(`[DEBUG] ✨ Drew ${drawnConnections} valid connections out of ${BODY_CONNECTIONS.length}`); // キーポイントの描画(20個・つま先込み・配列ベース色分け + 座標変換) const maxKeypoints = Math.min(20, candidates.length); // つま先込み20個 let drawnKeypoints = 0; for (let i = 0; i < maxKeypoints; i++) { const point = candidates[i]; // 🚫 無効座標をフィルタリング(0,0や範囲外も除外) if (point && point[0] > 1 && point[1] > 1 && point[0] < originalRes[0] && point[1] < originalRes[1]) { // 🔄 座標変換を適用(レターボックス対応) const scaledX = fit.offsetX + point[0] * fit.scale; const scaledY = fit.offsetY + point[1] * fit.scale; // 🔧 ハイライト対応: ドラッグ中のキーポイントを強調表示 if (i === highlightIndex) { ctx.fillStyle = 'rgb(255,255,0)'; // 黄色でハイライト drawKeypoint(scaledX, scaledY, KEYPOINT_RADIUS + 2); // 少し大きく } else { ctx.fillStyle = SKELETON_COLORS[i % SKELETON_COLORS.length]; drawKeypoint(scaledX, scaledY); } drawnKeypoints++; if (i < 5) { // 最初の5つのキーポイントをログ // console.log(`[DEBUG] ✅ Keypoint ${i}: (${point[0]}, ${point[1]}) → (${scaledX.toFixed(1)}, ${scaledY.toFixed(1)}) color=${SKELETON_COLORS[i % SKELETON_COLORS.length]}`); } } else { if (i < 5) { // 最初の5つの無効キーポイントをログ // console.log(`[DEBUG] 🚫 Skipped keypoint ${i}: (${point ? point[0] : 'null'}, ${point ? point[1] : 'null'}) invalid`); } } } // console.log(`[DEBUG] ✨ Drew ${drawnKeypoints} valid keypoints out of ${maxKeypoints}`); // 🎨 補間機能: 有効キーポイントが少ない場合の視覚的改善 if (drawnKeypoints < 10) { // console.log(`[DEBUG] 💡 Low keypoint count (${drawnKeypoints}), applying visual enhancements`); drawEstimatedConnections(candidates, originalRes); } } // キーポイント描画 function drawKeypoint(x, y, radius = KEYPOINT_RADIUS) { const ctx = window.poseEditorGlobals.ctx; ctx.beginPath(); ctx.arc(x, y, radius, 0, Math.PI * 2); ctx.fill(); } // 手の描画(21キーポイント × 2)- refs互換 function drawHands(handsData, originalRes, scaleX_unused, scaleY_unused) { if (!handsData || handsData.length === 0) return; // console.log(`[DEBUG] 👋 Drawing hands with ${handsData.length} hand(s) - refs互換`); // 手の接続定義(refsから完全コピー) const HAND_CONNECTIONS = [ // 親指 [0, 1], [1, 2], [2, 3], [3, 4], // 人差し指 [0, 5], [5, 6], [6, 7], [7, 8], // 中指 [0, 9], [9, 10], [10, 11], [11, 12], // 薬指 [0, 13], [13, 14], [14, 15], [15, 16], // 小指 [0, 17], [17, 18], [18, 19], [19, 20] ]; // 左右の手を描画 handsData.forEach((hand, handIndex) => { if (hand && hand.length > 0) { // 手のキーポイントを3要素ずつ解析 const handKeypoints = []; for (let i = 0; i < hand.length; i += 3) { const x = hand[i]; const y = hand[i + 1]; const conf = hand[i + 2]; if (conf > 0.1) { // refs互換の閾値 // 座標変換を適用(レターボックス対応) const pt = dataToCanvasXY(x, y, originalRes); const scaledX = pt.x; const scaledY = pt.y; handKeypoints.push([scaledX, scaledY, conf]); } else { handKeypoints.push([0, 0, 0]); // 無効キーポイント } } // 🔧 refs互換の色設定に修正: 手のキーポイントは青 const handColor = 'rgb(0,0,255)'; // refs互換: 手のキーポイントは青 const handName = handIndex === 0 ? '左手' : '右手'; // console.log(`[DEBUG] 👋 ${handName} drawing with color ${handColor}`); // 手の接続線を描画(refs互換: カラフル) ctx.lineWidth = 2; let drawnConnections = 0; for (let connIdx = 0; connIdx < HAND_CONNECTIONS.length; connIdx++) { const [start, end] = HAND_CONNECTIONS[connIdx]; if (start < handKeypoints.length && end < handKeypoints.length) { const startPoint = handKeypoints[start]; const endPoint = handKeypoints[end]; if (startPoint[2] > 0.1 && endPoint[2] > 0.1) { // 両方有効 // 🎨 refs互換: HSV→RGBでカラフルな線 const hue = (connIdx / HAND_CONNECTIONS.length) * 360; ctx.strokeStyle = `hsl(${hue}, 100%, 50%)`; ctx.beginPath(); ctx.moveTo(startPoint[0], startPoint[1]); ctx.lineTo(endPoint[0], endPoint[1]); ctx.stroke(); drawnConnections++; } } } // 手のキーポイントを描画 ctx.fillStyle = handColor; let drawnHandPoints = 0; for (let i = 0; i < handKeypoints.length; i++) { const [x, y, conf] = handKeypoints[i]; if (conf > 0.1) { drawKeypoint(x, y, 3); drawnHandPoints++; // 詳細ログ(最初の5個のみ) if (drawnHandPoints <= 5) { // console.log(`[DEBUG] 👋 ${handName} Point ${drawnHandPoints-1}: (${x.toFixed(1)},${y.toFixed(1)})`); } } } // console.log(`[DEBUG] ✋ ${handName}: drew ${drawnConnections} connections, ${drawnHandPoints} keypoints`); } }); } // 顔の描画(68キーポイント)- refs互換 function drawFaces(facesData, originalRes, scaleX_unused, scaleY_unused) { if (!facesData || facesData.length === 0) return; // console.log(`[DEBUG] 👤 Drawing faces with ${facesData.length} face(s) - refs互換`); const face = facesData[0]; // 最初の顔のみ if (face && face.length > 0) { // 顔のキーポイントを3要素ずつ解析 const faceKeypoints = []; for (let i = 0; i < face.length; i += 3) { const x = face[i]; const y = face[i + 1]; const conf = face[i + 2]; if (conf > 0.1) { // refs互換の閾値 // 座標変換を適用(レターボックス対応) const pt = dataToCanvasXY(x, y, originalRes); const scaledX = pt.x; const scaledY = pt.y; faceKeypoints.push([scaledX, scaledY, conf]); } else { faceKeypoints.push([0, 0, 0]); // 無効キーポイント } } // refs互換の顔描画(白い円) // console.log(`[DEBUG] 😊 Face drawing with white circles (refs互換)`); ctx.fillStyle = 'rgb(255,255,255)'; // 白色(refsと同じ) ctx.strokeStyle = 'rgb(0,0,0)'; // 黒枠(refsと同じ) ctx.lineWidth = 1; let drawnFacePoints = 0; for (let i = 0; i < faceKeypoints.length; i++) { const [x, y, conf] = faceKeypoints[i]; if (conf > 0.1) { // refs互換の顔キーポイント描画(白い円に黒枠) ctx.beginPath(); ctx.arc(x, y, 2, 0, 2 * Math.PI); ctx.fill(); ctx.stroke(); drawnFacePoints++; // 詳細ログ(最初の5個のみ) if (drawnFacePoints <= 5) { // console.log(`[DEBUG] 😊 Face Point ${drawnFacePoints-1}: (${x.toFixed(1)},${y.toFixed(1)})`); } } } // console.log(`[DEBUG] 😊 Face: drew ${drawnFacePoints} white circle keypoints`); } } // 座標変換システム let coordinateTransformer = { dataResolution: [512, 512], displayResolution: [640, 640], scaleX: 640 / 512, scaleY: 640 / 512, updateResolution: function(dataRes, displayRes) { this.dataResolution = dataRes || this.dataResolution; this.displayResolution = displayRes || this.displayResolution; this.scaleX = this.displayResolution[0] / this.dataResolution[0]; this.scaleY = this.displayResolution[1] / this.dataResolution[1]; }, dataToDisplay: function(x, y) { return { x: x * this.scaleX, y: y * this.scaleY }; }, displayToData: function(x, y) { return { x: x / this.scaleX, y: y / this.scaleY }; } }; // レターボックス対応のフィット変換(アスペクト比維持・黒帯) function getFitParams(originalRes) { const dataW = (originalRes && originalRes[0]) || 512; const dataH = (originalRes && originalRes[1]) || 512; const cw = canvas.width; const ch = canvas.height; const s = Math.min(cw / dataW, ch / dataH); const drawW = dataW * s; const drawH = dataH * s; const offsetX = (cw - drawW) / 2; const offsetY = (ch - drawH) / 2; return { scale: s, offsetX, offsetY }; } function dataToCanvasXY(x, y, originalRes) { const { scale, offsetX, offsetY } = getFitParams(originalRes); return { x: offsetX + x * scale, y: offsetY + y * scale }; } function canvasToDataXY(cx, cy, originalRes) { const dataW = (originalRes && originalRes[0]) || 512; const dataH = (originalRes && originalRes[1]) || 512; const { scale, offsetX, offsetY } = getFitParams(originalRes); const x = (cx - offsetX) / scale; const y = (cy - offsetY) / scale; return { x: Math.max(0, Math.min(dataW, x)), y: Math.max(0, Math.min(dataH, y)) }; } // データ解像度とCanvas表示サイズの変換(後方互換性) function transformCoordinate(x, y, dataWidth, dataHeight) { const scaleX = canvas.width / dataWidth; const scaleY = canvas.height / dataHeight; return { x: x * scaleX, y: y * scaleY }; } // 描画時に座標変換を適用 function drawKeypointScaled(x, y, dataRes, radius = KEYPOINT_RADIUS) { const scaled = dataToCanvasXY(x, y, dataRes); drawKeypoint(scaled.x, scaled.y, radius); } // Canvas解像度更新 // 既存キーポイントを新しいサイズへ変換(正規化スケーリング) function updateKeypointsForNewSize(p, oldW, oldH, newW, newH) { if (!p) return; const oW = Math.max(1, oldW || 512); const oH = Math.max(1, oldH || 512); const nW = Math.max(1, newW || oW); const nH = Math.max(1, newH || oH); function scaleXY(x, y) { // 正規化検出(0〜1範囲なら正規化座標とみなす) const isNorm = (x >= 0 && x <= 1 && y >= 0 && y <= 1); const nx = isNorm ? x * nW : (x / oW) * nW; const ny = isNorm ? y * nH : (y / oH) * nH; return [nx, ny]; } // bodies.candidate: [[x,y,conf,...], ...] try { if (p.bodies && Array.isArray(p.bodies.candidate)) { for (let i = 0; i < p.bodies.candidate.length; i++) { const pt = p.bodies.candidate[i]; if (pt && pt.length >= 2) { const [nx, ny] = scaleXY(pt[0], pt[1]); p.bodies.candidate[i][0] = nx; p.bodies.candidate[i][1] = ny; } } } } catch (e) { /* no-op */ } // people[0].pose_keypoints_2d: [x,y,conf, ...] try { if (p.people && p.people[0] && Array.isArray(p.people[0].pose_keypoints_2d)) { const arr = p.people[0].pose_keypoints_2d; for (let i = 0; i < arr.length; i += 3) { if (i + 1 < arr.length) { const [nx, ny] = scaleXY(arr[i], arr[i + 1]); arr[i] = nx; arr[i + 1] = ny; } } } } catch (e) { /* no-op */ } // hands (people形式優先) try { if (p.people && p.people[0]) { const person = p.people[0]; const handFields = ['hand_left_keypoints_2d', 'hand_right_keypoints_2d']; for (const field of handFields) { if (Array.isArray(person[field])) { for (let i = 0; i < person[field].length; i += 3) { if (i + 1 < person[field].length) { const [nx, ny] = scaleXY(person[field][i], person[field][i + 1]); person[field][i] = nx; person[field][i + 1] = ny; } } } } } } catch (e) { /* no-op */ } // faces (people形式優先) try { if (p.people && p.people[0] && Array.isArray(p.people[0].face_keypoints_2d)) { const arr = p.people[0].face_keypoints_2d; for (let i = 0; i < arr.length; i += 3) { if (i + 1 < arr.length) { const [nx, ny] = scaleXY(arr[i], arr[i + 1]); arr[i] = nx; arr[i + 1] = ny; } } } } catch (e) { /* no-op */ } // 旧形式 hands/faces も同期 try { if (Array.isArray(p.hands)) { for (let h = 0; h < p.hands.length; h++) { const hand = p.hands[h]; if (Array.isArray(hand)) { for (let i = 0; i < hand.length; i += 3) { if (i + 1 < hand.length) { const [nx, ny] = scaleXY(hand[i], hand[i + 1]); hand[i] = nx; hand[i + 1] = ny; } } } } } } catch (e) { /* no-op */ } try { if (Array.isArray(p.faces)) { for (let f = 0; f < p.faces.length; f++) { const face = p.faces[f]; if (Array.isArray(face)) { for (let i = 0; i < face.length; i += 3) { if (i + 1 < face.length) { const [nx, ny] = scaleXY(face[i], face[i + 1]); face[i] = nx; face[i + 1] = ny; } } } } } } catch (e) { /* no-op */ } // 解像度メタ更新 p.resolution = [nW, nH]; if (!p.metadata) p.metadata = {}; p.metadata.resolution = [nW, nH]; } // 出力フォームとposeData.resolutionの不一致を検出してデータ側をスケール function alignPoseDataToOutputForm() { try { const p = window.poseEditorGlobals.poseData || poseData; if (!p) return false; // 取得: 現在のデータ解像度 const curRes = (p.resolution || (p.metadata && p.metadata.resolution)) || [512,512]; let curW = curRes[0] || 512, curH = curRes[1] || 512; // 取得: フォームの出力サイズ const nums = Array.from(document.querySelectorAll('input[type="number"]')); let outW = null, outH = null; for (const el of nums) { const label = el.labels && el.labels[0] ? (el.labels[0].textContent || '').trim() : ''; if (label.includes('幅')) outW = parseInt(el.value || '0'); if (label.includes('高さ')) outH = parseInt(el.value || '0'); } if (!outW || !outH) return false; // すでに一致していれば何もしない if (curW === outW && curH === outH) return false; // データを新サイズへスケール updateKeypointsForNewSize(p, curW, curH, outW, outH); // 解像度メタも更新 p.resolution = [outW, outH]; if (!p.metadata) p.metadata = {}; p.metadata.resolution = [outW, outH]; // 参照を戻す poseData = p; window.poseEditorGlobals.poseData = p; return true; } catch (e) { return false; } } // Canvas解像度更新(座標も新サイズへスケール) function updateCanvasResolution(width, height) { if (!canvas) return false; const newW = Math.max(1, Math.floor(width)); const newH = Math.max(1, Math.floor(height)); // 旧データ解像度を取得 const current = window.poseEditorGlobals.poseData || poseData; const oldRes = (current && (current.resolution || (current.metadata && current.metadata.resolution))) || [512, 512]; const oldW = oldRes[0] || 512; const oldH = oldRes[1] || 512; // 表示Canvasは「出力比率」に揃える(解像度ではなく比率がポイント) const base = 640; // 表示上の基準長辺 let dispW, dispH; if (newW >= newH) { dispW = base; dispH = Math.max(1, Math.round(base * (newH / newW))); } else { dispH = base; dispW = Math.max(1, Math.round(base * (newW / newH))); } // Canvasサイズ更新(表示) canvas.width = dispW; canvas.height = dispH; // ディスプレイ座標系更新 coordinateTransformer.updateResolution(null, [dispW, dispH]); // 既存データを新サイズにスケール if (current) { updateKeypointsForNewSize(current, oldW, oldH, newW, newH); // 参照も同期 poseData = current; window.poseEditorGlobals.poseData = current; } // フォームに合わせて(万一)比率を再調整 ensureCanvasAspectFromOutputForm(base); // 再描画 if (poseData) { drawPose(poseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace); } else { clearCanvas(); } notifyCanvasOperation(`Canvas解像度(表示)を${dispW}x${dispH}に、データを${newW}x${newH}に変更しました`); // 🔄 サーバーへ最新データを送信してエクスポートと同期 try { if (typeof sendPoseDataToGradio === 'function') { // 次の描画フレーム後に送信してループを回避 setTimeout(() => { try { sendPoseDataToGradio(); } catch (e) {} }, 0); } } catch (e) {} return true; } // Canvas解像度更新(データはスケールせず、表示のみ変更) function setCanvasSizeNoScale(width, height) { if (!canvas) return false; const newW = Math.max(1, Math.floor(width)); const newH = Math.max(1, Math.floor(height)); canvas.width = newW; canvas.height = newH; coordinateTransformer.updateResolution(null, [newW, newH]); if (poseData) { drawPose(poseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace); } else { clearCanvas(); } notifyCanvasOperation(`Canvas表示サイズを${newW}x${newH}に変更しました(データはスケールしません)`); return true; } // Gradioからのデータ受信用(refs互換) window.gradioCanvasUpdate = function(pose_json_str) { // console.log('[DEBUG] gradioCanvasUpdate called, isUpdating:', window.poseEditorGlobals.isUpdating); // Issue 043: 処理中フラグチェック if (window.poseEditorGlobals.isUpdating) { console.log('⚠️ Canvas更新処理中のため、新しい要求をスキップ'); return pose_json_str; } // 処理開始フラグ window.poseEditorGlobals.isUpdating = true; // console.log('[DEBUG] isUpdating set to true'); try { if (typeof pose_json_str === 'string') { poseData = JSON.parse(pose_json_str); } else { poseData = pose_json_str; } // 💖 グローバルposeDataを更新(但し、people形式チェック付き) // 🎨 背景画像が含まれている場合は設定 if (poseData && poseData.background_image) { if (window.setBackgroundImage) { window.setBackgroundImage(poseData.background_image); } } // 💥 テンプレート読み込み時は完全置換、それ以外は既存データ保護 const isTemplateLoad = poseData.is_template_load === true; const existingPoseData = window.poseEditorGlobals.poseData; if (isTemplateLoad) { // テンプレート読み込み時は完全置換 window.poseEditorGlobals.poseData = poseData; // baseOriginalKeypointsもリセット(新しいテンプレート用) window.poseEditorGlobals.baseOriginalKeypoints = null; // 🔧 Issue #043: チェックボックス状態の取得をより確実なタイミングで実行 // DOM要素の安定化を待ってから表示設定を取得・適用 setTimeout(() => { updateDisplaySettingsFromCheckbox(); setupGradioCheckboxListeners(); }, 200); // 100ms → 200msに延長してDOM安定化を確実に } else if (existingPoseData && existingPoseData.people && existingPoseData.people[0] && poseData && (!poseData.people || !poseData.people[0])) { // Python側データを古い形式に変換してからマージ if (poseData.bodies || poseData.hands || poseData.faces) { // 既存のpeople形式を保持し、bodies部分だけ更新 const preservedPoseData = JSON.parse(JSON.stringify(existingPoseData)); // bodies.candidateがある場合は pose_keypoints_2d を更新 if (poseData.bodies && poseData.bodies.candidate) { const pose_keypoints_2d = []; for (const candidate of poseData.bodies.candidate) { if (candidate && candidate.length >= 2) { pose_keypoints_2d.push(candidate[0], candidate[1], candidate[2] || 1.0); } } preservedPoseData.people[0].pose_keypoints_2d = pose_keypoints_2d; } // 手と顔データは既存を保持(Python側で消失している可能性があるため) window.poseEditorGlobals.poseData = preservedPoseData; poseData = preservedPoseData; // 描画用も更新 } else { // people形式で来た場合はそのまま使用 window.poseEditorGlobals.poseData = poseData; } } else { // 通常通り更新 window.poseEditorGlobals.poseData = poseData; } // 💖 表示Canvasを出力比率に自動追従 try { const c = window.poseEditorGlobals.canvas || document.getElementById('pose_canvas'); if (c) { const res = (poseData && (poseData.resolution || (poseData.metadata && poseData.metadata.resolution))) || [512,512]; const outW = Math.max(1, Math.floor(res[0] || 512)); const outH = Math.max(1, Math.floor(res[1] || 512)); const base = 640; // 長辺基準 let dispW, dispH; if (outW >= outH) { dispW = base; dispH = Math.max(1, Math.round(base * (outH/outW))); } else { dispH = base; dispW = Math.max(1, Math.round(base * (outW/outH))); } if (c.width !== dispW || c.height !== dispH) { c.width = dispW; c.height = dispH; window.poseEditorGlobals.canvas = c; window.poseEditorGlobals.ctx = c.getContext('2d'); } // 変換器の表示解像度も更新 if (typeof coordinateTransformer?.updateResolution === 'function') { coordinateTransformer.updateResolution(null, [c.width, c.height]); } } } catch(e) {} // 💖 originalKeypointsも設定(但し、baseOriginalKeypointsは保護) if (poseData && poseData.people && poseData.people[0]) { // baseOriginalKeypointsは上書きしない(編集セッション保持のため) if (!window.poseEditorGlobals.baseOriginalKeypoints) { window.poseEditorGlobals.baseOriginalKeypoints = JSON.parse(JSON.stringify(poseData)); } // originalKeypointsは更新してOK(作業用) window.poseEditorGlobals.originalKeypoints = JSON.parse(JSON.stringify(poseData)); } if (!isCanvasReady()) { // console.log(`[DEBUG] isCanvasReady check: canvas=${!!canvas}, ctx=${!!ctx}, isInitialized=${isInitialized}`); // console.log(`[DEBUG] window.poseEditorGlobals.canvas=${!!window.poseEditorGlobals.canvas}, window.poseEditorGlobals.ctx=${!!window.poseEditorGlobals.ctx}`); initializePoseEditor(); // 再帰呼び出しではなく、フラグをリセットして終了 window.poseEditorGlobals.isUpdating = false; // console.log('[DEBUG] Canvas not ready, isUpdating reset to false'); return pose_json_str; } // 確実なキャンバスクリア const canvas = window.poseEditorGlobals.canvas; const ctx = window.poseEditorGlobals.ctx; ctx.clearRect(0, 0, canvas.width, canvas.height); // 🔧 Issue #043: 描画前に現在のチェックボックス状態を確実に取得 // テンプレート読み込み以外の場合でも表示設定を正確に反映 let currentHandsEnabled = window.poseEditorGlobals.enableHands; let currentFaceEnabled = window.poseEditorGlobals.enableFace; // チェックボックスから直接状態を取得(フォールバック用) const allCheckboxes = document.querySelectorAll('input[type="checkbox"]'); allCheckboxes.forEach((checkbox) => { const parentText = checkbox.parentElement?.textContent?.trim() || ''; if (parentText.includes('手を描画')) { currentHandsEnabled = checkbox.checked; } else if (parentText.includes('顔を描画')) { currentFaceEnabled = checkbox.checked; } }); // グローバル設定も更新 window.poseEditorGlobals.enableHands = currentHandsEnabled; window.poseEditorGlobals.enableFace = currentFaceEnabled; // グローバル設定で描画(手・顔表示設定を反映) if (poseData && Object.keys(poseData).length > 0) { // 表示Canvasはフォーム比率に追従 ensureCanvasAspectFromOutputForm(640); // 受信直後にデータ解像度をフォーム値に合わせてスケール(初回ズレ防止) alignPoseDataToOutputForm(); drawPose( poseData, currentHandsEnabled, currentFaceEnabled ); } else { } } catch (error) { console.error('Canvas update error:', error); } finally { // 確実なフラグ解除 window.poseEditorGlobals.isUpdating = false; // console.log('[DEBUG] isUpdating reset to false in finally'); } return pose_json_str; }; // Gradioからのデータ受信用(後方互換性) window.updatePoseData = function(data, enableHands = true, enableFace = true) { if (!isCanvasReady()) { return; } poseData = data; // 🔧 グローバルposeDataも更新(ドラッグ機能のため) window.poseEditorGlobals.poseData = poseData; drawPose(poseData, enableHands, enableFace); }; // Gradioへのデータ送信用 window.getPoseData = function() { return poseData; }; // 🔧 Gradio設定更新用(レガシー機能・現在はJavaScript直接監視により不使用) window.updateDisplaySettings = function(enableHands, enableFace, editMode) { // グローバル設定を更新 window.poseEditorGlobals.enableHands = enableHands; window.poseEditorGlobals.enableFace = enableFace; window.poseEditorGlobals.editMode = editMode; // 🎯 詳細モードに切り替えた時は矩形編集モードを確実に終了 if (editMode === "詳細モード") { window.poseEditorGlobals.rectEditMode = null; window.poseEditorGlobals.rectEditModeActive = false; window.poseEditorGlobals.draggedRectControl = null; window.poseEditorGlobals.draggedRect = null; window.poseEditorGlobals.currentRects = { leftHand: null, rightHand: null, face: null }; // 🔧 編集関連の状態も完全にクリア(連続編集対応) window.poseEditorGlobals.baseOriginalKeypoints = null; window.poseEditorGlobals.originalKeypoints = null; window.poseEditorGlobals.originalRect = null; window.poseEditorGlobals.rectEditInfo = null; window.poseEditorGlobals.dragStartPos = { x: 0, y: 0 }; } // 🔧 設定更新時は強制的に再描画(レガシー機能) const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (currentPoseData && Object.keys(currentPoseData).length > 0) { drawPose(currentPoseData, enableHands, enableFace); } }; // Gradioトースト通知のトリガー window.showToast = function(type, message) { // Gradioの隠しコンポーネントを使って通知 if (window.triggerToast) { window.triggerToast(type, message); } else { // フォールバック: コンソールログ console.log(`[${type.toUpperCase()}] ${message}`); } }; // 🎨 背景画像設定機能(refs互換) window.setBackgroundImage = function(imageData) { if (!imageData) { // 背景画像をクリア window.poseEditorGlobals.backgroundImage = null; // 現在のポーズデータがあれば再描画 const currentPoseData = window.poseEditorGlobals.poseData; if (currentPoseData && Object.keys(currentPoseData).length > 0) { drawPose(currentPoseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace); } return; } const img = new Image(); img.onload = function() { window.poseEditorGlobals.backgroundImage = img; // 背景画像が設定されたらCanvas再描画 const currentPoseData = window.poseEditorGlobals.poseData; if (currentPoseData && Object.keys(currentPoseData).length > 0) { drawPose(currentPoseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace); } }; img.onerror = function(e) { }; // Base64データまたはURLから画像を設定 if (typeof imageData === 'string') { img.src = imageData; } else if (imageData.url) { img.src = imageData.url; } else if (imageData.path) { img.src = imageData.path; } }; // Canvas操作時の通知 function notifyCanvasOperation(message) { showToast('info', message); } // Canvas状態変更の通知 function notifyCanvasStateChange(state) { switch(state) { case 'initialized': notifyCanvasOperation('キャンバスが初期化されました'); break; case 'cleared': notifyCanvasOperation('キャンバスをクリアしました'); break; case 'error': showToast('error', 'キャンバスでエラーが発生しました'); break; default: notifyCanvasOperation(`キャンバス状態: ${state}`); } } // グローバルエラーハンドラー window.addEventListener('error', (event) => { if (isCanvasReady()) { showCanvasError('エラーが発生しました'); } }); // Promise rejection ハンドラ window.addEventListener('unhandledrejection', (event) => { event.preventDefault(); if (isCanvasReady()) { showCanvasError('非同期処理でエラーが発生しました'); } }); // Canvas操作の安全な実行 function safeExecute(operation, errorMessage = "操作中にエラーが発生しました") { try { return operation(); } catch (error) { if (isCanvasReady()) { showCanvasError(errorMessage); } return null; } } // Canvas操作のtry-catch(後方互換性のため残す) function safeCanvasOperation(operation) { return safeExecute(operation, "Canvas操作中にエラーが発生しました") !== null; } // 🔧 簡易モード:手の矩形描画(refs互換) function drawHandRectangles(handsData, originalRes, scaleX_unused, scaleY_unused) { if (!handsData || handsData.length === 0) return; const ctx = window.poseEditorGlobals.ctx; // 矩形の色定義(refs互換) const HAND_RECT_COLORS = ['rgb(255,165,0)', 'rgb(255,69,0)']; // オレンジ系 const HAND_TYPES = ['leftHand', 'rightHand']; handsData.forEach((hand, handIndex) => { if (hand && hand.length > 0) { const rect = calculateHandRect(hand, originalRes); if (rect) { const handType = HAND_TYPES[handIndex] || `hand_${handIndex}`; // 🔧 グローバルに矩形情報保存(編集モード中は既存の矩形を保持) if (!window.poseEditorGlobals.currentRects[handType] || !window.poseEditorGlobals.rectEditModeActive) { window.poseEditorGlobals.currentRects[handType] = rect; } else { } // 描画は現在の矩形を使用(編集中は保存されている矩形) const drawRect = window.poseEditorGlobals.currentRects[handType] || rect; drawEditableRect(ctx, drawRect, HAND_RECT_COLORS[handIndex % 2], handType); } } }); } // 🔧 簡易モード:顔の矩形描画(refs互換) function drawFaceRectangles(facesData, originalRes, scaleX_unused, scaleY_unused) { if (!facesData || facesData.length === 0) return; const ctx = window.poseEditorGlobals.ctx; // 矩形の色定義(refs互換) const FACE_RECT_COLOR = 'rgb(34,139,34)'; // 緑 const face = facesData[0]; // 最初の顔のみ(編集済みデータ) if (face && face.length > 0) { const rect = calculateFaceRect(face, originalRes); if (rect) { // 🔧 グローバルに矩形情報保存(編集モード中は既存の矩形を保持) if (!window.poseEditorGlobals.currentRects.face || !window.poseEditorGlobals.rectEditModeActive) { window.poseEditorGlobals.currentRects.face = rect; } else { } // 描画は現在の矩形を使用(編集中は保存されている矩形) const drawRect = window.poseEditorGlobals.currentRects.face || rect; drawEditableRect(ctx, drawRect, FACE_RECT_COLOR, 'face'); } } } // 🔧 キーポイントから矩形を計算(refs互換) function calculateHandRect(handData, originalRes, scaleX, scaleY) { let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; let validPointCount = 0; // 3要素ごとに処理(x, y, confidence) for (let i = 0; i < handData.length; i += 3) { const x = handData[i]; const y = handData[i + 1]; const confidence = handData[i + 2]; if (confidence > 0.3) { // refs互換の閾値 // 🔧 レターボックス対応の座標変換 const pt = dataToCanvasXY(x, y, originalRes); const finalX = pt.x; const finalY = pt.y; minX = Math.min(minX, finalX); minY = Math.min(minY, finalY); maxX = Math.max(maxX, finalX); maxY = Math.max(maxY, finalY); validPointCount++; } } if (validPointCount < 3) return null; // 有効ポイントが少なすぎる // 10px余白付きで矩形返却(refs互換) const margin = 10; const rect = { x: minX - margin, y: minY - margin, width: (maxX - minX) + (margin * 2), height: (maxY - minY) + (margin * 2) }; // 🔍 デバッグログ: 手の矩形計算結果 const canvas = window.poseEditorGlobals.canvas; // 🔍 全キーポイントの詳細ログを追加 const allPoints = []; for (let i = 0; i < handData.length; i += 3) { if (i + 2 < handData.length && handData[i + 2] > 0.3) { allPoints.push({ index: i / 3, x: handData[i], y: handData[i + 1], confidence: handData[i + 2] }); } } if (window.poseEditorDebug.rect) console.log(`🔍 [calculateHandRect] 矩形計算結果:`, { rawBounds: { minX, minY, maxX, maxY }, finalRect: rect, validPointCount, firstPoint: handData.length >= 3 ? { x: handData[0], y: handData[1], conf: handData[2] } : null, scaleFactors: { scaleX, scaleY }, coordinateDetection: handData[0] > 10 ? 'ピクセル座標' : '正規化座標', canvasInfo: canvas ? { width: canvas.width, height: canvas.height, clientWidth: canvas.clientWidth, clientHeight: canvas.clientHeight, scale: canvas.width / canvas.clientWidth } : 'Canvas未取得' }); // 🔍 キーポイント座標を詳細表示 if (window.poseEditorDebug.rect) console.log(`🔍 [calculateHandRect] 全有効キーポイント座標:`, allPoints); // 🔍 座標の範囲をさらに詳細分析 const xCoords = allPoints.map(p => p.x); const yCoords = allPoints.map(p => p.y); if (window.poseEditorDebug.rect) console.log(`🔍 [calculateHandRect] 座標範囲詳細:`, { xCoords: xCoords, yCoords: yCoords, xRange: `${Math.min(...xCoords).toFixed(2)} 〜 ${Math.max(...xCoords).toFixed(2)}`, yRange: `${Math.min(...yCoords).toFixed(2)} 〜 ${Math.max(...yCoords).toFixed(2)}`, xSpread: (Math.max(...xCoords) - Math.min(...xCoords)).toFixed(2), ySpread: (Math.max(...yCoords) - Math.min(...yCoords)).toFixed(2) }); return rect; } // 🔧 顔キーポイントから矩形を計算(refs互換) function calculateFaceRect(faceData, originalRes, scaleX, scaleY) { let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; let validPointCount = 0; // 3要素ごとに処理(x, y, confidence) for (let i = 0; i < faceData.length; i += 3) { const x = faceData[i]; const y = faceData[i + 1]; const confidence = faceData[i + 2]; if (confidence > 0.3) { // refs互換の閾値 const pt = dataToCanvasXY(x, y, originalRes); const scaledX = pt.x; const scaledY = pt.y; minX = Math.min(minX, scaledX); minY = Math.min(minY, scaledY); maxX = Math.max(maxX, scaledX); maxY = Math.max(maxY, scaledY); validPointCount++; } } if (validPointCount < 10) return null; // 顔は多めのポイントが必要 // 15px余白付きで矩形返却(顔は少し大きめ) const margin = 15; return { x: minX - margin, y: minY - margin, width: (maxX - minX) + (margin * 2), height: (maxY - minY) + (margin * 2) }; } // 🔧 編集可能矩形の描画(refs互換) function drawEditableRect(ctx, rect, color, id) { if (!rect) return; // 🔍 デバッグログ: 実際の描画座標 if (window.poseEditorDebug.rect) console.log(`🔍 [drawEditableRect] ${id} 描画座標:`, { rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height }, color, canvasTransform: ctx.getTransform() }); // 太め破線で矩形描画(refs互換) ctx.strokeStyle = color; ctx.lineWidth = 3; ctx.setLineDash([8, 8]); // 破線パターン ctx.strokeRect(rect.x, rect.y, rect.width, rect.height); // 🔧 編集モード時のみコントロールポイントを表示(refs互換) if (window.poseEditorGlobals.rectEditModeActive && window.poseEditorGlobals.rectEditMode === id) { drawRectControlPoints(ctx, rect, color); } else { // 通常時は角のポイントのみ(小さめ) const controlSize = 6; ctx.fillStyle = color; ctx.setLineDash([]); // 実線に戻す // 4角のコントロールポイント const corners = [ { x: rect.x, y: rect.y }, // 左上 { x: rect.x + rect.width, y: rect.y }, // 右上 { x: rect.x, y: rect.y + rect.height }, // 左下 { x: rect.x + rect.width, y: rect.y + rect.height } // 右下 ]; corners.forEach(corner => { ctx.fillRect(corner.x - controlSize/2, corner.y - controlSize/2, controlSize, controlSize); }); } } // 🔧 既存の矩形を描画(編集モード中用) function drawExistingRectangles(rectTypes) { const ctx = window.poseEditorGlobals.ctx; const colorMap = { 'leftHand': 'rgb(255,165,0)', // オレンジ 'rightHand': 'rgb(255,69,0)', // 濃いオレンジ 'face': 'rgb(34,139,34)' // 緑 }; rectTypes.forEach(rectType => { const rect = window.poseEditorGlobals.currentRects[rectType]; if (rect) { const color = colorMap[rectType] || 'rgb(255,165,0)'; drawEditableRect(ctx, rect, color, rectType); } }); } // 🔧 矩形再計算なしの描画(編集中専用) function redrawPoseWithoutRecalculation() { if (!isCanvasReady()) return; const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (!currentPoseData) return; const canvas = window.poseEditorGlobals.canvas; const ctx = window.poseEditorGlobals.ctx; // キャンバスクリア ctx.clearRect(0, 0, canvas.width, canvas.height); // 📐 解像度情報の取得 const originalRes = currentPoseData.resolution || [512, 512]; const fit = getFitParams(originalRes); // ボディの描画(ハイライトなし) drawBody(currentPoseData, -1); // 手の描画(設定制御・座標変換パラメータ付き) // 💖 people形式で手を描画 if (window.poseEditorGlobals.enableHands && currentPoseData.people && currentPoseData.people[0]) { const person = currentPoseData.people[0]; const handsData = [ person.hand_left_keypoints_2d || [], person.hand_right_keypoints_2d || [] ]; drawHands(handsData, originalRes); } // 顔の描画(設定制御・座標変換パラメータ付き) if (window.poseEditorGlobals.enableFace && currentPoseData.faces) { drawFaces(currentPoseData.faces, originalRes); } // 🔧 既存の矩形のみ描画(再計算なし) if (window.poseEditorGlobals.editMode === "簡易モード") { const allRectTypes = ['leftHand', 'rightHand', 'face']; drawExistingRectangles(allRectTypes); } } // 🔧 矩形コントロールポイント描画(編集モード時) function drawRectControlPoints(ctx, rect, color) { const controlSize = 10; ctx.fillStyle = color; ctx.strokeStyle = 'white'; ctx.lineWidth = 2; ctx.setLineDash([]); // 実線 // 8つのコントロールポイント(4角 + 4辺の中央) const controlPoints = [ { x: rect.x, y: rect.y, type: 'corner', position: 'tl' }, // 左上 { x: rect.x + rect.width, y: rect.y, type: 'corner', position: 'tr' }, // 右上 { x: rect.x, y: rect.y + rect.height, type: 'corner', position: 'bl' }, // 左下 { x: rect.x + rect.width, y: rect.y + rect.height, type: 'corner', position: 'br' }, // 右下 { x: rect.x + rect.width / 2, y: rect.y, type: 'edge', position: 't' }, // 上辺 { x: rect.x + rect.width, y: rect.y + rect.height / 2, type: 'edge', position: 'r' }, // 右辺 { x: rect.x + rect.width / 2, y: rect.y + rect.height, type: 'edge', position: 'b' }, // 下辺 { x: rect.x, y: rect.y + rect.height / 2, type: 'edge', position: 'l' } // 左辺 ]; controlPoints.forEach(point => { // 白い枠付きの四角形 ctx.fillRect(point.x - controlSize/2, point.y - controlSize/2, controlSize, controlSize); ctx.strokeRect(point.x - controlSize/2, point.y - controlSize/2, controlSize, controlSize); }); } // 🔧 点が矩形内にあるかチェック(refs互換) function isPointInRect(x, y, rect) { if (!rect) return false; return x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height; } // 🔧 どの矩形にクリックが含まれてるか探す(refs互換) function findRectContaining(x, y) { const rects = window.poseEditorGlobals.currentRects; if (rects.leftHand && isPointInRect(x, y, rects.leftHand)) { return 'leftHand'; } if (rects.rightHand && isPointInRect(x, y, rects.rightHand)) { return 'rightHand'; } if (rects.face && isPointInRect(x, y, rects.face)) { return 'face'; } return null; } // 🔧 矩形コントロールポイント検出(refs互換) function findNearestRectControlPoint(x, y) { const rectType = window.poseEditorGlobals.rectEditMode; if (!rectType) { return null; } const rect = window.poseEditorGlobals.currentRects[rectType]; if (!rect) { return null; } const threshold = 15; // クリック判定の閾値 // 8つのコントロールポイント const controlPoints = [ { x: rect.x, y: rect.y, type: 'corner', position: 'tl' }, // 左上 { x: rect.x + rect.width, y: rect.y, type: 'corner', position: 'tr' }, // 右上 { x: rect.x, y: rect.y + rect.height, type: 'corner', position: 'bl' }, // 左下 { x: rect.x + rect.width, y: rect.y + rect.height, type: 'corner', position: 'br' }, // 右下 { x: rect.x + rect.width / 2, y: rect.y, type: 'edge', position: 't' }, // 上辺 { x: rect.x + rect.width, y: rect.y + rect.height / 2, type: 'edge', position: 'r' }, // 右辺 { x: rect.x + rect.width / 2, y: rect.y + rect.height, type: 'edge', position: 'b' }, // 下辺 { x: rect.x, y: rect.y + rect.height / 2, type: 'edge', position: 'l' } // 左辺 ]; // 最も近いコントロールポイントを探す let nearestPoint = null; let minDistance = Infinity; for (const point of controlPoints) { const distance = Math.sqrt((x - point.x) ** 2 + (y - point.y) ** 2); if (distance < threshold && distance < minDistance) { minDistance = distance; nearestPoint = point; } } if (nearestPoint) { } else { } return nearestPoint; } // 🔧 矩形コントロールポイントドラッグ処理(refs互換・絶対座標計算) function updateRectControlDrag(mouseX, mouseY) { const controlPoint = window.poseEditorGlobals.draggedRectControl; const rectType = window.poseEditorGlobals.rectEditMode; // 🔍 関数呼び出しログ if (window.poseEditorDebug.rect) console.log(`🔍 [updateRectControlDrag] Called:`, { rectType: rectType, controlPoint: controlPoint, mousePos: { x: mouseX, y: mouseY }, hasOriginalRect: !!window.poseEditorGlobals.originalRect }); if (!controlPoint) { if (window.poseEditorDebug.rect) console.log(`⚠️ [updateRectControlDrag] No controlPoint, exiting`); return; } if (!rectType) { if (window.poseEditorDebug.rect) console.log(`⚠️ [updateRectControlDrag] No rectType, exiting`); return; } // 🔧 元の矩形座標を取得(初回保存済み) const originalRect = window.poseEditorGlobals.originalRect; if (!originalRect) { return; } // 🔧 絶対座標で新しい矩形を計算(refs互換) let newRect = { ...originalRect }; // コントロールポイントの位置に応じて絶対座標で計算 switch (controlPoint.position) { case 'tl': // 左上 newRect.width = originalRect.width + (originalRect.x - mouseX); newRect.height = originalRect.height + (originalRect.y - mouseY); newRect.x = mouseX; newRect.y = mouseY; break; case 'tr': // 右上 newRect.width = mouseX - originalRect.x; newRect.height = originalRect.height + (originalRect.y - mouseY); newRect.y = mouseY; break; case 'bl': // 左下 newRect.width = originalRect.width + (originalRect.x - mouseX); newRect.height = mouseY - originalRect.y; newRect.x = mouseX; break; case 'br': // 右下 newRect.width = mouseX - originalRect.x; newRect.height = mouseY - originalRect.y; break; case 't': // 上辺 newRect.y = mouseY; newRect.height = originalRect.height + (originalRect.y - mouseY); break; case 'r': // 右辺 newRect.width = mouseX - originalRect.x; break; case 'b': // 下辺 newRect.height = mouseY - originalRect.y; break; case 'l': // 左辺 newRect.x = mouseX; newRect.width = originalRect.width + (originalRect.x - mouseX); break; } // 🔧 最小サイズ制限(refs互換) const minSize = 20; if (newRect.width < minSize) { if (controlPoint.position.includes('l')) { newRect.x = newRect.x + newRect.width - minSize; } newRect.width = minSize; } if (newRect.height < minSize) { if (controlPoint.position.includes('t')) { newRect.y = newRect.y + newRect.height - minSize; } newRect.height = minSize; } // 🔧 矩形を更新(累積変形防止のため直接設定) window.poseEditorGlobals.currentRects[rectType] = newRect; // 🔍 矩形変換ログ if (window.poseEditorDebug.rect) console.log(`🔍 [updateRectControlDrag] 矩形変換:`, { rectType: rectType, controlPosition: controlPoint.position, originalRect: originalRect, newRect: newRect, mousePos: { x: mouseX, y: mouseY }, rectDelta: { x: newRect.x - originalRect.x, y: newRect.y - originalRect.y, width: newRect.width - originalRect.width, height: newRect.height - originalRect.height } }); // 🔧 手・顔キーポイントの座標も更新(直接矩形変換版) if (controlPoint) { if (window.poseEditorDebug.rect) console.log(`🔍 [updateRectControlDrag] Calling transformKeypointsDirectly...`); transformKeypointsDirectly(rectType, originalRect, newRect); } // 🚀 refs互換:編集中もリアルタイム描画 const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (currentPoseData) { drawPose( currentPoseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace ); } } // 🔧 rectEditInfo全体初期化(矩形編集モード開始時) function initializeRectEditInfo() { // 🔧 前回の状態をクリアしてから初期化(連続編集対応) window.poseEditorGlobals.rectEditInfo = {}; window.poseEditorGlobals.baseOriginalKeypoints = null; window.poseEditorGlobals.originalKeypoints = null; window.poseEditorGlobals.originalRect = null; const rectTypes = ['face', 'leftHand', 'rightHand']; for (const rectType of rectTypes) { const currentRect = window.poseEditorGlobals.currentRects[rectType]; if (currentRect) { window.poseEditorGlobals.rectEditInfo[rectType] = { originalRect: { ...currentRect }, keypointIndices: getRectKeypointIndices(rectType) }; } } // 💖 元のキーポイントを保存(連続編集対応:ベースデータは初回のみ保存) const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (currentPoseData) { // 💥 ベースは初回のみ保存!編集済みデータで上書きしない if (!window.poseEditorGlobals.baseOriginalKeypoints) { window.poseEditorGlobals.baseOriginalKeypoints = JSON.parse(JSON.stringify(currentPoseData)); if (window.poseEditorDebug.rect) console.log('💖 [initializeRectEditInfo] Base original keypoints saved for FIRST editing session'); // ベースデータの詳細確認 if (window.poseEditorDebug.rect) console.log('🔍 [initializeRectEditInfo] Base data details:', { hasHandLeft: !!window.poseEditorGlobals.baseOriginalKeypoints.people?.[0]?.hand_left_keypoints_2d, hasHandRight: !!window.poseEditorGlobals.baseOriginalKeypoints.people?.[0]?.hand_right_keypoints_2d, hasFace: !!window.poseEditorGlobals.baseOriginalKeypoints.people?.[0]?.face_keypoints_2d, handLeftLength: window.poseEditorGlobals.baseOriginalKeypoints.people?.[0]?.hand_left_keypoints_2d?.length || 0, handRightLength: window.poseEditorGlobals.baseOriginalKeypoints.people?.[0]?.hand_right_keypoints_2d?.length || 0, faceLength: window.poseEditorGlobals.baseOriginalKeypoints.people?.[0]?.face_keypoints_2d?.length || 0 }); } else { if (window.poseEditorDebug.rect) console.log('💖 [initializeRectEditInfo] Base original keypoints already exists - keeping original data'); } // 作業用は常にベースからコピー(編集済みデータではなく!) window.poseEditorGlobals.originalKeypoints = JSON.parse(JSON.stringify(window.poseEditorGlobals.baseOriginalKeypoints)); if (window.poseEditorDebug.rect) console.log('💖 [initializeRectEditInfo] Working original keypoints restored from base for current editing session'); // 作業用データの詳細確認 if (window.poseEditorDebug.rect) console.log('🔍 [initializeRectEditInfo] Working data details:', { hasHandLeft: !!window.poseEditorGlobals.originalKeypoints.people?.[0]?.hand_left_keypoints_2d, hasHandRight: !!window.poseEditorGlobals.originalKeypoints.people?.[0]?.hand_right_keypoints_2d, hasFace: !!window.poseEditorGlobals.originalKeypoints.people?.[0]?.face_keypoints_2d, handLeftLength: window.poseEditorGlobals.originalKeypoints.people?.[0]?.hand_left_keypoints_2d?.length || 0, handRightLength: window.poseEditorGlobals.originalKeypoints.people?.[0]?.hand_right_keypoints_2d?.length || 0, faceLength: window.poseEditorGlobals.originalKeypoints.people?.[0]?.face_keypoints_2d?.length || 0 }); } } // 🔧 矩形タイプに対応するキーポイントインデックスを取得(refs互換:詳細キーポイントのみ) function getRectKeypointIndices(rectType) { // 🔧 refs互換:矩形編集では体のキーポイントは編集しない、詳細キーポイントのみ // 体のキーポイント(pose_keypoints_2d)は触らずに、詳細キーポイントのみを編集 return []; // 空配列を返して体のキーポイント編集を無効化 } // 🔧 refs互換のシンプル矩形キーポイント更新 // 🚀 refs互換:シンプル矩形編集(detail keypointsのみ) function updateKeypointsByRectSimple(rectType, newRect) { const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (!currentPoseData || !currentPoseData.people || !currentPoseData.people[0]) { return; } const person = currentPoseData.people[0]; const originalRect = window.poseEditorGlobals.originalRect; if (!originalRect) { return; } // 🚀 refs互換:detail keypointsを直接リサイズ updateDetailKeypointsByRect(rectType, originalRect, newRect); } // 🚀 refs互換:シンプル矩形移動(detail keypointsのみ) function moveKeypointsByRectSimple(rectType, deltaX, deltaY) { const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (!currentPoseData || !currentPoseData.people || !currentPoseData.people[0]) { return; } const person = currentPoseData.people[0]; // 🚀 refs互換:detail keypointsを直接移動 if (rectType === 'leftHand' && person.hand_left_keypoints_2d) { moveKeypointsArray(person.hand_left_keypoints_2d, deltaX, deltaY, 'left_hand'); } else if (rectType === 'rightHand' && person.hand_right_keypoints_2d) { moveKeypointsArray(person.hand_right_keypoints_2d, deltaX, deltaY, 'right_hand'); } else if (rectType === 'face' && person.face_keypoints_2d) { moveKeypointsArray(person.face_keypoints_2d, deltaX, deltaY, 'face'); } } // 🚀 refs互換:detail keypointsをリサイズ(originalKeypointsから復元) function updateDetailKeypointsByRect(rectType, originalRect, newRect) { const currentPoseData = window.poseEditorGlobals.poseData || poseData; const originalKeypoints = window.poseEditorGlobals.originalKeypoints; if (!currentPoseData || !originalKeypoints || !currentPoseData.people || !originalKeypoints.people || !currentPoseData.people[0] || !originalKeypoints.people[0]) { return; } const person = currentPoseData.people[0]; const originalPerson = originalKeypoints.people[0]; if (rectType === 'leftHand' && person.hand_left_keypoints_2d && originalPerson.hand_left_keypoints_2d) { updateKeypointsArrayByRect(person.hand_left_keypoints_2d, originalPerson.hand_left_keypoints_2d, originalRect, newRect, 'left_hand'); } else if (rectType === 'rightHand' && person.hand_right_keypoints_2d && originalPerson.hand_right_keypoints_2d) { updateKeypointsArrayByRect(person.hand_right_keypoints_2d, originalPerson.hand_right_keypoints_2d, originalRect, newRect, 'right_hand'); } else if (rectType === 'face' && person.face_keypoints_2d && originalPerson.face_keypoints_2d) { updateKeypointsArrayByRect(person.face_keypoints_2d, originalPerson.face_keypoints_2d, originalRect, newRect, 'face'); } } // 🔧 キーポイント配列をリサイズ(3要素ずつ) function updateKeypointsArrayByRect(keypointsArray, originalKeypointsArray, originalRect, newRect, label) { let updatedCount = 0; for (let i = 0; i < keypointsArray.length; i += 3) { if (i + 2 < keypointsArray.length && i + 2 < originalKeypointsArray.length) { const confidence = originalKeypointsArray[i + 2]; if (confidence > 0.1) { // 有効なキーポイントのみ const origX = originalKeypointsArray[i]; const origY = originalKeypointsArray[i + 1]; if (origX > 0 && origY > 0) { // 🚀 座標変換:Canvas↔データ座標(レターボックス対応) const res = (window.poseEditorGlobals.poseData && window.poseEditorGlobals.poseData.resolution) || [512,512]; const fit = getFitParams(res); const origRectDataX = (originalRect.x - fit.offsetX) / fit.scale; const origRectDataY = (originalRect.y - fit.offsetY) / fit.scale; const newRectDataX = (newRect.x - fit.offsetX) / fit.scale; const newRectDataY = (newRect.y - fit.offsetY) / fit.scale; const origRectWidthData = originalRect.width / fit.scale; const origRectHeightData = originalRect.height / fit.scale; const newRectWidthData = newRect.width / fit.scale; const newRectHeightData = newRect.height / fit.scale; // 元矩形内の相対位置を計算 const relativeX = (origX - origRectDataX) / origRectWidthData; const relativeY = (origY - origRectDataY) / origRectHeightData; // 新矩形での絶対位置を計算(データ座標) const newX = newRectDataX + (relativeX * newRectWidthData); const newY = newRectDataY + (relativeY * newRectHeightData); // キーポイント更新(512x512にクランプ) keypointsArray[i] = Math.max(0, Math.min(512, newX)); keypointsArray[i + 1] = Math.max(0, Math.min(512, newY)); updatedCount++; } } } } } // 🔧 手と顔の詳細キーポイントを移動(refs互換) function moveDetailKeypoints(rectType, deltaX, deltaY) { const person = window.poseEditorGlobals.poseData.people[0]; if (rectType === 'leftHand' && person.hand_left_keypoints_2d) { moveKeypointsArray(person.hand_left_keypoints_2d, deltaX, deltaY, 'left_hand'); } else if (rectType === 'rightHand' && person.hand_right_keypoints_2d) { moveKeypointsArray(person.hand_right_keypoints_2d, deltaX, deltaY, 'right_hand'); } else if (rectType === 'face' && person.face_keypoints_2d) { moveKeypointsArray(person.face_keypoints_2d, deltaX, deltaY, 'face'); } else { // No matching detail keypoints found } } // 🔧 キーポイント配列を移動(3要素ずつ) function moveKeypointsArray(keypointsArray, deltaX, deltaY, label) { let movedCount = 0; for (let i = 0; i < keypointsArray.length; i += 3) { if (i + 2 < keypointsArray.length) { const confidence = keypointsArray[i + 2]; if (confidence > 0.1) { // 有効なキーポイントのみ const currentX = keypointsArray[i]; const currentY = keypointsArray[i + 1]; // 🚀 座標変換:表示座標の移動量→データ座標の移動量(レタボ対応) const res = (window.poseEditorGlobals.poseData && window.poseEditorGlobals.poseData.resolution) || [512,512]; const fit = getFitParams(res); const dataDeltaX = deltaX / fit.scale; const dataDeltaY = deltaY / fit.scale; // 移動(512x512にクランプ) const dataW = res[0] || 512; const dataH = res[1] || 512; const newX = Math.max(0, Math.min(dataW, currentX + dataDeltaX)); const newY = Math.max(0, Math.min(dataH, currentY + dataDeltaY)); keypointsArray[i] = newX; keypointsArray[i + 1] = newY; movedCount++; } } } } // 🔧 矩形移動ドラッグ処理(refs互換修正版) function updateRectMoveDrag(mouseX, mouseY) { const rectType = window.poseEditorGlobals.draggedRect; if (!rectType) return; const rect = window.poseEditorGlobals.currentRects[rectType]; if (!rect) return; const startPos = window.poseEditorGlobals.dragStartPos; const deltaX = mouseX - startPos.x; const deltaY = mouseY - startPos.y; // 矩形の位置を更新(refs方式) const newRect = { ...rect, x: rect.x + deltaX, y: rect.y + deltaY }; // Canvas境界制限 const canvas = window.poseEditorGlobals.canvas; newRect.x = Math.max(0, Math.min(canvas.width - newRect.width, newRect.x)); newRect.y = Math.max(0, Math.min(canvas.height - newRect.height, newRect.y)); // 矩形を更新 window.poseEditorGlobals.currentRects[rectType] = newRect; // キーポイントを矩形の移動に合わせて移動(refs方式) moveKeypointsWithRect(rectType, deltaX, deltaY); // 🚀 refs互換:編集中もリアルタイム描画 const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (currentPoseData) { drawPose( currentPoseData, window.poseEditorGlobals.enableHands, window.poseEditorGlobals.enableFace ); } // ドラッグ開始位置を更新(連続移動対応) window.poseEditorGlobals.dragStartPos = { x: mouseX, y: mouseY }; } // 🔧 矩形タイプに応じた色取得 function getColorForRectType(rectType) { const colors = { 'leftHand': 'rgb(255,165,0)', // オレンジ 'rightHand': 'rgb(255,69,0)', // 濃いオレンジ 'face': 'rgb(34,139,34)' // 緑 }; return colors[rectType] || 'rgb(255,165,0)'; } // 🔧 矩形リサイズに合わせてキーポイント更新(refs互換) function updateKeypointsByRect(rectType, newRect) { const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (!currentPoseData) return; // 元矩形情報を取得 const originalRect = window.poseEditorGlobals.originalRect; if (!originalRect) return; // 最小サイズ制限(refs互換) const minSize = 20; if (newRect.width < minSize || newRect.height < minSize) { return; } // 変換比率を計算 const scaleX = newRect.width / originalRect.width; const scaleY = newRect.height / originalRect.height; // 対象キーポイントデータを取得(refs互換の構造もサポート) let targetKeypoints = null; let fieldName = null; // まず新しい構造(people[0].xxx_keypoints_2d)をチェック if (currentPoseData.people && currentPoseData.people[0]) { const person = currentPoseData.people[0]; if (rectType === 'leftHand' && person.hand_left_keypoints_2d) { targetKeypoints = person.hand_left_keypoints_2d; fieldName = 'hand_left_keypoints_2d'; } else if (rectType === 'rightHand' && person.hand_right_keypoints_2d) { targetKeypoints = person.hand_right_keypoints_2d; fieldName = 'hand_right_keypoints_2d'; } else if (rectType === 'face' && person.face_keypoints_2d) { targetKeypoints = person.face_keypoints_2d; fieldName = 'face_keypoints_2d'; } } // 💖 people形式で再取得を試行(古い構造フォールバックを削除) if (!targetKeypoints && currentPoseData.people && currentPoseData.people[0]) { const person = currentPoseData.people[0]; if (rectType === 'leftHand') { targetKeypoints = person.hand_left_keypoints_2d; } else if (rectType === 'rightHand') { targetKeypoints = person.hand_right_keypoints_2d; } else if (rectType === 'face') { targetKeypoints = person.face_keypoints_2d; } } if (!targetKeypoints) { return; } // 📐 解像度情報の取得(refs互換) let dataResolutionWidth = 512; // デフォルト値 let dataResolutionHeight = 512; // metadata.resolutionをチェック(refs形式) if (currentPoseData.metadata && currentPoseData.metadata.resolution && Array.isArray(currentPoseData.metadata.resolution) && currentPoseData.metadata.resolution.length >= 2) { dataResolutionWidth = currentPoseData.metadata.resolution[0]; dataResolutionHeight = currentPoseData.metadata.resolution[1]; } else if (currentPoseData.resolution) { // 通常のresolutionフィールド dataResolutionWidth = currentPoseData.resolution[0]; dataResolutionHeight = currentPoseData.resolution[1]; } const fit = getFitParams([dataResolutionWidth, dataResolutionHeight]); // 正規化座標かピクセル座標かを判定(refs互換) let isNormalized = false; if (targetKeypoints.length > 0) { for (let i = 0; i < targetKeypoints.length; i += 3) { if (i + 2 < targetKeypoints.length && targetKeypoints[i + 2] > 0) { const x = targetKeypoints[i]; const y = targetKeypoints[i + 1]; isNormalized = (x >= 0 && x <= 1 && y >= 0 && y <= 1); break; } } } let updatedCount = 0; // 各キーポイントを変換(3要素ずつ:x, y, confidence) for (let i = 0; i < targetKeypoints.length; i += 3) { if (i + 2 < targetKeypoints.length) { const confidence = targetKeypoints[i + 2]; if (confidence > 0.1) { // refs互換の閾値 let x = targetKeypoints[i]; let y = targetKeypoints[i + 1]; // データ座標→Canvas座標 let canvasX, canvasY; if (isNormalized) { canvasX = fit.offsetX + (x * dataResolutionWidth) * fit.scale; canvasY = fit.offsetY + (y * dataResolutionHeight) * fit.scale; } else { canvasX = fit.offsetX + x * fit.scale; canvasY = fit.offsetY + y * fit.scale; } // 🔧 元矩形内での相対位置を計算(refs互換・安全範囲チェック) let relativeX = (canvasX - originalRect.x) / originalRect.width; let relativeY = (canvasY - originalRect.y) / originalRect.height; // 🔧 相対位置を0-1の範囲内にクランプ(refs互換) relativeX = Math.max(0, Math.min(1, relativeX)); relativeY = Math.max(0, Math.min(1, relativeY)); // 🔧 新矩形での新しい位置を計算 const newCanvasX = newRect.x + (relativeX * newRect.width); const newCanvasY = newRect.y + (relativeY * newRect.height); // 🔧 Canvas座標→データ座標に戻す(refs互換・範囲制限付き) if (isNormalized) { const dataX = (newCanvasX - fit.offsetX) / fit.scale; const dataY = (newCanvasY - fit.offsetY) / fit.scale; let newNormX = dataX / dataResolutionWidth; let newNormY = dataY / dataResolutionHeight; // 正規化座標の範囲制限(0-1) newNormX = Math.max(0, Math.min(1, newNormX)); newNormY = Math.max(0, Math.min(1, newNormY)); targetKeypoints[i] = newNormX; targetKeypoints[i + 1] = newNormY; } else { let newDataX = (newCanvasX - fit.offsetX) / fit.scale; let newDataY = (newCanvasY - fit.offsetY) / fit.scale; // ピクセル座標の範囲制限 newDataX = Math.max(0, Math.min(dataResolutionWidth, newDataX)); newDataY = Math.max(0, Math.min(dataResolutionHeight, newDataY)); targetKeypoints[i] = newDataX; targetKeypoints[i + 1] = newDataY; } updatedCount++; // 🔧 デバッグログ(最初の3つのキーポイントのみ) if (updatedCount <= 3) { } } } } // 矩形情報を更新(refs互換) if (window.poseEditorGlobals.rects) { window.poseEditorGlobals.rects[rectType] = newRect; } } // 🔧 矩形移動に合わせてキーポイント移動(refs互換) function moveKeypointsByRect(rectType, deltaX, deltaY) { const currentPoseData = window.poseEditorGlobals.poseData || poseData; if (!currentPoseData) return; // 対象キーポイントデータを取得(refs互換の構造もサポート) let targetKeypoints = null; let fieldName = null; // まず新しい構造(people[0].xxx_keypoints_2d)をチェック if (currentPoseData.people && currentPoseData.people[0]) { const person = currentPoseData.people[0]; if (rectType === 'leftHand' && person.hand_left_keypoints_2d) { targetKeypoints = person.hand_left_keypoints_2d; fieldName = 'hand_left_keypoints_2d'; } else if (rectType === 'rightHand' && person.hand_right_keypoints_2d) { targetKeypoints = person.hand_right_keypoints_2d; fieldName = 'hand_right_keypoints_2d'; } else if (rectType === 'face' && person.face_keypoints_2d) { targetKeypoints = person.face_keypoints_2d; fieldName = 'face_keypoints_2d'; } } // 💖 people形式で再取得を試行(古い構造フォールバックを削除) if (!targetKeypoints && currentPoseData.people && currentPoseData.people[0]) { const person = currentPoseData.people[0]; if (rectType === 'leftHand') { targetKeypoints = person.hand_left_keypoints_2d; } else if (rectType === 'rightHand') { targetKeypoints = person.hand_right_keypoints_2d; } else if (rectType === 'face') { targetKeypoints = person.face_keypoints_2d; } } if (!targetKeypoints) { return; } // 📐 解像度情報の取得(refs互換) let dataResolutionWidth = 512; // デフォルト値 let dataResolutionHeight = 512; // metadata.resolutionをチェック(refs形式) if (currentPoseData.metadata && currentPoseData.metadata.resolution && Array.isArray(currentPoseData.metadata.resolution) && currentPoseData.metadata.resolution.length >= 2) { dataResolutionWidth = currentPoseData.metadata.resolution[0]; dataResolutionHeight = currentPoseData.metadata.resolution[1]; } else if (currentPoseData.resolution) { // 通常のresolutionフィールド dataResolutionWidth = currentPoseData.resolution[0]; dataResolutionHeight = currentPoseData.resolution[1]; } const fit = getFitParams([dataResolutionWidth, dataResolutionHeight]); // 正規化座標かピクセル座標かを判定(refs互換) let isNormalized = false; if (targetKeypoints.length > 0) { for (let i = 0; i < targetKeypoints.length; i += 3) { if (i + 2 < targetKeypoints.length && targetKeypoints[i + 2] > 0) { const x = targetKeypoints[i]; const y = targetKeypoints[i + 1]; isNormalized = (x >= 0 && x <= 1 && y >= 0 && y <= 1); break; } } } // Canvas座標での移動量をデータ座標での移動量に変換 const dataDeltaX = deltaX / fit.scale; const dataDeltaY = deltaY / fit.scale; let movedCount = 0; // 各キーポイントを移動(3要素ずつ:x, y, confidence) for (let i = 0; i < targetKeypoints.length; i += 3) { if (i + 2 < targetKeypoints.length) { const confidence = targetKeypoints[i + 2]; if (confidence > 0.1) { // refs互換の閾値 if (isNormalized) { // 正規化座標の場合 targetKeypoints[i] += dataDeltaX / dataResolutionWidth; targetKeypoints[i + 1] += dataDeltaY / dataResolutionHeight; } else { // ピクセル座標の場合 targetKeypoints[i] += dataDeltaX; targetKeypoints[i + 1] += dataDeltaY; } movedCount++; } } } } // 🎨 推定接続の描画(少ないキーポイント用の補間機能) function drawEstimatedConnections(candidates, originalRes) { const ctx = window.poseEditorGlobals.ctx; const fit = getFitParams(originalRes); // 有効なキーポイントを取得 const validPoints = []; for (let i = 0; i < candidates.length; i++) { const point = candidates[i]; if (point && point[0] > 1 && point[1] > 1 && point[0] < originalRes[0] && point[1] < originalRes[1]) { validPoints.push({ index: i, x: fit.offsetX + point[0] * fit.scale, y: fit.offsetY + point[1] * fit.scale, originalX: point[0], originalY: point[1] }); } } // console.log(`[DEBUG] 🔗 Drawing estimated connections for ${validPoints.length} valid points`); if (validPoints.length < 2) return; // 点線スタイルで推定接続を描画 ctx.setLineDash([5, 5]); // 点線 ctx.strokeStyle = '#888888'; // グレー ctx.lineWidth = 2; ctx.globalAlpha = 0.6; // 半透明 // 近接する有効ポイント同士を接続 for (let i = 0; i < validPoints.length - 1; i++) { for (let j = i + 1; j < validPoints.length; j++) { const p1 = validPoints[i]; const p2 = validPoints[j]; // 距離が近い場合のみ接続(推定接続) const distance = Math.sqrt( Math.pow(p1.originalX - p2.originalX, 2) + Math.pow(p1.originalY - p2.originalY, 2) ); // 画像サイズに応じた適応的な距離閾値 const maxDistance = Math.max(originalRes[0], originalRes[1]) * 0.3; if (distance < maxDistance) { ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.stroke(); // console.log(`[DEBUG] 🔗 Estimated connection: ${p1.index}→${p2.index} (dist: ${distance.toFixed(1)})`); } } } // スタイルをリセット // 出力フォーム(幅/高さ)から比率を取得しCanvas表示サイズを合わせる function ensureCanvasAspectFromOutputForm(baseLongSide = 640) { try { const c = window.poseEditorGlobals.canvas || document.getElementById('pose_canvas'); if (!c) return; // 「幅」「高さ」ラベル付きのnumber inputを探索 const nums = Array.from(document.querySelectorAll('input[type="number"]')); let w = null, h = null; for (const el of nums) { const labelText = el.labels && el.labels[0] ? (el.labels[0].textContent || '').trim() : ''; if (labelText.includes('幅')) w = parseInt(el.value || '0'); if (labelText.includes('高さ')) h = parseInt(el.value || '0'); } if (!w || !h || w <= 0 || h <= 0) return; // 比率に合わせて表示Canvasサイズを再計算 let dispW, dispH; if (w >= h) { dispW = baseLongSide; dispH = Math.max(1, Math.round(baseLongSide * (h / w))); } else { dispH = baseLongSide; dispW = Math.max(1, Math.round(baseLongSide * (w / h))); } if (c.width !== dispW || c.height !== dispH) { c.width = dispW; c.height = dispH; window.poseEditorGlobals.canvas = c; window.poseEditorGlobals.ctx = c.getContext('2d'); if (typeof coordinateTransformer?.updateResolution === 'function') { coordinateTransformer.updateResolution(null, [dispW, dispH]); } } } catch (e) {} } ctx.setLineDash([]); // 実線に戻す ctx.globalAlpha = 1.0; // 不透明に戻す } // 🎨 pose_editor.js initialization complete // --- Global helper to enforce canvas aspect from output form --- (function(){ if (!window.ensureCanvasAspectFromOutputForm) { window.ensureCanvasAspectFromOutputForm = function(baseLongSide = 640) { try { const c = window.poseEditorGlobals?.canvas || document.getElementById('pose_canvas'); if (!c) return; // 1) try form values const nums = Array.from(document.querySelectorAll('input[type="number"]')); let w = null, h = null; for (const el of nums) { const label = el.labels && el.labels[0] ? (el.labels[0].textContent||'').trim() : ''; if (label.includes('幅')) w = parseInt(el.value||'0'); if (label.includes('高さ')) h = parseInt(el.value||'0'); } // 2) fallback to pose resolution if (!w || !h) { const res = (window.poseEditorGlobals?.poseData?.resolution) || [512,512]; w = res[0]; h = res[1]; } if (!w || !h || w<=0 || h<=0) return; let dispW, dispH; if (w >= h) { dispW = baseLongSide; dispH = Math.max(1, Math.round(baseLongSide * (h/w))); } else { dispH = baseLongSide; dispW = Math.max(1, Math.round(baseLongSide * (w/h))); } if (c.width !== dispW || c.height !== dispH) { c.width = dispW; c.height = dispH; window.poseEditorGlobals.canvas = c; window.poseEditorGlobals.ctx = c.getContext('2d'); if (typeof coordinateTransformer?.updateResolution === 'function') { coordinateTransformer.updateResolution(null, [dispW, dispH]); } } } catch(e) {} } } })();