582 lines
28 KiB (Stored with Git LFS)
Plaintext
582 lines
28 KiB (Stored with Git LFS)
Plaintext
var ws = null;
|
|
var charactersData = {};
|
|
var presets = [];
|
|
|
|
function connectWebSocket() {
|
|
var wsUrl = 'ws://' + window.location.hostname + ':{{WS_PORT}}/retargeting';
|
|
ws = new WebSocket(wsUrl);
|
|
ws.onopen = function() {
|
|
updateConnectionStatus(true);
|
|
// 연결 즉시 캐릭터 목록 요청
|
|
send({ action: 'refresh' });
|
|
};
|
|
ws.onclose = function() { updateConnectionStatus(false); setTimeout(connectWebSocket, 3000); };
|
|
ws.onerror = function(e) { console.error('WS Error:', e); };
|
|
ws.onmessage = function(e) { handleMessage(JSON.parse(e.data)); };
|
|
}
|
|
|
|
function updateConnectionStatus(c) {
|
|
var s = document.getElementById('connectionStatus');
|
|
s.textContent = c ? '연결됨' : '연결 끊김';
|
|
s.className = 'connection-status ' + (c ? 'connected' : 'disconnected');
|
|
}
|
|
|
|
function handleMessage(d) {
|
|
if (d.type === 'characterList') updateCharacterList(d.characters);
|
|
else if (d.type === 'characterData') updateCharacterData(d);
|
|
else if (d.type === 'handPosePresets') { presets = d.presets; updateAllPresetGrids(); }
|
|
else if (d.type === 'status') showToast(d.message, d.success ? 'success' : 'error');
|
|
}
|
|
|
|
function updateCharacterList(chars) {
|
|
var container = document.getElementById('charactersContainer');
|
|
container.innerHTML = '';
|
|
|
|
if (chars.length === 0) {
|
|
container.innerHTML = '<div class="loading-message">등록된 캐릭터가 없습니다</div>';
|
|
return;
|
|
}
|
|
|
|
for (var i = 0; i < chars.length; i++) {
|
|
var charId = chars[i];
|
|
container.appendChild(createCharacterPanel(charId));
|
|
send({ action: 'getCharacterData', characterId: charId });
|
|
}
|
|
}
|
|
|
|
function createCharacterPanel(charId) {
|
|
var panel = document.createElement('div');
|
|
panel.className = 'character-panel';
|
|
panel.id = 'panel_' + charId;
|
|
|
|
panel.innerHTML =
|
|
'<div class="character-header">' +
|
|
'<h2>' + charId + '</h2>' +
|
|
'</div>' +
|
|
'<div class="character-content" id="content_' + charId + '">' +
|
|
'<div class="character-inner">' +
|
|
createSectionsHTML(charId) +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
return panel;
|
|
}
|
|
|
|
function createSectionsHTML(charId) {
|
|
var html = '';
|
|
|
|
// 1. 가중치 설정 (IK 가중치 통합) - 범위 슬라이더 사용
|
|
html += '<div class="section">' +
|
|
'<div class="section-header collapsed" onclick="toggleSection(this)">⚖️ 가중치 설정</div>' +
|
|
'<div class="section-content hidden">' +
|
|
'<div class="sub-section-title">손과 프랍과의 범위 (가중치 1→0)</div>' +
|
|
createRangeSlider(charId, 'limbMinDistance', 'limbMaxDistance', '거리 범위', 0, 1, 0.01) +
|
|
'<div class="sub-section-title">의자와 허리 거리 범위 (가중치 1→0)</div>' +
|
|
createRangeSlider(charId, 'hipsMinDistance', 'hipsMaxDistance', '거리 범위', 0, 1, 0.01) +
|
|
'<div class="sub-section-title">바닥과 허리 높이 블렌딩 (가중치 0→1)</div>' +
|
|
createRangeSlider(charId, 'groundHipsMinHeight', 'groundHipsMaxHeight', '높이 범위', 0, 2, 0.01) +
|
|
'<div class="sub-section-title">발 높이 IK 블렌딩 (가중치 1→0)</div>' +
|
|
createRangeSlider(charId, 'footHeightMinThreshold', 'footHeightMaxThreshold', '높이 범위', 0.1, 1, 0.01) +
|
|
'<div class="sub-section-title">가중치 보간</div>' +
|
|
createSlider(charId, 'weightSmoothSpeed', '변화 속도', 0.1, 20, 0.5) +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
// 2. 힙 위치 보정
|
|
html += '<div class="section">' +
|
|
'<div class="section-header collapsed" onclick="toggleSection(this)">🦴 힙 위치 보정 (로컬)</div>' +
|
|
'<div class="section-content hidden">' +
|
|
createSlider(charId, 'hipsHorizontal', '← 좌우 →', -1, 1, 0.001) +
|
|
createSlider(charId, 'hipsVertical', '↓ 상하 ↑', -1, 1, 0.001) +
|
|
createSlider(charId, 'hipsForward', '← 앞뒤 →', -1, 1, 0.001) +
|
|
'<div class="sub-section-title">의자 앉기 높이</div>' +
|
|
createSlider(charId, 'chairSeatHeightOffset', '높이 오프셋', -1, 1, 0.01) +
|
|
'<button class="btn btn-secondary" onclick="resetHips(\'' + charId + '\')">힙 위치 리셋</button>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
// 3. 무릎 위치 조정
|
|
html += '<div class="section">' +
|
|
'<div class="section-header collapsed" onclick="toggleSection(this)">🦵 무릎 위치 조정</div>' +
|
|
'<div class="section-content hidden">' +
|
|
createSlider(charId, 'kneeFrontBackWeight', '무릎 앞/뒤 가중치', -1, 1, 0.01) +
|
|
createSlider(charId, 'kneeInOutWeight', '무릎 안/밖 가중치', -1, 1, 0.01) +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
// 4. 발 IK 위치 조정
|
|
html += '<div class="section">' +
|
|
'<div class="section-header collapsed" onclick="toggleSection(this)">👟 발 IK 위치 조정</div>' +
|
|
'<div class="section-content hidden">' +
|
|
createSlider(charId, 'feetForwardBackward', '발 앞/뒤 오프셋', -1, 1, 0.01) +
|
|
createSlider(charId, 'feetNarrow', '발 벌리기/모으기', -1, 1, 0.01) +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
// 5. 손가락 제어 설정 (손 포즈 프리셋)
|
|
html += '<div class="section">' +
|
|
'<div class="section-header collapsed" onclick="toggleSection(this)">🖐️ 손가락 제어 설정</div>' +
|
|
'<div class="section-content hidden">' +
|
|
'<div class="toggle-row">' +
|
|
'<label class="toggle-label"><input type="checkbox" id="handPoseEnabled_' + charId + '" onchange="updateHandPoseEnabled(\'' + charId + '\', this.checked)"> <strong>스크립트 활성화</strong></label>' +
|
|
'</div>' +
|
|
'<div class="toggle-row">' +
|
|
'<label class="toggle-label"><input type="checkbox" id="leftHandEnabled_' + charId + '" onchange="updateToggle(\'' + charId + '\', \'leftHandEnabled\', this.checked)"> 왼손</label>' +
|
|
'<label class="toggle-label"><input type="checkbox" id="rightHandEnabled_' + charId + '" onchange="updateToggle(\'' + charId + '\', \'rightHandEnabled\', this.checked)"> 오른손</label>' +
|
|
'</div>' +
|
|
'<div class="preset-hint">클릭: 왼손 | 우클릭: 오른손</div>' +
|
|
'<div class="preset-grid" id="presetGrid_' + charId + '"></div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
// 6. 손가락 복제 설정
|
|
html += '<div class="section">' +
|
|
'<div class="section-header collapsed" onclick="toggleSection(this)">✋ 손가락 복제 설정</div>' +
|
|
'<div class="section-content hidden">' +
|
|
'<div class="control-group">' +
|
|
'<label style="font-size:0.8em;color:var(--text-muted)">복제 방식</label>' +
|
|
'<select id="fingerCopyMode_' + charId + '" onchange="updateFingerMode(\'' + charId + '\', this.value)">' +
|
|
'<option value="0">없음 (None)</option>' +
|
|
'<option value="1">Muscle Data</option>' +
|
|
'<option value="2">Rotation</option>' +
|
|
'<option value="3">Mingle</option>' +
|
|
'</select>' +
|
|
'</div>' +
|
|
'<div id="mingleSettings_' + charId + '" class="mingle-settings" style="display:none;">' +
|
|
'<div class="sub-section-title">Mingle 캘리브레이션</div>' +
|
|
'<p class="setting-description">소스 아바타의 손가락 회전 범위를 캘리브레이션하여 타겟에 적용합니다.</p>' +
|
|
'<div class="button-group">' +
|
|
'<button class="btn btn-secondary" onclick="calibrateMingleOpen(\'' + charId + '\')">펼침 기록 (Open)</button>' +
|
|
'<button class="btn btn-secondary" onclick="calibrateMingleClose(\'' + charId + '\')">모음 기록 (Close)</button>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
// 7. 모션 설정 (실제 에디터와 동일)
|
|
html += '<div class="section">' +
|
|
'<div class="section-header collapsed" onclick="toggleSection(this)">💪 모션 설정</div>' +
|
|
'<div class="section-content hidden">' +
|
|
'<div class="toggle-row">' +
|
|
'<label class="toggle-label"><input type="checkbox" id="useMotionFilter_' + charId + '" onchange="updateToggle(\'' + charId + '\', \'useMotionFilter\', this.checked)"> 모션 필터 사용</label>' +
|
|
'</div>' +
|
|
'<div id="motionFilterSettings_' + charId + '">' +
|
|
createSlider(charId, 'filterBufferSize', '필터 버퍼 크기', 1, 20, 1) +
|
|
'</div>' +
|
|
'<div class="toggle-row" style="margin-top:10px;">' +
|
|
'<label class="toggle-label"><input type="checkbox" id="useBodyRoughMotion_' + charId + '" onchange="updateToggle(\'' + charId + '\', \'useBodyRoughMotion\', this.checked)"> 몸 러프 모션 사용</label>' +
|
|
'</div>' +
|
|
'<div id="bodyRoughSettings_' + charId + '">' +
|
|
createSlider(charId, 'bodyRoughness', '몸 러프니스', 0, 20, 0.5) +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
// 8. 바닥 높이 설정
|
|
html += '<div class="section">' +
|
|
'<div class="section-header collapsed" onclick="toggleSection(this)">🏠 바닥 높이 설정</div>' +
|
|
'<div class="section-content hidden">' +
|
|
createSlider(charId, 'floorHeight', '바닥 높이 (-1 ~ 1)', -1, 1, 0.01) +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
// 9. 아바타/머리 크기 설정
|
|
html += '<div class="section">' +
|
|
'<div class="section-header collapsed" onclick="toggleSection(this)">📏 아바타 크기 설정</div>' +
|
|
'<div class="section-content hidden">' +
|
|
createSlider(charId, 'avatarScale', '아바타 크기', 0.1, 3, 0.01) +
|
|
createSlider(charId, 'headScale', '머리 크기', 0.1, 3, 0.01) +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
// 10. 머리 회전 오프셋
|
|
html += '<div class="section">' +
|
|
'<div class="section-header collapsed" onclick="toggleSection(this)">🗣️ 머리 회전 오프셋</div>' +
|
|
'<div class="section-content hidden">' +
|
|
createSlider(charId, 'headRotationOffsetX', 'X (Roll) - 좌우 기울기', -180, 180, 1) +
|
|
createSlider(charId, 'headRotationOffsetY', 'Y (Yaw) - 좌우 회전', -180, 180, 1) +
|
|
createSlider(charId, 'headRotationOffsetZ', 'Z (Pitch) - 상하 회전', -180, 180, 1) +
|
|
'<div class="button-group">' +
|
|
'<button class="btn btn-secondary" onclick="resetHeadRotation(\'' + charId + '\')">회전 초기화</button>' +
|
|
'<button class="btn btn-primary" onclick="calibrateHeadForward(\'' + charId + '\')">정면 캘리브레이션</button>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
// 11. 캘리브레이션 (마지막)
|
|
html += '<div class="section">' +
|
|
'<div class="section-header collapsed" onclick="toggleSection(this)">📐 캘리브레이션</div>' +
|
|
'<div class="section-content hidden">' +
|
|
'<div class="calibration-status" id="calibrationStatus_' + charId + '">데이터 없음</div>' +
|
|
'<div class="button-group">' +
|
|
'<button class="btn btn-primary" onclick="calibrateIPose(\'' + charId + '\')">I-포즈 캘리브레이션</button>' +
|
|
'<button class="btn btn-danger" onclick="resetCalibration(\'' + charId + '\')">캘리브레이션 초기화</button>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>';
|
|
|
|
return html;
|
|
}
|
|
|
|
function createSlider(charId, id, label, min, max, step) {
|
|
var fullId = id + '_' + charId;
|
|
return '<div class="control-group">' +
|
|
'<div class="slider-header">' +
|
|
'<label>' + label + '</label>' +
|
|
'<input type="number" class="value-input" id="' + fullId + 'Input" min="' + min + '" max="' + max + '" step="' + step + '" value="0" ' +
|
|
'onchange="inputValueChange(\'' + charId + '\', \'' + id + '\')" ' +
|
|
'onkeydown="inputKeyDown(event, \'' + charId + '\', \'' + id + '\')">' +
|
|
'</div>' +
|
|
'<div class="slider-row">' +
|
|
'<button class="btn-fine" onclick="fineAdjust(\'' + charId + '\', \'' + id + '\', ' + (-step) + ')">-</button>' +
|
|
'<input type="range" id="' + fullId + '" min="' + min + '" max="' + max + '" step="' + step + '" value="0" ' +
|
|
'oninput="sliderRealtime(\'' + charId + '\', \'' + id + '\')" ' +
|
|
'onchange="sliderFinish(\'' + charId + '\', \'' + id + '\')">' +
|
|
'<button class="btn-fine" onclick="fineAdjust(\'' + charId + '\', \'' + id + '\', ' + step + ')">+</button>' +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
function createRangeSlider(charId, minId, maxId, label, min, max, step) {
|
|
var minFullId = minId + '_' + charId;
|
|
var maxFullId = maxId + '_' + charId;
|
|
var fillId = 'rangeFill_' + minId + '_' + charId;
|
|
return '<div class="range-slider-group">' +
|
|
'<div class="range-slider-header">' +
|
|
'<label>' + label + '</label>' +
|
|
'<div class="range-slider-values">' +
|
|
'<input type="number" id="' + minFullId + 'Input" min="' + min + '" max="' + max + '" step="' + step + '" ' +
|
|
'onchange="rangeInputChange(\'' + charId + '\', \'' + minId + '\', \'' + maxId + '\', \'min\')">' +
|
|
'<span>~</span>' +
|
|
'<input type="number" id="' + maxFullId + 'Input" min="' + min + '" max="' + max + '" step="' + step + '" ' +
|
|
'onchange="rangeInputChange(\'' + charId + '\', \'' + minId + '\', \'' + maxId + '\', \'max\')">' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="range-slider-container">' +
|
|
'<div class="range-slider-track"></div>' +
|
|
'<div class="range-slider-fill" id="' + fillId + '"></div>' +
|
|
'<input type="range" id="' + minFullId + '" min="' + min + '" max="' + max + '" step="' + step + '" value="' + min + '" ' +
|
|
'oninput="rangeSliderInput(\'' + charId + '\', \'' + minId + '\', \'' + maxId + '\', \'min\')" ' +
|
|
'onchange="rangeSliderChange(\'' + charId + '\', \'' + minId + '\')">' +
|
|
'<input type="range" id="' + maxFullId + '" min="' + min + '" max="' + max + '" step="' + step + '" value="' + max + '" ' +
|
|
'oninput="rangeSliderInput(\'' + charId + '\', \'' + minId + '\', \'' + maxId + '\', \'max\')" ' +
|
|
'onchange="rangeSliderChange(\'' + charId + '\', \'' + maxId + '\')">' +
|
|
'</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
function rangeSliderInput(charId, minId, maxId, which) {
|
|
var minSlider = document.getElementById(minId + '_' + charId);
|
|
var maxSlider = document.getElementById(maxId + '_' + charId);
|
|
var minInput = document.getElementById(minId + '_' + charId + 'Input');
|
|
var maxInput = document.getElementById(maxId + '_' + charId + 'Input');
|
|
var fill = document.getElementById('rangeFill_' + minId + '_' + charId);
|
|
|
|
var minVal = parseFloat(minSlider.value);
|
|
var maxVal = parseFloat(maxSlider.value);
|
|
|
|
if (which === 'min' && minVal > maxVal) { minSlider.value = maxVal; minVal = maxVal; }
|
|
if (which === 'max' && maxVal < minVal) { maxSlider.value = minVal; maxVal = minVal; }
|
|
|
|
minInput.value = minVal.toFixed(2);
|
|
maxInput.value = maxVal.toFixed(2);
|
|
|
|
var rangeMin = parseFloat(minSlider.min);
|
|
var rangeMax = parseFloat(minSlider.max);
|
|
var leftPercent = ((minVal - rangeMin) / (rangeMax - rangeMin)) * 100;
|
|
var rightPercent = ((maxVal - rangeMin) / (rangeMax - rangeMin)) * 100;
|
|
fill.style.left = leftPercent + '%';
|
|
fill.style.width = (rightPercent - leftPercent) + '%';
|
|
|
|
if (which === 'min') {
|
|
send({ action: 'updateValueRealtime', characterId: charId, property: minId, value: minVal });
|
|
} else {
|
|
send({ action: 'updateValueRealtime', characterId: charId, property: maxId, value: maxVal });
|
|
}
|
|
}
|
|
|
|
function rangeSliderChange(charId, id) {
|
|
var slider = document.getElementById(id + '_' + charId);
|
|
send({ action: 'updateValueRealtime', characterId: charId, property: id, value: parseFloat(slider.value) });
|
|
}
|
|
|
|
function rangeInputChange(charId, minId, maxId, which) {
|
|
var minInput = document.getElementById(minId + '_' + charId + 'Input');
|
|
var maxInput = document.getElementById(maxId + '_' + charId + 'Input');
|
|
var minSlider = document.getElementById(minId + '_' + charId);
|
|
var maxSlider = document.getElementById(maxId + '_' + charId);
|
|
|
|
var minVal = parseFloat(minInput.value);
|
|
var maxVal = parseFloat(maxInput.value);
|
|
|
|
if (which === 'min' && minVal > maxVal) { minVal = maxVal; minInput.value = minVal.toFixed(2); }
|
|
if (which === 'max' && maxVal < minVal) { maxVal = minVal; maxInput.value = maxVal.toFixed(2); }
|
|
|
|
minSlider.value = minVal;
|
|
maxSlider.value = maxVal;
|
|
rangeSliderInput(charId, minId, maxId, which);
|
|
}
|
|
|
|
function updateRangeSlider(charId, minId, maxId, minVal, maxVal) {
|
|
var minSlider = document.getElementById(minId + '_' + charId);
|
|
var maxSlider = document.getElementById(maxId + '_' + charId);
|
|
var minInput = document.getElementById(minId + '_' + charId + 'Input');
|
|
var maxInput = document.getElementById(maxId + '_' + charId + 'Input');
|
|
var fill = document.getElementById('rangeFill_' + minId + '_' + charId);
|
|
|
|
if (!minSlider || !maxSlider) return;
|
|
|
|
minSlider.value = minVal;
|
|
maxSlider.value = maxVal;
|
|
minInput.value = minVal.toFixed(2);
|
|
maxInput.value = maxVal.toFixed(2);
|
|
|
|
var rangeMin = parseFloat(minSlider.min);
|
|
var rangeMax = parseFloat(minSlider.max);
|
|
var leftPercent = ((minVal - rangeMin) / (rangeMax - rangeMin)) * 100;
|
|
var rightPercent = ((maxVal - rangeMin) / (rangeMax - rangeMin)) * 100;
|
|
fill.style.left = leftPercent + '%';
|
|
fill.style.width = (rightPercent - leftPercent) + '%';
|
|
}
|
|
|
|
function toggleCharacterPanel(charId) {
|
|
var content = document.getElementById('content_' + charId);
|
|
content.classList.toggle('expanded');
|
|
}
|
|
|
|
function toggleSection(header) {
|
|
header.classList.toggle('collapsed');
|
|
header.nextElementSibling.classList.toggle('hidden');
|
|
}
|
|
|
|
function expandAll() {
|
|
var contents = document.querySelectorAll('.character-content');
|
|
contents.forEach(function(c) { c.classList.add('expanded'); });
|
|
var headers = document.querySelectorAll('.section-header');
|
|
headers.forEach(function(h) { h.classList.remove('collapsed'); h.nextElementSibling.classList.remove('hidden'); });
|
|
}
|
|
|
|
function collapseAll() {
|
|
var contents = document.querySelectorAll('.character-content');
|
|
contents.forEach(function(c) { c.classList.remove('expanded'); });
|
|
var headers = document.querySelectorAll('.section-header');
|
|
headers.forEach(function(h) { h.classList.add('collapsed'); h.nextElementSibling.classList.add('hidden'); });
|
|
}
|
|
|
|
function updateCharacterData(r) {
|
|
var charId = r.characterId;
|
|
var data = r.data;
|
|
charactersData[charId] = data;
|
|
|
|
// 일반 슬라이더 필드
|
|
var fields = ['hipsVertical','hipsForward','hipsHorizontal','kneeFrontBackWeight','kneeInOutWeight',
|
|
'feetForwardBackward','feetNarrow','floorHeight','avatarScale','headScale',
|
|
'weightSmoothSpeed','chairSeatHeightOffset',
|
|
'filterBufferSize','bodyRoughness',
|
|
'headRotationOffsetX','headRotationOffsetY','headRotationOffsetZ'];
|
|
|
|
for (var i = 0; i < fields.length; i++) {
|
|
setSliderValue(charId, fields[i], data[fields[i]]);
|
|
}
|
|
|
|
// 범위 슬라이더 업데이트
|
|
updateRangeSlider(charId, 'limbMinDistance', 'limbMaxDistance', data.limbMinDistance, data.limbMaxDistance);
|
|
updateRangeSlider(charId, 'hipsMinDistance', 'hipsMaxDistance', data.hipsMinDistance, data.hipsMaxDistance);
|
|
updateRangeSlider(charId, 'groundHipsMinHeight', 'groundHipsMaxHeight', data.groundHipsMinHeight, data.groundHipsMaxHeight);
|
|
updateRangeSlider(charId, 'footHeightMinThreshold', 'footHeightMaxThreshold', data.footHeightMinThreshold, data.footHeightMaxThreshold);
|
|
|
|
var fingerMode = document.getElementById('fingerCopyMode_' + charId);
|
|
if (fingerMode) {
|
|
fingerMode.value = data.fingerCopyMode;
|
|
updateMingleSettingsVisibility(charId);
|
|
}
|
|
|
|
setCheckbox(charId, 'handPoseEnabled', data.handPoseEnabled);
|
|
setCheckbox(charId, 'leftHandEnabled', data.leftHandEnabled);
|
|
setCheckbox(charId, 'rightHandEnabled', data.rightHandEnabled);
|
|
setCheckbox(charId, 'useMotionFilter', data.useMotionFilter);
|
|
setCheckbox(charId, 'useBodyRoughMotion', data.useBodyRoughMotion);
|
|
|
|
var cs = document.getElementById('calibrationStatus_' + charId);
|
|
if (cs) {
|
|
cs.textContent = data.hasCalibrationData ? '캘리브레이션 데이터가 저장되어 있습니다.' : '저장된 캘리브레이션 데이터가 없습니다.';
|
|
cs.className = 'calibration-status' + (data.hasCalibrationData ? ' has-data' : '');
|
|
}
|
|
|
|
updatePresetGrid(charId);
|
|
}
|
|
|
|
function setSliderValue(charId, id, val) {
|
|
var sl = document.getElementById(id + '_' + charId);
|
|
var inp = document.getElementById(id + '_' + charId + 'Input');
|
|
if (sl && val !== undefined && val !== null) sl.value = val;
|
|
if (inp && val !== undefined && val !== null) inp.value = Number(val).toFixed(3);
|
|
}
|
|
|
|
function setCheckbox(charId, id, checked) {
|
|
var cb = document.getElementById(id + '_' + charId);
|
|
if (cb) cb.checked = checked;
|
|
}
|
|
|
|
function sliderRealtime(charId, id) {
|
|
var sl = document.getElementById(id + '_' + charId);
|
|
var inp = document.getElementById(id + '_' + charId + 'Input');
|
|
if (inp) inp.value = Number(sl.value).toFixed(3);
|
|
send({ action: 'updateValueRealtime', characterId: charId, property: id, value: parseFloat(sl.value) });
|
|
}
|
|
|
|
function inputValueChange(charId, id) {
|
|
var sl = document.getElementById(id + '_' + charId);
|
|
var inp = document.getElementById(id + '_' + charId + 'Input');
|
|
if (sl && inp) {
|
|
var val = parseFloat(inp.value);
|
|
if (!isNaN(val)) {
|
|
val = Math.max(parseFloat(sl.min), Math.min(parseFloat(sl.max), val));
|
|
sl.value = val;
|
|
inp.value = Number(val).toFixed(3);
|
|
send({ action: 'updateValueRealtime', characterId: charId, property: id, value: val });
|
|
}
|
|
}
|
|
}
|
|
|
|
function inputKeyDown(event, charId, id) {
|
|
if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
inputValueChange(charId, id);
|
|
event.target.blur();
|
|
}
|
|
}
|
|
|
|
function sliderFinish(charId, id) {
|
|
// 드래그 완료
|
|
}
|
|
|
|
function fineAdjust(charId, id, delta) {
|
|
var sl = document.getElementById(id + '_' + charId);
|
|
var inp = document.getElementById(id + '_' + charId + 'Input');
|
|
var nv = Math.max(parseFloat(sl.min), Math.min(parseFloat(sl.max), parseFloat(sl.value) + delta));
|
|
sl.value = nv;
|
|
if (inp) inp.value = Number(nv).toFixed(3);
|
|
send({ action: 'updateValueRealtime', characterId: charId, property: id, value: nv });
|
|
}
|
|
|
|
function updateToggle(charId, prop, checked) {
|
|
send({ action: 'updateValueRealtime', characterId: charId, property: prop, value: checked ? 1 : 0 });
|
|
|
|
// 왼손/오른손 활성화 시 스크립트 활성화 체크박스도 업데이트
|
|
if ((prop === 'leftHandEnabled' || prop === 'rightHandEnabled') && checked) {
|
|
var scriptCheckbox = document.getElementById('handPoseEnabled_' + charId);
|
|
if (scriptCheckbox) scriptCheckbox.checked = true;
|
|
}
|
|
}
|
|
|
|
function updateHandPoseEnabled(charId, checked) {
|
|
send({ action: 'updateValueRealtime', characterId: charId, property: 'handPoseEnabled', value: checked ? 1 : 0 });
|
|
}
|
|
|
|
function updateFingerMode(charId, val) {
|
|
send({ action: 'updateValueRealtime', characterId: charId, property: 'fingerCopyMode', value: parseFloat(val) });
|
|
updateMingleSettingsVisibility(charId);
|
|
}
|
|
|
|
function updateMingleSettingsVisibility(charId) {
|
|
var mingleDiv = document.getElementById('mingleSettings_' + charId);
|
|
var fingerMode = document.getElementById('fingerCopyMode_' + charId);
|
|
if (mingleDiv && fingerMode) {
|
|
mingleDiv.style.display = fingerMode.value === '3' ? 'block' : 'none';
|
|
}
|
|
}
|
|
|
|
function calibrateIPose(charId) {
|
|
send({ action: 'calibrateIPose', characterId: charId });
|
|
}
|
|
|
|
function resetCalibration(charId) {
|
|
send({ action: 'resetCalibration', characterId: charId });
|
|
}
|
|
|
|
function calibrateMingleOpen(charId) {
|
|
send({ action: 'calibrateMingleOpen', characterId: charId });
|
|
showToast('펼침 상태 기록됨', 'success');
|
|
}
|
|
|
|
function calibrateMingleClose(charId) {
|
|
send({ action: 'calibrateMingleClose', characterId: charId });
|
|
showToast('모음 상태 기록됨', 'success');
|
|
}
|
|
|
|
function resetHips(charId) {
|
|
['hipsVertical','hipsForward','hipsHorizontal'].forEach(function(id) {
|
|
setSliderValue(charId, id, 0);
|
|
send({ action: 'updateValueRealtime', characterId: charId, property: id, value: 0 });
|
|
});
|
|
}
|
|
|
|
function resetHeadRotation(charId) {
|
|
['headRotationOffsetX','headRotationOffsetY','headRotationOffsetZ'].forEach(function(id) {
|
|
setSliderValue(charId, id, 0);
|
|
send({ action: 'updateValueRealtime', characterId: charId, property: id, value: 0 });
|
|
});
|
|
}
|
|
|
|
function calibrateHeadForward(charId) {
|
|
send({ action: 'calibrateHeadForward', characterId: charId });
|
|
showToast('정면 캘리브레이션 실행됨', 'success');
|
|
// 값 새로고침
|
|
setTimeout(function() {
|
|
send({ action: 'getCharacterData', characterId: charId });
|
|
}, 100);
|
|
}
|
|
|
|
function updateAllPresetGrids() {
|
|
for (var charId in charactersData) {
|
|
updatePresetGrid(charId);
|
|
}
|
|
}
|
|
|
|
function updatePresetGrid(charId) {
|
|
var grid = document.getElementById('presetGrid_' + charId);
|
|
if (!grid) return;
|
|
|
|
grid.innerHTML = '';
|
|
for (var i = 0; i < presets.length; i++) {
|
|
var btn = document.createElement('button');
|
|
btn.className = 'preset-btn';
|
|
btn.textContent = presets[i];
|
|
btn.setAttribute('data-name', presets[i]);
|
|
btn.setAttribute('data-char', charId);
|
|
btn.onclick = function() { applyPreset(this.getAttribute('data-char'), this.getAttribute('data-name'), 'leftHandPreset'); };
|
|
btn.oncontextmenu = function(e) { e.preventDefault(); applyPreset(this.getAttribute('data-char'), this.getAttribute('data-name'), 'rightHandPreset'); };
|
|
grid.appendChild(btn);
|
|
}
|
|
}
|
|
|
|
function applyPreset(charId, name, hand) {
|
|
send({ action: 'setHandPosePreset', characterId: charId, property: hand, stringValue: name });
|
|
|
|
// 프리셋 적용 시 스크립트 활성화 및 해당 손 활성화 체크박스 업데이트
|
|
var scriptCheckbox = document.getElementById('handPoseEnabled_' + charId);
|
|
if (scriptCheckbox) scriptCheckbox.checked = true;
|
|
|
|
var handCheckbox = document.getElementById((hand === 'leftHandPreset' ? 'leftHandEnabled' : 'rightHandEnabled') + '_' + charId);
|
|
if (handCheckbox) handCheckbox.checked = true;
|
|
}
|
|
|
|
function refresh() {
|
|
send({ action: 'refresh' });
|
|
showToast('새로고침...', 'success');
|
|
}
|
|
|
|
function send(data) {
|
|
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(data));
|
|
}
|
|
|
|
function showToast(msg, type) {
|
|
var t = document.getElementById('toast');
|
|
t.textContent = msg;
|
|
t.className = 'toast show ' + (type || '');
|
|
setTimeout(function() { t.className = 'toast'; }, 2000);
|
|
}
|
|
|
|
window.onload = function() { connectWebSocket(); };
|