Spaces:
Sleeping
Sleeping
feat(issue-045): Canvas aspect sync + data scaling + export fixes\n\n- Canvas display follows output aspect (long-side 640)\n- Align poseData resolution to output form on load\n- Keep JS->Python resolution sync; fix background fit\n- Export uses declared resolution; safe fallback\n\nClose: issue_045_キャンバスサイズ動的更新機能実装.md
ac4b077
| // 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) {} | |
| } | |
| } | |
| })(); | |