grmchn's picture
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) {}
}
}
})();