Allposes
d29942647fb7c8dd82123234c19835e3706779e16b757d6ac8c879b71f3fc73959c40dacbee85a9f2a9448a96e2424295b414ae8c13866672f5de752d0ae357538a54db595182b39c02241b6691492bb502d1b8bd3247f913b4fd3ff2e9c86beb37957e141457ae7f4485903967ce0c010aebe3acf8ba95e007732737c5c7725
All
Generate Entry
Image URL (500x350px): Link URL: Link Title: Description (optional): Date (Unix timestamp as yyyy-mm-dd-hh-mm):

[PoseModel1 loc] [PoseModel2 loc] NO POSE - STOCK

Wissy n Cea :)

Date: 2025-11-29-16:41

Fedis tech in a nutshell. Good Thing wissy is here => Result Img

Date: 2025-11-05-07:59

Standart TPose twrds e.o.

Date: 2025-11-02-07:20

src

    photostudio.html
    --
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>PMX + VPD Viewer</title>
    <script src="./libs/three.js"></script>
    <script src="./libs/mmdparser.min.js"></script>
    <script src="./libs/MMDAnimationHelper.js"></script>
    <script src="./libs/CCDIKSolver.js"></script>
    <script src="./libs/ammo.min.js"></script>
    <script src="./libs/TGALoader.js"></script>
    <script src="./libs/MMDLoader.js"></script>
    <style>
    *{margin:0;padding:0;box-sizing:border-box;touch-action:none}
    body{overflow:hidden;background:#1a1a2e;color:white;font-family:system-ui;height:100vh;width:100vw}
    #container{position:fixed;top:0;left:0;width:100%;height:100%;z-index:1}
    #loading{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.7);padding:20px 30px;border-radius:10px;z-index:10;text-align:center;font-size:18px}
    #info{position:fixed;top:10px;left:10px;background:rgba(0,0,0,0.7);padding:12px;border-radius:10px;z-index:5;max-width:90%;font-size:14px;line-height:1.4}
    #info h1{font-size:16px;margin-bottom:8px}
    #controls{position:fixed;bottom:20px;left:0;width:100%;display:flex;justify-content:center;gap:15px;z-index:5;padding:0 20px}
    .control-btn{background:rgba(30,30,60,0.8);border:2px solid #4a4a8a;color:white;padding:12px 20px;border-radius:50px;font-size:16px;font-weight:600;cursor:pointer;box-shadow:0 4px 8px rgba(0,0,0,0.3);transition:all 0.2s;min-width:140px}
    .control-btn:active{background:rgba(60,60,100,0.9);transform:scale(0.95)}
    .control-btn.active{background:rgba(80,80,160,0.9);border-color:#8a8aff}
    #model-selector{position:fixed;top:80px;right:10px;background:rgba(0,0,0,0.7);padding:10px;border-radius:10px;z-index:5;display:flex;flex-direction:column;gap:8px}
    .model-btn{background:rgba(40,40,80,0.8);border:1px solid #5a5aaa;color:white;padding:8px 12px;border-radius:6px;font-size:14px;cursor:pointer}
    .model-btn.active{background:rgba(80,80,160,0.9);border-color:#8a8aff}
    #help-overlay{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.9);padding:20px;border-radius:10px;z-index:20;max-width:90%;max-height:80%;overflow-y:auto;display:none}
    #help-overlay h2{margin-bottom:15px;text-align:center}
    .help-section{margin-bottom:15px}
    .help-section h3{margin-bottom:8px;color:#8a8aff}
    .help-key{display:inline-block;background:rgba(255,255,255,0.2);padding:2px 6px;border-radius:4px;margin:0 2px;font-family:monospace}
    #close-help{display:block;margin:15px auto 0;padding:8px 20px;background:#4a4a8a;border:none;border-radius:5px;color:white;cursor:pointer}
    #mode-indicator{position:fixed;top:10px;right:60px;background:rgba(0,0,0,0.7);padding:8px 12px;border-radius:6px;z-index:5;font-size:14px}
    #bone-controls{position:fixed;top:150px;left:10px;background:rgba(0,0,0,0.7);padding:10px;border-radius:10px;z-index:5;display:none;flex-direction:column;gap:8px;max-width:200px}
    .bone-section{margin-bottom:10px}
    .bone-section h4{margin-bottom:5px;color:#8a8aff;font-size:12px}
    .bone-btn{background:rgba(60,60,100,0.8);border:1px solid #6a6aaa;color:white;padding:4px 8px;border-radius:4px;font-size:12px;cursor:pointer;margin:2px;width:calc(50% - 4px);display:inline-block;text-align:center}
    .bone-btn.active{background:rgba(100,100,200,0.9);border-color:#8a8aff}
    #selected-bone-info{position:fixed;top:50%;left:10px;background:rgba(0,0,0,0.8);padding:10px;border-radius:10px;z-index:5;font-size:12px;display:none}
    #bone-transform-controls{position:fixed;bottom:120px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.7);padding:10px;border-radius:10px;z-index:5;display:none;gap:10px}
    .transform-btn{background:rgba(60,60,100,0.8);border:1px solid #6a6aaa;color:white;padding:8px 12px;border-radius:6px;font-size:14px;cursor:pointer;min-width:80px}
    .transform-btn.active{background:rgba(100,100,200,0.9);border-color:#8a8aff}
    #gesture-help{position:fixed;bottom:180px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.7);padding:8px 12px;border-radius:6px;z-index:5;font-size:12px;text-align:center;display:none}
    #stored-pose-controls{position:fixed;top:80px;left:10px;background:rgba(0,0,0,0.7);padding:10px;border-radius:10px;z-index:5;display:flex;flex-direction:column;gap:8px;max-width:200px}
    .stored-pose-section{margin-bottom:10px}
    .stored-pose-section h4{margin-bottom:5px;color:#8a8aff;font-size:12px}
    .stored-pose-btn{background:rgba(60,100,60,0.8);border:1px solid #6aaa6a;color:white;padding:6px 10px;border-radius:4px;font-size:12px;cursor:pointer;margin:2px;width:100%;text-align:center}
    .stored-pose-btn:active{background:rgba(80,140,80,0.9);transform:scale(0.95)}
    #state-controls{position:fixed;bottom:80px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.7);padding:10px;border-radius:10px;z-index:5;display:flex;flex-direction:column;gap:10px;width:90%;max-width:600px}
    .state-btn{background:rgba(100,60,100,0.8);border:1px solid #8a6a8a;color:white;padding:8px 12px;border-radius:6px;font-size:14px;cursor:pointer;min-width:100px}
    .state-btn:active{background:rgba(140,80,140,0.9);transform:scale(0.95)}
    #state-textbox{background:rgba(0,0,0,0.8);border:1px solid #4a4a8a;border-radius:6px;color:white;padding:8px;font-size:12px;font-family:monospace;width:100%;min-height:60px;resize:vertical;display:none}
    #help-btn{position:fixed;top:10px;right:10px;background:rgba(30,30,60,0.8);border:2px solid #4a4a8a;color:white;padding:8px 12px;border-radius:50%;width:40px;height:40px;cursor:pointer;z-index:5;font-size:18px;display:flex;align-items:center;justify-content:center}
    .bone-helper{cursor:pointer;transition:all 0.2s}
    .bone-helper:hover{transform:scale(1.2)}
    #mobile-gesture-info{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.9);padding:20px;border-radius:10px;z-index:15;text-align:center;display:none;max-width:90%}
    .gesture-item{margin:10px 0;padding:10px;background:rgba(255,255,255,0.1);border-radius:5px}
    @media (max-width:480px){#info{font-size:12px;padding:10px}#info h1{font-size:14px}.control-btn{padding:10px 15px;font-size:14px;min-width:120px}#mode-indicator{top:60px;right:10px}#bone-controls{top:120px;max-width:180px}#bone-transform-controls{bottom:100px;flex-wrap:wrap;justify-content:center}.transform-btn{padding:6px 10px;font-size:12px;min-width:70px}#gesture-help{bottom:160px;font-size:11px}#stored-pose-controls{top:60px;max-width:180px}#state-controls{bottom:70px;flex-wrap:wrap;justify-content:center}.state-btn{padding:6px 10px;font-size:12px;min-width:90px}#state-textbox{font-size:11px;min-height:50px}}
    </style>
    </head>
    <body>
    <div id="container"></div>
    <div id="loading">Loading models...</div>
    <div id="info">
    <h1>PMX + VPD Viewer</h1>
    <p>Touch models to move them, empty space to rotate/camera</p>
    <p id="modelInfo"></p>
    </div>

    <div id="mode-indicator">Mode: Model Control</div>
    <button id="help-btn">?</button>

    <div id="selected-bone-info">No bone selected</div>

    <div id="mobile-gesture-info">
    <h3>Mobile Gestures Guide</h3>
    <div class="gesture-item"><strong>Touch Model + Drag:</strong> Move model</div>
    <div class="gesture-item"><strong>Touch Empty Space + Drag:</strong> Rotate model (Model Mode) / Orbit camera (Camera Mode)</div>
    <div class="gesture-item"><strong>Two Finger Drag:</strong> Pan camera</div>
    <div class="gesture-item"><strong>Pinch:</strong> Zoom camera</div>
    <div class="gesture-item"><strong>Double Tap Bone:</strong> Select bone (Bone Mode)</div>
    <div class="gesture-item"><strong>Long Press + Drag Bone:</strong> Move bone (Bone Mode)</div>
    <button id="close-gesture-help" style="margin-top:15px;padding:8px 16px;background:#4a4a8a;color:white;border:none;border-radius:5px;cursor:pointer">Got it!</button>
    </div>

    <div id="state-controls">
    <button id="generate-state" class="state-btn">Generate State URL</button>
    <textarea id="state-textbox" placeholder="Click 'Generate State URL' to create a shareable URL with current camera and model positions"></textarea>
    </div>

    <div id="stored-pose-controls">
    <div class="stored-pose-section">
    <h4>Stored Poses</h4>
    <button id="reload-stor1" class="stored-pose-btn">Reload Pose 1</button>
    <button id="reload-stor2" class="stored-pose-btn">Reload Pose 2</button>
    <button id="reload-both" class="stored-pose-btn">Reload Both</button>
    </div>
    </div>

    <div id="bone-controls">
    <div class="bone-section"><h4>Head & Neck</h4><button class="bone-btn" data-bone="neck">Neck</button><button class="bone-btn" data-bone="head">Head</button></div>
    <div class="bone-section"><h4>Arms & Hands</h4><button class="bone-btn" data-bone="shoulder_l">Left Shoulder</button><button class="bone-btn" data-bone="shoulder_r">Right Shoulder</button><button class="bone-btn" data-bone="arm_l">Left Arm</button><button class="bone-btn" data-bone="arm_r">Right Arm</button><button class="bone-btn" data-bone="hand_l">Left Hand</button><button class="bone-btn" data-bone="hand_r">Right Hand</button></div>
    <div class="bone-section"><h4>Legs & Feet</h4><button class="bone-btn" data-bone="leg_l">Left Leg</button><button class="bone-btn" data-bone="leg_r">Right Leg</button><button class="bone-btn" data-bone="knee_l">Left Knee</button><button class="bone-btn" data-bone="knee_r">Right Knee</button><button class="bone-btn" data-bone="foot_l">Left Foot</button><button class="bone-btn" data-bone="foot_r">Right Foot</button></div>
    <div class="bone-section"><h4>Spine</h4><button class="bone-btn" data-bone="spine">Spine</button><button class="bone-btn" data-bone="waist">Waist</button></div>
    <button id="reset-bones" class="control-btn" style="margin-top:10px;min-width:auto">Reset Bones</button>
    </div>

    <div id="bone-transform-controls">
    <button id="move-bone" class="transform-btn active">Move</button>
    <button id="rotate-bone" class="transform-btn">Rotate</button>
    <button id="reset-bone-transform" class="transform-btn">Reset</button>
    </div>

    <div id="gesture-help"><div>👆 Touch model to move, empty space to rotate</div></div>

    <div id="help-overlay">
    <h2>Controls Guide</h2>
    <div class="help-section"><h3>Mobile Touch Controls</h3><p><strong>Touch Model + Drag:</strong> Move model</p><p><strong>Touch Empty Space + Drag:</strong> Rotate model (Model Mode) / Orbit camera (Camera Mode)</p><p><strong>Two Finger Drag:</strong> Pan camera</p><p><strong>Pinch:</strong> Zoom camera</p><p><strong>Double Tap Bone:</strong> Select bone (Bone Mode)</p><p><strong>Long Press + Drag Bone:</strong> Move bone (Bone Mode)</p></div>
    <div class="help-section"><h3>Control Modes</h3><p><span class="help-key">Model Mode</span> - Move and rotate models</p><p><span class="help-key">Camera Mode</span> - Control camera view</p><p><span class="help-key">Bone Mode</span> - Edit individual bone positions</p></div>
    <div class="help-section"><h3>Bone Editing (Bone Mode Only)</h3><p><span class="help-key">Tap Bone</span> - Select bone</p><p><span class="help-key">Long Press + Drag</span> - Move selected bone</p><p><span class="help-key">Two Finger Rotate</span> - Rotate selected bone</p></div>
    <div class="help-section"><h3>Stored Poses</h3><p><span class="help-key">Reload Pose 1</span> - Re-fetch and apply stor1 to Model 1</p><p><span class="help-key">Reload Pose 2</span> - Re-fetch and apply stor2 to Model 2</p><p><span class="help-key">Reload Both</span> - Re-fetch and apply both poses</p></div>
    <div class="help-section"><h3>State Management</h3><p><span class="help-key">Generate State URL</span> - Create URL with current scene state (camera position, model positions)</p><p><span class="help-key">Text Box</span> - Copy the generated URL manually to share your scene</p></div>
    <button id="close-help">Close</button>
    </div>

    <div id="model-selector">
    <button class="model-btn active" data-model="both">Both Models</button>
    <button class="model-btn" data-model="model1">Model 1</button>
    <button class="model-btn" data-model="model2">Model 2</button>
    </div>

    <div id="controls">
    <button id="toggleMode" class="control-btn">Camera Mode</button>
    <button id="toggleBones" class="control-btn">Bone Mode</button>
    <button id="resetView" class="control-btn">Reset View</button>
    <button id="toggleWireframe" class="control-btn">Wireframe</button>
    </div>

    <script>
    // Get URL parameters
    const urlParams = new URLSearchParams(window.location.search);
    const pmxPath1 = urlParams.get("pmx");
    const pmxPath2 = urlParams.get("pmx2");
    const vpdPath1 = urlParams.get("vpd");
    const vpdPath2 = urlParams.get("vpd2");

    // State parameters - simple format
    const stateCam = urlParams.get("cam");
    const stateM1 = urlParams.get("m1");
    const stateM2 = urlParams.get("m2");

    let scene, camera, renderer;
    let model1, model2;
    let loader;

    // Input control variables
    let isRotating = false;
    let isMovingModel = false;
    let isPanning = false;
    let rotateStartX = 0, rotateStartY = 0;
    let moveStartX = 0, moveStartY = 0;
    let panStartX = 0, panStartY = 0;
    let currentModel = 'both';
    let controlMode = 'model';

    // Keyboard state
    const keys = {};

    // Camera control variables
    let cameraTarget = new THREE.Vector3(0, 0, 0);
    let cameraDistance = 25;
    let cameraPhi = Math.PI / 6;
    let cameraTheta = 0;

    // Touch state
    let touchState = {
    isTwoFinger: false,
    initialDistance: 0,
    initialPan: { x: 0, y: 0 },
    lastTapTime: 0,
    isLongPress: false,
    longPressTimer: null,
    isTouchingModel: false
    };

    // Bone editing variables
    let selectedBone = null;
    let selectedModel = null;
    let boneHelpers = [];
    let isDraggingBone = false;
    let boneTransformMode = 'move';
    let dragPlane = new THREE.Plane();
    let dragStartPoint = new THREE.Vector3();
    let dragStartBonePosition = new THREE.Vector3();
    let dragStartBoneRotation = new THREE.Euler();

    const boneMappings = {
    'neck': ['首', 'neck', 'kubi'],
    'head': ['頭', 'head', 'atama'],
    'shoulder_l': ['左肩', 'shoulder_l', 'left shoulder', '左腕'],
    'shoulder_r': ['右肩', 'shoulder_r', 'right shoulder', '右腕'],
    'arm_l': ['左腕', 'arm_l', 'left arm', '左上腕'],
    'arm_r': ['右腕', 'arm_r', 'right arm', '右上腕'],
    'hand_l': ['左手', 'hand_l', 'left hand', '左手首'],
    'hand_r': ['右手', 'hand_r', 'right hand', '右手首'],
    'leg_l': ['左足', 'leg_l', 'left leg', '左大腿'],
    'leg_r': ['右足', 'leg_r', 'right leg', '右大腿'],
    'knee_l': ['左ひざ', 'knee_l', 'left knee', '左ひざ'],
    'knee_r': ['右ひざ', 'knee_r', 'right knee', '右ひざ'],
    'foot_l': ['左足首', 'foot_l', 'left foot', '左足先'],
    'foot_r': ['右足首', 'foot_r', 'right foot', '右足先'],
    'spine': ['上半身', 'spine', 'upper body', '上半身2'],
    'waist': ['下半身', 'waist', 'lower body', '腰']
    };

    // Essential functions that were missing
    function updateCameraPosition() {
    const spherical = new THREE.Spherical(cameraDistance, cameraPhi, cameraTheta);
    const position = new THREE.Vector3();
    position.setFromSpherical(spherical);
    position.add(cameraTarget);

    camera.position.copy(position);
    camera.lookAt(cameraTarget);
    }

    function setupTouchControls() {
    const container = document.getElementById('container');
    const gestureHelp = document.getElementById('gesture-help');

    container.addEventListener('touchstart', function(e) {
    const now = Date.now();
    const isDoubleTap = (now - touchState.lastTapTime) < 300;
    touchState.lastTapTime = now;

    gestureHelp.style.display = 'block';
    setTimeout(() => gestureHelp.style.display = 'none', 2000);

    if (e.touches.length === 1) {
    const touch = e.touches[0];
    const mouse = new THREE.Vector2();
    mouse.x = (touch.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1;

    const raycaster = new THREE.Raycaster();
    raycaster.setFromCamera(mouse, camera);

    const targetModels = getTargetModels();
    let hitModel = false;

    for (const model of targetModels) {
    if (!model) continue;
    const intersects = raycaster.intersectObject(model, true);
    if (intersects.length > 0) {
    hitModel = true;
    break;
    }
    }

    if (hitModel && controlMode === 'model') {
    touchState.isTouchingModel = true;
    isMovingModel = true;
    moveStartX = touch.clientX;
    moveStartY = touch.clientY;
    } else if (controlMode === 'bone' && isDoubleTap) {
    handleBoneSelection(e);
    e.preventDefault();
    return;
    } else {
    touchState.isTouchingModel = false;
    isRotating = true;
    rotateStartX = touch.clientX;
    rotateStartY = touch.clientY;

    if (controlMode === 'bone') {
    touchState.longPressTimer = setTimeout(() => {
    touchState.isLongPress = true;
    if (handleBoneDragStart(e, true)) {
    e.preventDefault();
    return;
    }
    }, 500);
    }
    }

    } else if (e.touches.length === 2) {
    touchState.isTwoFinger = true;
    const touch1 = e.touches[0];
    const touch2 = e.touches[1];

    touchState.initialDistance = Math.hypot(
    touch2.clientX - touch1.clientX,
    touch2.clientY - touch1.clientY
    );

    touchState.initialPan.x = (touch1.clientX + touch2.clientX) / 2;
    touchState.initialPan.y = (touch1.clientY + touch2.clientY) / 2;

    panStartX = touchState.initialPan.x;
    panStartY = touchState.initialPan.y;
    }

    e.preventDefault();
    });

    container.addEventListener('touchmove', function(e) {
    if (controlMode === 'bone' && isDraggingBone) {
    handleBoneDrag(e, true);
    return;
    }

    if (e.touches.length === 1) {
    const touch = e.touches[0];

    if (touchState.isTouchingModel && controlMode === 'model' && isMovingModel) {
    const deltaX = touch.clientX - moveStartX;
    const deltaY = touch.clientY - moveStartY;

    const moveSpeed = 0.05;
    const targetModels = getTargetModels();
    targetModels.forEach(model => {
    if (model) {
    model.position.x += deltaX * moveSpeed;
    model.position.z -= deltaY * moveSpeed;
    }
    });

    moveStartX = touch.clientX;
    moveStartY = touch.clientY;
    } else if (isRotating) {
    const deltaX = touch.clientX - rotateStartX;
    const deltaY = touch.clientY - rotateStartY;

    if (controlMode === 'model') {
    const rotationSpeed = 0.01;
    const targetModels = getTargetModels();
    targetModels.forEach(model => {
    if (model) {
    model.rotation.y += deltaX * rotationSpeed;
    model.rotation.x += deltaY * rotationSpeed;
    }
    });
    } else if (controlMode === 'camera') {
    const orbitSpeed = 0.01;
    cameraTheta -= deltaX * orbitSpeed;
    cameraPhi = Math.max(0.1, Math.min(Math.PI - 0.1, cameraPhi + deltaY * orbitSpeed));
    updateCameraPosition();
    }

    rotateStartX = touch.clientX;
    rotateStartY = touch.clientY;
    }
    } else if (e.touches.length === 2 && touchState.isTwoFinger) {
    const touch1 = e.touches[0];
    const touch2 = e.touches[1];

    const currentDistance = Math.hypot(
    touch2.clientX - touch1.clientX,
    touch2.clientY - touch1.clientY
    );
    const zoomDelta = (touchState.initialDistance - currentDistance) * 0.01;
    cameraDistance = Math.max(5, Math.min(100, cameraDistance + zoomDelta));
    touchState.initialDistance = currentDistance;

    const midX = (touch1.clientX + touch2.clientX) / 2;
    const midY = (touch1.clientY + touch2.clientY) / 2;
    const deltaX = midX - panStartX;
    const deltaY = midY - panStartY;

    const panSpeed = 0.005;
    const panVector = new THREE.Vector3(-deltaX, deltaY, 0).multiplyScalar(panSpeed);
    panVector.applyQuaternion(camera.quaternion);
    cameraTarget.add(panVector);

    panStartX = midX;
    panStartY = midY;

    updateCameraPosition();
    }

    e.preventDefault();
    });

    container.addEventListener('touchend', function(e) {
    if (touchState.longPressTimer) {
    clearTimeout(touchState.longPressTimer);
    touchState.longPressTimer = null;
    }

    if (controlMode === 'bone') {
    handleBoneDragEnd();
    touchState.isLongPress = false;
    }

    isMovingModel = false;
    isRotating = false;
    touchState.isTouchingModel = false;

    if (e.touches.length < 2) {
    touchState.isTwoFinger = false;
    }
    });
    }

    function setupMouseControls() {
    const container = document.getElementById('container');
    let isMouseDown = false;
    let isRightClick = false;
    let isMiddleClick = false;
    let lastMouseX = 0;
    let lastMouseY = 0;

    container.addEventListener('mousedown', function(e) {
    isMouseDown = true;
    isRightClick = e.button === 2;
    isMiddleClick = e.button === 1;
    lastMouseX = e.clientX;
    lastMouseY = e.clientY;

    const mouse = new THREE.Vector2();
    mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;

    const raycaster = new THREE.Raycaster();
    raycaster.setFromCamera(mouse, camera);

    const targetModels = getTargetModels();
    let hitModel = false;

    for (const model of targetModels) {
    if (!model) continue;
    const intersects = raycaster.intersectObject(model, true);
    if (intersects.length > 0) {
    hitModel = true;
    break;
    }
    }

    if (hitModel && controlMode === 'model' && e.button === 0) {
    isMovingModel = true;
    moveStartX = e.clientX;
    moveStartY = e.clientY;
    } else {
    if (controlMode === 'bone') {
    if (handleBoneDragStart(e, false)) {
    return;
    }

    if (e.button === 0) {
    const intersects = raycaster.intersectObjects(boneHelpers);
    if (intersects.length > 0) {
    selectedBone = intersects[0].object.userData.bone;
    selectedModel = intersects[0].object.userData.model;
    updateBoneSelection();
    updateBoneButtons();
    }
    }
    }

    isRotating = true;
    rotateStartX = e.clientX;
    rotateStartY = e.clientY;
    }

    e.preventDefault();
    });

    container.addEventListener('mousemove', function(e) {
    if (controlMode === 'bone' && isDraggingBone) {
    handleBoneDrag(e, false);
    return;
    }

    if (!isMouseDown) return;

    const deltaX = e.clientX - lastMouseX;
    const deltaY = e.clientY - lastMouseY;

    if (isMovingModel && controlMode === 'model') {
    const moveSpeed = 0.02;
    const targetModels = getTargetModels();
    targetModels.forEach(model => {
    if (model) {
    model.position.x += deltaX * moveSpeed;
    model.position.z -= deltaY * moveSpeed;
    }
    });
    } else if (isRotating) {
    if (controlMode === 'model') {
    const targetModels = getTargetModels();

    if (isMiddleClick) {
    const panSpeed = 0.01;
    const panVector = new THREE.Vector3(-deltaX, deltaY, 0).multiplyScalar(panSpeed);
    panVector.applyQuaternion(camera.quaternion);
    cameraTarget.add(panVector);
    updateCameraPosition();
    } else if (isRightClick) {
    const moveSpeed = 0.02;
    targetModels.forEach(model => {
    if (model) {
    model.position.x += deltaX * moveSpeed;
    model.position.y -= deltaY * moveSpeed;
    }
    });
    } else {
    const rotationSpeed = 0.01;
    targetModels.forEach(model => {
    if (model) {
    model.rotation.y += deltaX * rotationSpeed;
    model.rotation.x += deltaY * rotationSpeed;
    }
    });
    }
    } else if (controlMode === 'camera') {
    if (isRightClick || isMiddleClick) {
    const panSpeed = 0.01;
    const panVector = new THREE.Vector3(-deltaX, deltaY, 0).multiplyScalar(panSpeed);
    panVector.applyQuaternion(camera.quaternion);
    cameraTarget.add(panVector);
    updateCameraPosition();
    } else {
    const orbitSpeed = 0.01;
    cameraTheta -= deltaX * orbitSpeed;
    cameraPhi = Math.max(0.1, Math.min(Math.PI - 0.1, cameraPhi + deltaY * orbitSpeed));
    updateCameraPosition();
    }
    }
    }

    lastMouseX = e.clientX;
    lastMouseY = e.clientY;
    });

    container.addEventListener('mouseup', function() {
    isMouseDown = false;
    isMovingModel = false;
    isRotating = false;
    if (controlMode === 'bone') {
    handleBoneDragEnd();
    }
    });

    container.addEventListener('wheel', function(e) {
    if (controlMode !== 'bone') {
    const zoomSpeed = 0.5;
    cameraDistance = Math.max(5, Math.min(100, cameraDistance + e.deltaY * 0.01 * zoomSpeed));
    updateCameraPosition();
    e.preventDefault();
    }
    });

    container.addEventListener('contextmenu', function(e) {
    e.preventDefault();
    });
    }

    function setupKeyboardControls() {
    document.addEventListener('keydown', function(e) {
    keys[e.key.toLowerCase()] = true;

    switch(e.key.toLowerCase()) {
    case 'r':
    if (controlMode === 'bone') {
    resetSelectedBone();
    } else {
    resetView();
    }
    break;
    case 'f':
    toggleWireframe();
    break;
    case 'h':
    document.getElementById('help-overlay').style.display =
    document.getElementById('help-overlay').style.display === 'block' ? 'none' : 'block';
    break;
    case 'c':
    if (controlMode !== 'bone') toggleControlMode();
    break;
    case 'b':
    toggleBoneMode();
    break;
    case '1':
    selectModel('both');
    break;
    case '2':
    selectModel('model1');
    break;
    case '3':
    selectModel('model2');
    break;
    }
    });

    document.addEventListener('keyup', function(e) {
    keys[e.key.toLowerCase()] = false;
    });
    }

    function handleKeyboardInput() {
    if (controlMode === 'bone') {
    handleBoneEditing();
    return;
    }

    const moveSpeed = 0.1;
    const rotationSpeed = 0.03;

    if (controlMode === 'model') {
    const targetModels = getTargetModels();

    if (keys['w']) {
    targetModels.forEach(model => {
    if (model) model.position.z -= moveSpeed;
    });
    }
    if (keys['s']) {
    targetModels.forEach(model => {
    if (model) model.position.z += moveSpeed;
    });
    }
    if (keys['a']) {
    targetModels.forEach(model => {
    if (model) model.position.x -= moveSpeed;
    });
    }
    if (keys['d']) {
    targetModels.forEach(model => {
    if (model) model.position.x += moveSpeed;
    });
    }

    if (keys['q']) {
    targetModels.forEach(model => {
    if (model) model.position.y += moveSpeed;
    });
    }
    if (keys['e']) {
    targetModels.forEach(model => {
    if (model) model.position.y -= moveSpeed;
    });
    }

    if (keys['arrowup']) {
    targetModels.forEach(model => {
    if (model) model.rotation.x -= rotationSpeed;
    });
    }
    if (keys['arrowdown']) {
    targetModels.forEach(model => {
    if (model) model.rotation.x += rotationSpeed;
    });
    }
    if (keys['arrowleft']) {
    targetModels.forEach(model => {
    if (model) model.rotation.y += rotationSpeed;
    });
    }
    if (keys['arrowright']) {
    targetModels.forEach(model => {
    if (model) model.rotation.y -= rotationSpeed;
    });
    }
    } else if (controlMode === 'camera') {
    if (keys['w']) {
    const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(camera.quaternion);
    cameraTarget.add(forward.multiplyScalar(moveSpeed));
    updateCameraPosition();
    }
    if (keys['s']) {
    const backward = new THREE.Vector3(0, 0, 1).applyQuaternion(camera.quaternion);
    cameraTarget.add(backward.multiplyScalar(moveSpeed));
    updateCameraPosition();
    }
    if (keys['a']) {
    const left = new THREE.Vector3(-1, 0, 0).applyQuaternion(camera.quaternion);
    cameraTarget.add(left.multiplyScalar(moveSpeed));
    updateCameraPosition();
    }
    if (keys['d']) {
    const right = new THREE.Vector3(1, 0, 0).applyQuaternion(camera.quaternion);
    cameraTarget.add(right.multiplyScalar(moveSpeed));
    updateCameraPosition();
    }

    if (keys['q']) {
    cameraTarget.y += moveSpeed;
    updateCameraPosition();
    }
    if (keys['e']) {
    cameraTarget.y -= moveSpeed;
    updateCameraPosition();
    }

    if (keys['arrowup']) {
    cameraPhi = Math.max(0.1, cameraPhi - rotationSpeed);
    updateCameraPosition();
    }
    if (keys['arrowdown']) {
    cameraPhi = Math.min(Math.PI - 0.1, cameraPhi + rotationSpeed);
    updateCameraPosition();
    }
    if (keys['arrowleft']) {
    cameraTheta += rotationSpeed;
    updateCameraPosition();
    }
    if (keys['arrowright']) {
    cameraTheta -= rotationSpeed;
    updateCameraPosition();
    }
    }
    }

    // State management functions
    function generateState() {
    const params = new URLSearchParams();

    if (pmxPath1) params.set('pmx', pmxPath1);
    if (pmxPath2) params.set('pmx2', pmxPath2);
    if (vpdPath1) params.set('vpd', vpdPath1);
    if (vpdPath2) params.set('vpd2', vpdPath2);

    // Camera state: targetX,targetY,targetZ,distance,phi,theta
    const camState = `${cameraTarget.x.toFixed(2)},${cameraTarget.y.toFixed(2)},${cameraTarget.z.toFixed(2)},${cameraDistance.toFixed(2)},${cameraPhi.toFixed(4)},${cameraTheta.toFixed(4)}`;
    params.set('cam', camState);

    // Model 1 state: posX,posY,posZ,rotX,rotY,rotZ
    if (model1) {
    const m1State = `${model1.position.x.toFixed(2)},${model1.position.y.toFixed(2)},${model1.position.z.toFixed(2)},${model1.rotation.x.toFixed(4)},${model1.rotation.y.toFixed(4)},${model1.rotation.z.toFixed(4)}`;
    params.set('m1', m1State);
    }

    // Model 2 state
    if (model2) {
    const m2State = `${model2.position.x.toFixed(2)},${model2.position.y.toFixed(2)},${model2.position.z.toFixed(2)},${model2.rotation.x.toFixed(4)},${model2.rotation.y.toFixed(4)},${model2.rotation.z.toFixed(4)}`;
    params.set('m2', m2State);
    }

    // Add stored poses to URL if they exist
    const storedPose1 = localStorage.getItem('stor1');
    const storedPose2 = localStorage.getItem('stor2');

    if (storedPose1) {
    params.set('stor1', encodeURIComponent(storedPose1));
    }

    if (storedPose2) {
    params.set('stor2', encodeURIComponent(storedPose2));
    }

    const stateURL = `${window.location.origin}${window.location.pathname}?${params.toString()}`;

    // Show URL in textbox
    const textbox = document.getElementById('state-textbox');
    textbox.value = stateURL;
    textbox.style.display = 'block';
    textbox.select();

    return stateURL;
    }

    // Also update the loadStateFromURL function to handle stored poses from URL
    function loadStateFromURL() {
    if (stateCam) {
    const parts = stateCam.split(',');
    if (parts.length === 6) {
    cameraTarget.set(parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2]));
    cameraDistance = parseFloat(parts[3]);
    cameraPhi = parseFloat(parts[4]);
    cameraTheta = parseFloat(parts[5]);
    updateCameraPosition();
    }
    }

    // Load stored poses from URL parameters
    const urlStor1 = urlParams.get('stor1');
    const urlStor2 = urlParams.get('stor2');

    if (urlStor1) {
    try {
    const decodedStor1 = decodeURIComponent(urlStor1);
    // Validate it's valid JSON before storing
    JSON.parse(decodedStor1);
    localStorage.setItem('stor1', decodedStor1);
    } catch (error) {
    console.error('Invalid stor1 data in URL:', error);
    }
    }

    if (urlStor2) {
    try {
    const decodedStor2 = decodeURIComponent(urlStor2);
    // Validate it's valid JSON before storing
    JSON.parse(decodedStor2);
    localStorage.setItem('stor2', decodedStor2);
    } catch (error) {
    console.error('Invalid stor2 data in URL:', error);
    }
    }
    }

    // Update the applyModelStates function to also apply stored poses
    function applyModelStates() {
    if (stateM1 && model1) {
    const parts = stateM1.split(',');
    if (parts.length === 6) {
    model1.position.set(parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2]));
    model1.rotation.set(parseFloat(parts[3]), parseFloat(parts[4]), parseFloat(parts[5]));
    }
    }

    if (stateM2 && model2) {
    const parts = stateM2.split(',');
    if (parts.length === 6) {
    model2.position.set(parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2]));
    model2.rotation.set(parseFloat(parts[3]), parseFloat(parts[4]), parseFloat(parts[5]));
    }
    }

    // Apply stored poses from localStorage (which might have been loaded from URL)
    setTimeout(() => {
    applyStoredPosesOnLoad();
    }, 100);
    }

    function applyModelStates() {
    if (stateM1 && model1) {
    const parts = stateM1.split(',');
    if (parts.length === 6) {
    model1.position.set(parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2]));
    model1.rotation.set(parseFloat(parts[3]), parseFloat(parts[4]), parseFloat(parts[5]));
    }
    }

    if (stateM2 && model2) {
    const parts = stateM2.split(',');
    if (parts.length === 6) {
    model2.position.set(parseFloat(parts[0]), parseFloat(parts[1]), parseFloat(parts[2]));
    model2.rotation.set(parseFloat(parts[3]), parseFloat(parts[4]), parseFloat(parts[5]));
    }
    }
    }

    // Rest of the essential functions
    function getTargetModels() {
    switch(currentModel) {
    case 'model1': return [model1];
    case 'model2': return [model2];
    default: return [model1, model2];
    }
    }

    function resolveAssetPath(assetPath, defaultBase) {
    if (!assetPath) return null;
    assetPath = assetPath.trim();
    if (/^(https?:|file:|\/)/i.test(assetPath)) return assetPath;
    if (/^(\.\/|\.\.\/)/.test(assetPath)) return assetPath;
    if (/^vpd\//i.test(assetPath)) return "./" + assetPath;
    return (defaultBase || "./pmx/pronama/") + assetPath;
    }

    function loadModels() {
    if (pmxPath1) {
    const full1 = resolveAssetPath(pmxPath1, "./pmx/pronama/");
    loader.load(full1, object => {
    model1 = object;
    model1.position.set(-5, 0, 0);
    scene.add(model1);
    if (vpdPath1) loadAndApplyVPD(model1, resolveAssetPath(vpdPath1, "./"));
    applyModelStates();
    updateModelInfo();
    checkLoadingComplete();
    }, null, err => console.error("Error loading PMX1", err));
    }

    if (pmxPath2) {
    const full2 = resolveAssetPath(pmxPath2, "./pmx/pronama/");
    loader.load(full2, object => {
    model2 = object;
    model2.position.set(5, 0, 0);
    scene.add(model2);
    if (vpdPath2) loadAndApplyVPD(model2, resolveAssetPath(vpdPath2, "./"));
    applyModelStates();
    updateModelInfo();
    checkLoadingComplete();
    }, null, err => console.error("Error loading PMX2", err));
    }

    if (!pmxPath1 && !pmxPath2) {
    document.getElementById("loading").textContent = "No PMX models specified.";
    }

    setTimeout(loadStateFromURL, 100);
    }

    function loadAndApplyVPD(model, vpdPath) {
    console.log("Loading VPD:", vpdPath);
    loader.loadVPD(vpdPath, false, function(vpd) {
    console.log("VPD loaded successfully:", vpd);
    requestAnimationFrame(() => {
    try {
    helper.pose(model, vpd);
    console.log("Pose applied successfully to model");
    } catch (error) {
    console.error("Error applying pose:", error);
    }
    });
    }, null, function(error) {
    console.error("Error loading VPD:", error);
    });
    }

    function updateModelInfo() {
    let info = "";
    if (model1) info += "Model 1 loaded" + (vpdPath1 ? " + Pose 1" : "") + "<br>";
    if (model2) info += "Model 2 loaded" + (vpdPath2 ? " + Pose 2" : "") + "<br>";
    document.getElementById("modelInfo").innerHTML = info;
    }

    function checkLoadingComplete() {
    const total = [pmxPath1, pmxPath2].filter(Boolean).length;
    const loaded = [model1, model2].filter(Boolean).length;
    if (loaded === total) {
    document.getElementById("loading").style.display = "none";

    // Always try to apply stored poses, regardless of URL parameters
    setTimeout(() => {
    applyStoredPosesOnLoad();
    }, 1500);
    }
    }

    function applyStoredPosesOnLoad() {
    const storedPose1 = localStorage.getItem('stor1');
    if (storedPose1 && model1) {
    try {
    const pose = JSON.parse(storedPose1);
    applyPoseToModelFromStorage(model1, pose);
    } catch (error) {
    console.error('Error applying stored pose 1:', error);
    }
    }

    const storedPose2 = localStorage.getItem('stor2');
    if (storedPose2 && model2) {
    try {
    const pose = JSON.parse(storedPose2);
    applyPoseToModelFromStorage(model2, pose);
    } catch (error) {
    console.error('Error applying stored pose 2:', error);
    }
    }
    }

    function applyPoseToModelFromStorage(model, pose) {
    if (!model || !model.skeleton || !pose) return;

    model.updateMatrixWorld(true);

    Object.keys(pose).forEach(japaneseName => {
    const bone = model.skeleton.bones.find(b => b.name === japaneseName);
    if (!bone || !pose[japaneseName]) return;

    const relQuat = new THREE.Quaternion(
    pose[japaneseName].quaternion.x,
    pose[japaneseName].quaternion.y,
    pose[japaneseName].quaternion.z,
    pose[japaneseName].quaternion.w
    );

    bone.quaternion.copy(relQuat).normalize();
    });

    model.skeleton.update();
    model.updateMatrixWorld(true);
    }

    function resetView() {
    cameraTarget.set(0, 0, 0);
    cameraDistance = 25;
    cameraPhi = Math.PI / 6;
    cameraTheta = 0;
    updateCameraPosition();

    if (model1) {
    model1.position.set(-5, 0, 0);
    model1.rotation.set(0, 0, 0);
    }
    if (model2) {
    model2.position.set(5, 0, 0);
    model2.rotation.set(0, 0, 0);
    }
    }

    function toggleWireframe() {
    [model1, model2].forEach(model => {
    if (!model) return;
    model.traverse(c => {
    if (c.isMesh) {
    if (Array.isArray(c.material)) {
    c.material.forEach(m => m.wireframe = !m.wireframe);
    } else {
    c.material.wireframe = !c.material.wireframe;
    }
    }
    });
    });
    }

    function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
    }

    function toggleControlMode() {
    if (controlMode === 'bone') return;
    controlMode = controlMode === 'model' ? 'camera' : 'model';
    updateModeUI();
    }

    function toggleBoneMode() {
    if (controlMode === 'bone') {
    controlMode = 'model';
    document.getElementById('bone-controls').style.display = 'none';
    document.getElementById('selected-bone-info').style.display = 'none';
    document.getElementById('bone-transform-controls').style.display = 'none';

    boneHelpers.forEach(helper => scene.remove(helper));
    boneHelpers = [];
    selectedBone = null;
    selectedModel = null;
    } else {
    controlMode = 'bone';
    document.getElementById('bone-controls').style.display = 'flex';
    document.getElementById('bone-transform-controls').style.display = 'flex';

    const targetModels = getTargetModels();
    selectedModel = targetModels[0] || null;

    setupBoneHelpers();
    setBoneTransformMode('move');
    }
    updateModeUI();
    }

    function updateModeUI() {
    const modeBtn = document.getElementById('toggleMode');
    const boneBtn = document.getElementById('toggleBones');
    const modeIndicator = document.getElementById('mode-indicator');

    if (controlMode === 'camera') {
    modeBtn.textContent = 'Model Mode';
    modeBtn.classList.add('active');
    boneBtn.textContent = 'Bone Mode';
    boneBtn.classList.remove('active');
    modeIndicator.textContent = 'Mode: Camera Control';
    } else if (controlMode === 'model') {
    modeBtn.textContent = 'Camera Mode';
    modeBtn.classList.remove('active');
    boneBtn.textContent = 'Bone Mode';
    boneBtn.classList.remove('active');
    modeIndicator.textContent = 'Mode: Model Control';
    } else if (controlMode === 'bone') {
    modeBtn.textContent = 'Camera Mode';
    modeBtn.classList.remove('active');
    boneBtn.textContent = 'Exit Bones';
    boneBtn.classList.add('active');
    modeIndicator.textContent = 'Mode: Bone Editing';
    }
    }

    function selectModel(model) {
    currentModel = model;
    document.querySelectorAll('.model-btn').forEach(btn => {
    btn.classList.remove('active');
    if (btn.getAttribute('data-model') === model) {
    btn.classList.add('active');
    }
    });
    updateSelectedModel();
    }

    function updateSelectedModel() {
    if (controlMode === 'bone') {
    const targetModels = getTargetModels();
    selectedModel = targetModels[0] || null;
    if (selectedModel && selectedBone) {
    const boneName = selectedBone.name;
    const newBone = findBoneInModel(selectedModel, boneName);
    selectedBone = newBone;
    updateBoneSelection();
    }
    }
    }

    function setupBoneHelpers() {
    boneHelpers.forEach(helper => scene.remove(helper));
    boneHelpers = [];

    const targetModels = getTargetModels();
    targetModels.forEach(model => {
    if (model && model.skeleton) {
    model.skeleton.bones.forEach(bone => {
    const geometry = new THREE.SphereGeometry(0.15, 12, 12);
    const material = new THREE.MeshBasicMaterial({
    color: 0x00ff00,
    transparent: true,
    opacity: 0.8
    });
    const helper = new THREE.Mesh(geometry, material);

    const worldPos = new THREE.Vector3();
    bone.getWorldPosition(worldPos);
    helper.position.copy(worldPos);

    helper.userData = { bone: bone, model: model };
    helper.className = 'bone-helper';
    boneHelpers.push(helper);
    scene.add(helper);
    });
    }
    });
    }

    function findBoneInModel(model, boneName) {
    if (!model || !model.skeleton) return null;
    return model.skeleton.bones.find(bone =>
    bone.name.toLowerCase().includes(boneName.toLowerCase())
    );
    }

    function selectBoneByType(boneType) {
    if (!selectedModel) return;

    const possibleNames = boneMappings[boneType];
    if (!possibleNames) return;

    for (const name of possibleNames) {
    const bone = findBoneInModel(selectedModel, name);
    if (bone) {
    selectedBone = bone;
    updateBoneSelection();

    document.querySelectorAll('.bone-btn').forEach(btn => {
    btn.classList.remove('active');
    });
    document.querySelector(`.bone-btn[data-bone="${boneType}"]`).classList.add('active');

    return;
    }
    }
    }

    function updateBoneSelection() {
    updateSelectedBoneInfo();

    if (selectedBone && selectedModel) {
    boneHelpers.forEach(helper => {
    if (helper.userData.bone === selectedBone) {
    helper.material.color.set(0xff0000);
    helper.scale.set(1.5, 1.5, 1.5);
    } else {
    helper.material.color.set(0x00ff00);
    helper.scale.set(1, 1, 1);
    }
    });
    } else {
    boneHelpers.forEach(helper => {
    helper.material.color.set(0x00ff00);
    helper.scale.set(1, 1, 1);
    });
    }
    }

    function updateSelectedBoneInfo() {
    const infoElement = document.getElementById('selected-bone-info');

    if (selectedBone && selectedModel) {
    infoElement.textContent = `Selected: ${selectedBone.name} (${boneTransformMode} mode)`;
    infoElement.style.display = 'block';
    } else {
    infoElement.textContent = 'No bone selected';
    infoElement.style.display = 'block';
    }
    }

    function setBoneTransformMode(mode) {
    boneTransformMode = mode;
    document.getElementById('move-bone').classList.toggle('active', mode === 'move');
    document.getElementById('rotate-bone').classList.toggle('active', mode === 'rotate');
    updateSelectedBoneInfo();
    }

    function handleBoneDragStart(event, isTouch = false) {
    if (controlMode !== 'bone' || !selectedBone) return false;

    const mouse = new THREE.Vector2();
    if (isTouch) {
    mouse.x = (event.touches[0].clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.touches[0].clientY / window.innerHeight) * 2 + 1;
    } else {
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
    }

    const raycaster = new THREE.Raycaster();
    raycaster.setFromCamera(mouse, camera);

    const intersects = raycaster.intersectObjects(boneHelpers);
    const hitSelectedBone = intersects.some(intersect => intersect.object.userData.bone === selectedBone);

    if (hitSelectedBone) {
    isDraggingBone = true;

    if (boneTransformMode === 'move') {
    const cameraDirection = new THREE.Vector3();
    camera.getWorldDirection(cameraDirection);
    dragPlane.setFromNormalAndCoplanarPoint(cameraDirection, selectedBone.getWorldPosition(new THREE.Vector3()));

    raycaster.ray.intersectPlane(dragPlane, dragStartPoint);
    dragStartBonePosition.copy(selectedBone.position);
    } else {
    dragStartBoneRotation.copy(selectedBone.rotation);
    dragStartPoint.set(mouse.x, mouse.y, 0);
    }

    event.preventDefault();
    return true;
    }

    return false;
    }

    function handleBoneDrag(event, isTouch = false) {
    if (!isDraggingBone || !selectedBone) return;

    const mouse = new THREE.Vector2();
    if (isTouch) {
    mouse.x = (event.touches[0].clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.touches[0].clientY / window.innerHeight) * 2 + 1;
    } else {
    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
    }

    if (boneTransformMode === 'move') {
    const raycaster = new THREE.Raycaster();
    raycaster.setFromCamera(mouse, camera);

    const dragPoint = new THREE.Vector3();
    if (raycaster.ray.intersectPlane(dragPlane, dragPoint)) {
    const delta = new THREE.Vector3().subVectors(dragPoint, dragStartPoint);
    selectedBone.position.copy(dragStartBonePosition).add(delta);
    }
    } else {
    const deltaX = mouse.x - dragStartPoint.x;
    const deltaY = mouse.y - dragStartPoint.y;

    const rotationSpeed = 2;
    selectedBone.rotation.x = dragStartBoneRotation.x + deltaY * rotationSpeed;
    selectedBone.rotation.y = dragStartBoneRotation.y + deltaX * rotationSpeed;
    }

    if (selectedModel) {
    selectedModel.skeleton.pose();
    selectedModel.updateMatrixWorld(true);
    }

    updateBoneHelpers();

    event.preventDefault();
    }

    function handleBoneDragEnd() {
    isDraggingBone = false;
    }

    function handleBoneSelection(e) {
    const touch = e.touches[0];
    const mouse = new THREE.Vector2();
    mouse.x = (touch.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1;

    const raycaster = new THREE.Raycaster();
    raycaster.setFromCamera(mouse, camera);

    const intersects = raycaster.intersectObjects(boneHelpers);
    if (intersects.length > 0) {
    selectedBone = intersects[0].object.userData.bone;
    selectedModel = intersects[0].object.userData.model;
    updateBoneSelection();
    updateBoneButtons();
    }
    }

    function handleBoneEditing() {
    if (!selectedBone || controlMode !== 'bone') return;

    const moveSpeed = 0.05;
    const rotationSpeed = 0.03;

    if (keys['w']) selectedBone.position.z -= moveSpeed;
    if (keys['s']) selectedBone.position.z += moveSpeed;
    if (keys['a']) selectedBone.position.x -= moveSpeed;
    if (keys['d']) selectedBone.position.x += moveSpeed;
    if (keys['q']) selectedBone.position.y += moveSpeed;
    if (keys['e']) selectedBone.position.y -= moveSpeed;

    if (keys['arrowup']) selectedBone.rotation.x -= rotationSpeed;
    if (keys['arrowdown']) selectedBone.rotation.x += rotationSpeed;
    if (keys['arrowleft']) selectedBone.rotation.y += rotationSpeed;
    if (keys['arrowright']) selectedBone.rotation.y -= rotationSpeed;

    if (selectedModel) {
    selectedModel.skeleton.pose();
    selectedModel.updateMatrixWorld(true);
    }

    updateBoneHelpers();
    }

    function updateBoneHelpers() {
    boneHelpers.forEach(helper => {
    const bone = helper.userData.bone;
    const worldPos = new THREE.Vector3();
    bone.getWorldPosition(worldPos);
    helper.position.copy(worldPos);
    });
    }

    function resetSelectedBone() {
    if (selectedBone) {
    selectedBone.position.set(0, 0, 0);
    selectedBone.rotation.set(0, 0, 0);
    selectedBone.scale.set(1, 1, 1);

    if (selectedModel) {
    selectedModel.skeleton.pose();
    selectedModel.updateMatrixWorld(true);
    }

    updateBoneHelpers();
    }
    }

    function updateBoneButtons() {
    if (!selectedBone) return;

    const boneName = selectedBone.name.toLowerCase();
    document.querySelectorAll('.bone-btn').forEach(btn => {
    btn.classList.remove('active');
    const boneType = btn.getAttribute('data-bone');
    const possibleNames = boneMappings[boneType];

    if (possibleNames && possibleNames.some(name => boneName.includes(name.toLowerCase()))) {
    btn.classList.add('active');
    }
    });
    }

    function reloadStoredPose(storageKey, model) {
    if (!model) return;
    const storedPose = localStorage.getItem(storageKey);
    if (storedPose) {
    try {
    const pose = JSON.parse(storedPose);
    applyPoseToModelFromStorage(model, pose);
    showPoseReloadFeedback(storageKey);
    } catch (error) {
    console.error(`Error applying ${storageKey}:`, error);
    }
    }
    }

    function reloadBothStoredPoses() {
    if (model1) reloadStoredPose('stor1', model1);
    if (model2) reloadStoredPose('stor2', model2);
    showPoseReloadFeedback('both');
    }

    function showPoseReloadFeedback(poseType) {
    const feedback = document.createElement('div');
    feedback.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,0.8);color:#8a8aff;padding:10px 20px;border-radius:10px;z-index:100;font-size:16px;font-weight:bold';
    feedback.textContent = poseType === 'both' ? 'Both Poses Reloaded!' : `Pose ${poseType.slice(-1)} Reloaded!`;
    document.body.appendChild(feedback);
    setTimeout(() => document.body.removeChild(feedback), 1000);
    }

    function animate() {
    requestAnimationFrame(animate);
    handleKeyboardInput();
    renderer.render(scene, camera);
    }

    // Initialize the application - MOVE THIS TO THE END
    function init() {
    helper = new THREE.MMDAnimationHelper();
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x1a1a2e);

    camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 1000);
    updateCameraPosition();

    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(window.devicePixelRatio);
    document.getElementById("container").appendChild(renderer.domElement);

    scene.add(new THREE.AmbientLight(0xffffff, 0.6));
    const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
    dirLight.position.set(10, 20, 15);
    scene.add(dirLight);

    scene.add(new THREE.GridHelper(20, 20));
    scene.add(new THREE.AxesHelper(5));

    loader = new THREE.MMDLoader();

    setupTouchControls();
    setupMouseControls();
    setupKeyboardControls();

    loadModels();

    window.addEventListener("resize", onWindowResize);
    document.getElementById("resetView").addEventListener("click", resetView);
    document.getElementById("toggleWireframe").addEventListener("click", toggleWireframe);
    document.getElementById("toggleMode").addEventListener("click", toggleControlMode);
    document.getElementById("toggleBones").addEventListener("click", toggleBoneMode);
    document.getElementById("reset-bones").addEventListener("click", resetSelectedBone);

    document.getElementById("move-bone").addEventListener("click", () => setBoneTransformMode('move'));
    document.getElementById("rotate-bone").addEventListener("click", () => setBoneTransformMode('rotate'));
    document.getElementById("reset-bone-transform").addEventListener("click", resetSelectedBone);

    document.querySelectorAll('.model-btn').forEach(btn => {
    btn.addEventListener('click', function() {
    document.querySelectorAll('.model-btn').forEach(b => b.classList.remove('active'));
    this.classList.add('active');
    currentModel = this.getAttribute('data-model');
    updateSelectedModel();
    });
    });

    document.querySelectorAll('.bone-btn').forEach(btn => {
    btn.addEventListener('click', function() {
    if (controlMode !== 'bone') return;
    const boneType = this.getAttribute('data-bone');
    selectBoneByType(boneType);
    });
    });

    document.getElementById('reload-stor1').addEventListener('click', () => reloadStoredPose('stor1', model1));
    document.getElementById('reload-stor2').addEventListener('click', () => reloadStoredPose('stor2', model2));
    document.getElementById('reload-both').addEventListener('click', reloadBothStoredPoses);

    document.getElementById('generate-state').addEventListener('click', generateState);

    document.getElementById('help-btn').addEventListener('click', () => {
    document.getElementById('help-overlay').style.display = 'block';
    });

    document.getElementById('close-help').addEventListener('click', () => {
    document.getElementById('help-overlay').style.display = 'none';
    });

    document.getElementById('close-gesture-help').addEventListener('click', () => {
    document.getElementById('mobile-gesture-info').style.display = 'none';
    });

    if (/Mobi|Android|iPhone|iPad/.test(navigator.userAgent)) {
    setTimeout(() => {
    document.getElementById('mobile-gesture-info').style.display = 'block';
    }, 1000);
    }

    animate();
    }

    // Finally, call init after all functions are defined
    init();
    </script>
    </body>
    </html>

    pose2vpd.html
    --
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>MMD 3D Pose Editor</title>
    <style>
    body {
    margin: 0;
    overflow: hidden;
    background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
    font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
    touch-action: none;
    -webkit-tap-highlight-color: transparent;
    }
    canvas {
    display: block;
    cursor: grab;
    touch-action: none;
    }
    canvas.orb-dragging {
    cursor: grabbing !important;
    }
    #ui {
    position: absolute;
    top: 10px;
    left: 10px;
    background: rgba(25, 25, 35, 0.85);
    color: white;
    padding: 20px;
    border-radius: 12px;
    width: 280px;
    z-index: 100;
    backdrop-filter: blur(10px);
    border: 1px solid rgba(255, 255, 255, 0.1);
    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
    }
    #ui h2 {
    margin-top: 0;
    color: #6c63ff;
    text-align: center;
    font-weight: 600;
    font-size: 1.4em;
    margin-bottom: 20px;
    }
    button {
    width: 100%;
    padding: 12px;
    margin-bottom: 12px;
    border-radius: 6px;
    border: none;
    background: linear-gradient(135deg, #6c63ff 0%, #4a44b5 100%);
    color: white;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.3s;
    min-height: 44px;
    }
    button:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(108, 99, 255, 0.4);
    }
    #reset-btn {
    background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
    }
    #reset-btn:hover {
    box-shadow: 0 4px 12px rgba(255, 107, 107, 0.4);
    }
    #instructions {
    font-size: 12px;
    color: #8888aa;
    margin-top: 15px;
    line-height: 1.5;
    text-align: center;
    }
    #loading {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    color: white;
    font-size: 18px;
    z-index: 100;
    background: rgba(25, 25, 35, 0.9);
    padding: 20px 30px;
    border-radius: 10px;
    text-align: center;
    }
    .footer {
    display: flex;
    justify-content: space-between;
    margin-top: 15px;
    font-size: 11px;
    color: #666;
    }
    .camera-controls {
    margin-top: 15px;
    padding: 15px;
    background: rgba(42, 42, 58, 0.5);
    border-radius: 8px;
    }
    .control-hint {
    display: flex;
    justify-content: space-between;
    margin-bottom: 8px;
    font-size: 11px;
    }
    .control-key {
    background: rgba(108, 99, 255, 0.3);
    padding: 2px 6px;
    border-radius: 3px;
    }
    #axis-controls {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    display: none;
    z-index: 200;
    }
    .axis-btn {
    position: absolute;
    width: 44px;
    height: 44px;
    border-radius: 50%;
    border: none;
    color: white;
    font-size: 18px;
    font-weight: bold;
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    transition: all 0.2s;
    touch-action: manipulation;
    }
    .axis-btn:hover {
    transform: scale(1.2);
    }
    #x-plus { background: #ff4444; top: -60px; left: 0; }
    #x-minus { background: #ff4444; top: 60px; left: 0; }
    #y-plus { background: #44ff44; top: 0; left: -60px; }
    #y-minus { background: #44ff44; top: 0; left: 60px; }
    #z-plus { background: #4444ff; top: -40px; left: -40px; }
    #z-minus { background: #4444ff; top: 40px; left: 40px; }
    .mode-toggle {
    display: flex;
    margin-bottom: 15px;
    border-radius: 6px;
    overflow: hidden;
    background: rgba(42, 42, 58, 0.5);
    }
    .mode-btn {
    flex: 1;
    padding: 10px;
    background: transparent;
    border: none;
    color: #8888aa;
    font-size: 12px;
    font-weight: 600;
    cursor: pointer;
    transition: all 0.3s;
    min-height: auto;
    }
    .mode-btn.active {
    background: #6c63ff;
    color: white;
    }
    #control-mode-indicator {
    text-align: center;
    margin-top: 10px;
    font-size: 12px;
    color: #a0a0c0;
    }
    .storage-buttons {
    display: flex;
    gap: 10px;
    margin-bottom: 12px;
    }
    .storage-buttons button {
    flex: 1;
    margin-bottom: 0;
    }
    #stor1-btn {
    background: linear-gradient(135deg, #ffa726 0%, #f57c00 100%);
    }
    #stor2-btn {
    background: linear-gradient(135deg, #66bb6a 0%, #388e3c 100%);
    }
    #load-buttons {
    display: flex;
    gap: 10px;
    margin-bottom: 12px;
    }
    #load-buttons button {
    flex: 1;
    margin-bottom: 0;
    }
    #load1-btn {
    background: linear-gradient(135deg, #ffb74d 0%, #ff9800 100%);
    }
    #load2-btn {
    background: linear-gradient(135deg, #81c784 0%, #4caf50 100%);
    }
    #export-btn {
    background: linear-gradient(135deg, #ab47bc 0%, #8e24aa 100%);
    }
    #export-btn:hover {
    box-shadow: 0 4px 12px rgba(171, 71, 188, 0.4);
    }
    .notification {
    position: fixed;
    top: 20px;
    right: 20px;
    background: rgba(25, 25, 35, 0.95);
    color: white;
    padding: 15px 20px;
    border-radius: 8px;
    border-left: 4px solid #6c63ff;
    z-index: 1000;
    transform: translateX(400px);
    transition: transform 0.3s ease;
    backdrop-filter: blur(10px);
    max-width: 300px;
    }
    .notification.show {
    transform: translateX(0);
    }
    .mobile-controls {
    position: absolute;
    bottom: 20px;
    right: 20px;
    display: flex;
    flex-direction: column;
    gap: 10px;
    z-index: 50;
    }
    .mobile-control-btn {
    width: 50px;
    height: 50px;
    border-radius: 50%;
    border: none;
    background: rgba(108, 99, 255, 0.8);
    color: white;
    font-size: 20px;
    display: flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    backdrop-filter: blur(10px);
    border: 1px solid rgba(255, 255, 255, 0.2);
    touch-action: manipulation;
    }
    #mobile-ui-toggle {
    position: absolute;
    top: 20px;
    right: 20px;
    width: 50px;
    height: 50px;
    border-radius: 50%;
    border: none;
    background: rgba(25, 25, 35, 0.8);
    color: white;
    font-size: 20px;
    z-index: 101;
    backdrop-filter: blur(10px);
    display: none;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    }
    @media (max-width: 768px) {
    #ui {
    width: calc(100% - 40px);
    max-height: 70vh;
    overflow-y: auto;
    }
    #mobile-ui-toggle {
    display: flex;
    }
    .mobile-controls {
    display: flex;
    }
    }
    </style>

    <script src="./libs/three.js"></script>
    <script src="./libs/mmdparser.min.js"></script>
    <script src="./libs/ammo.min.js"></script>
    <script src="./libs/TGALoader.js"></script>
    <script src="./libs/CCDIKSolver.js"></script>
    <script src="./libs/MMDLoader.js"></script>
    <script src="./libs/MMDAnimationHelper.js"></script>
    </head>
    <body>
    <div id="loading">
    <div>Loading MMD Model...</div>
    <div style="margin-top:10px;font-size:14px;" id="loading-progress">0%</div>
    </div>

    <button id="mobile-ui-toggle">☰</button>

    <div id="ui" style="display: none;">
    <h2>MMD 3D Pose Editor</h2>

    <div class="mode-toggle">
    <button class="mode-btn active" id="rotation-mode">Rotation</button>
    <button class="mode-btn" id="position-mode">Position</button>
    </div>

    <button id="reset-btn">Reset All Poses</button>

    <div class="storage-buttons">
    <button id="stor1-btn">Store Pose 1</button>
    <button id="stor2-btn">Store Pose 2</button>
    </div>

    <div id="load-buttons">
    <button id="load1-btn">Load Pose 1</button>
    <button id="load2-btn">Load Pose 2</button>
    </div>

    <button id="export-btn">Export as VPD</button>

    <div id="control-mode-indicator">Current Mode: Rotation</div>

    <div class="camera-controls">
    <div style="text-align: center; margin-bottom: 10px; color: #a0a0c0; font-weight: 500;">Camera Controls</div>
    <div class="control-hint">
    <span>Orbit Camera:</span>
    <span class="control-key">Right Drag</span>
    </div>
    <div class="control-hint">
    <span>Zoom:</span>
    <span class="control-key">Scroll Wheel</span>
    </div>
    <div class="control-hint">
    <span>Pan:</span>
    <span class="control-key">Middle Drag</span>
    </div>
    <div class="control-hint">
    <span>Reset Camera:</span>
    <span class="control-key">Double Click</span>
    </div>
    </div>

    <div id="instructions">
    <p>Click and drag the colored orbs to pose the model</p>
    <p>Red: Rotation • Green: Position • Blue: Special</p>
    <p><strong>Left Click:</strong> Drag Orbs • <strong>Right Click:</strong> Orbit Camera</p>
    <p><strong>Click Orb:</strong> Show Axis Controls</p>
    </div>

    <div class="footer">
    <span>Magic Pose Editor</span>
    <span>v1.2</span>
    </div>
    </div>

    <div id="axis-controls">
    <button class="axis-btn" id="x-plus">X+</button>
    <button class="axis-btn" id="x-minus">X-</button>
    <button class="axis-btn" id="y-plus">Y+</button>
    <button class="axis-btn" id="y-minus">Y-</button>
    <button class="axis-btn" id="z-plus">Z+</button>
    <button class="axis-btn" id="z-minus">Z-</button>
    </div>

    <div class="mobile-controls">
    <button class="mobile-control-btn" id="mobile-orbit">🔄</button>
    <button class="mobile-control-btn" id="mobile-zoom-in">+</button>
    <button class="mobile-control-btn" id="mobile-zoom-out">-</button>
    <button class="mobile-control-btn" id="mobile-reset-cam">📷</button>
    </div>

    <div id="notification" class="notification"></div>

    <script>
    // Mobile detection and variables
    let isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
    let touchStartTime = 0;
    let lastTouchX = 0;
    let lastTouchY = 0;
    let isTouchOrbiting = false;
    let isTouchDraggingOrb = false;
    let touchOrbStartTime = 0;
    let touchOrb = null;

    let scene, camera, renderer, helper, model;
    let boneOrbs = [];
    let raycaster, mouse;
    let isDraggingOrb = false;
    let currentOrb = null;
    let orbGroup;
    let controls;
    let axisControls;
    let selectedOrb = null;
    let controlMode = 'rotation'; // 'rotation' or 'position'

    const ESSENTIAL_BONES = {
    '頭': 'head',
    '首': 'neck',
    '左腕': 'left_arm',
    '右腕': 'right_arm',
    '左ひじ': 'left_elbow',
    '右ひじ': 'right_elbow',
    '左手首': 'left_wrist',
    '右手首': 'right_wrist',
    '左足': 'left_leg_upper',
    '右足': 'right_leg_upper',
    '左ひざ': 'left_knee',
    '右ひざ': 'right_knee',
    '左足首': 'left_ankle',
    '右足首': 'right_ankle',
    '左目': 'left_eye',
    '右目': 'right_eye'
    };

    function radToDeg(radians) {
    return radians * (180 / Math.PI);
    }

    function degToRad(degrees) {
    return degrees * (Math.PI / 180);
    }

    function showNotification(message, duration = 3000) {
    const notification = document.getElementById('notification');
    notification.textContent = message;
    notification.classList.add('show');

    setTimeout(() => {
    notification.classList.remove('show');
    }, duration);
    }

    class SimpleOrbitControls {
    constructor(camera, domElement) {
    this.camera = camera;
    this.domElement = domElement;

    this.enabled = true;
    this.target = new THREE.Vector3(0, 10, 0);
    this.minDistance = 5;
    this.maxDistance = 100;
    this.minPolarAngle = 0;
    this.maxPolarAngle = Math.PI;

    this.spherical = new THREE.Spherical().setFromVector3(
    new THREE.Vector3().subVectors(camera.position, this.target)
    );
    this.sphericalDelta = new THREE.Spherical();
    this.scale = 1;
    this.panOffset = new THREE.Vector3();
    this.zoomChanged = false;

    this.mouseButtons = { LEFT: 0, MIDDLE: 1, RIGHT: 2 };
    this.state = -1;

    this.domElement.addEventListener('contextmenu', this.onContextMenu.bind(this));
    this.domElement.addEventListener('mousedown', this.onMouseDown.bind(this));
    this.domElement.addEventListener('wheel', this.onMouseWheel.bind(this));
    this.domElement.addEventListener('dblclick', this.onDoubleClick.bind(this));

    // Touch events for mobile
    this.domElement.addEventListener('touchstart', this.onTouchStart.bind(this));
    this.domElement.addEventListener('touchmove', this.onTouchMove.bind(this));
    this.domElement.addEventListener('touchend', this.onTouchEnd.bind(this));

    this.update();
    }

    onContextMenu(event) {
    event.preventDefault();
    }

    onMouseDown(event) {
    if (!this.enabled) return;

    if (event.button !== this.mouseButtons.RIGHT && event.button !== this.mouseButtons.MIDDLE) {
    return;
    }

    event.preventDefault();

    switch (event.button) {
    case this.mouseButtons.RIGHT:
    this.state = 0;
    break;
    case this.mouseButtons.MIDDLE:
    this.state = 2;
    break;
    }

    if (this.state !== -1) {
    document.addEventListener('mousemove', this.onMouseMove.bind(this));
    document.addEventListener('mouseup', this.onMouseUp.bind(this));
    }
    }

    onMouseMove(event) {
    if (!this.enabled) return;

    event.preventDefault();

    const movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
    const movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0;

    if (this.state === 0) {
    this.sphericalDelta.theta -= movementX * 0.01;
    this.sphericalDelta.phi -= movementY * 0.01;
    } else if (this.state === 2) {
    this.panOffset.x -= movementX * 0.01;
    this.panOffset.y += movementY * 0.01;
    }
    }

    onMouseUp() {
    this.state = -1;
    document.removeEventListener('mousemove', this.onMouseMove);
    document.removeEventListener('mouseup', this.onMouseUp);
    }

    onMouseWheel(event) {
    if (!this.enabled) return;

    event.preventDefault();

    if (event.deltaY < 0) {
    this.scale /= 1.1;
    } else if (event.deltaY > 0) {
    this.scale *= 1.1;
    }

    this.zoomChanged = true;
    }

    onDoubleClick() {
    this.target.set(0, 10, 0);
    this.spherical.set(1, Math.PI / 2, 0);
    this.spherical.radius = 45;
    this.sphericalDelta.set(0, 0, 0);
    this.panOffset.set(0, 0, 0);
    this.scale = 1;
    this.update();
    }

    // Touch event handlers for mobile
    onTouchStart(event) {
    if (!this.enabled || event.touches.length !== 1) return;

    event.preventDefault();

    const touch = event.touches[0];
    lastTouchX = touch.clientX;
    lastTouchY = touch.clientY;
    touchStartTime = Date.now();

    // Check if we're touching an orb (will be handled separately)
    const mouse = new THREE.Vector2(
    (touch.clientX / window.innerWidth) * 2 - 1,
    -(touch.clientY / window.innerHeight) * 2 + 1
    );

    raycaster.setFromCamera(mouse, camera);
    const intersects = raycaster.intersectObjects(boneOrbs);

    if (intersects.length > 0) {
    // Orb touch - let the orb handler deal with it
    return;
    }

    // Otherwise, start orbit camera
    isTouchOrbiting = true;
    this.state = 0;
    }

    onTouchMove(event) {
    if (!this.enabled || !isTouchOrbiting || event.touches.length !== 1) return;

    event.preventDefault();

    const touch = event.touches[0];
    const deltaX = touch.clientX - lastTouchX;
    const deltaY = touch.clientY - lastTouchY;

    if (this.state === 0) {
    this.sphericalDelta.theta -= deltaX * 0.01;
    this.sphericalDelta.phi -= deltaY * 0.01;
    }

    lastTouchX = touch.clientX;
    lastTouchY = touch.clientY;
    }

    onTouchEnd(event) {
    if (isTouchOrbiting) {
    isTouchOrbiting = false;
    this.state = -1;
    }
    }

    update() {
    const offset = new THREE.Vector3();

    this.spherical.theta += this.sphericalDelta.theta;
    this.spherical.phi += this.sphericalDelta.phi;

    this.spherical.phi = Math.max(this.minPolarAngle, Math.min(this.maxPolarAngle, this.spherical.phi));

    if (this.zoomChanged) {
    this.spherical.radius *= this.scale;
    this.spherical.radius = Math.max(this.minDistance, Math.min(this.maxDistance, this.spherical.radius));
    this.scale = 1;
    this.zoomChanged = false;
    }

    this.target.add(this.panOffset);
    this.panOffset.set(0, 0, 0);

    this.target.y = Math.max(0, Math.min(30, this.target.y));

    offset.setFromSpherical(this.spherical);
    this.camera.position.copy(this.target).add(offset);
    this.camera.lookAt(this.target);

    this.sphericalDelta.set(0, 0, 0);
    }
    }

    init();

    function init() {
    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x1a1a2e);

    camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 1, 2000);
    camera.position.set(0, 15, 45);

    const ambient = new THREE.AmbientLight(0xffffff, 0.4);
    scene.add(ambient);

    const light = new THREE.DirectionalLight(0xffffff, 0.8);
    light.position.set(0, 20, 10);
    scene.add(light);

    const rimLight = new THREE.DirectionalLight(0x4a44b5, 0.3);
    rimLight.position.set(0, 10, -20);
    scene.add(rimLight);

    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setPixelRatio(window.devicePixelRatio);
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    const groundGeometry = new THREE.PlaneGeometry(50, 50);
    const groundMaterial = new THREE.MeshStandardMaterial({
    color: 0x2a2a3a,
    roughness: 0.8,
    metalness: 0.2
    });
    const ground = new THREE.Mesh(groundGeometry, groundMaterial);
    ground.rotation.x = -Math.PI / 2;
    ground.position.y = -0.1;
    scene.add(ground);

    raycaster = new THREE.Raycaster();
    mouse = new THREE.Vector2();

    orbGroup = new THREE.Group();
    scene.add(orbGroup);

    controls = new SimpleOrbitControls(camera, renderer.domElement);

    helper = new THREE.MMDAnimationHelper();

    const loader = new THREE.MMDLoader();

    function getPMXPath() {
    const params = new URLSearchParams(window.location.search);
    const pmxParam = params.get('pmx');
    if (pmxParam) {
    // prepend folder path and decode URI safely
    return "pmx/pronama/" + decodeURIComponent(pmxParam);
    }
    return "pmx/pronama/YusakuFujiki/yusaku.pmx"; // default fallback
    }

    const modelPath = getPMXPath();

    loader.load(
    modelPath,
    function (mesh) {
    model = mesh;
    model.position.y = 0;
    scene.add(model);

    document.getElementById('loading').style.display = 'none';
    document.getElementById('ui').style.display = 'block';

    // Show mobile UI toggle on mobile
    if (isMobile) {
    document.getElementById('mobile-ui-toggle').style.display = 'flex';
    // Hide UI by default on mobile to maximize canvas space
    document.getElementById('ui').style.display = 'none';
    }

    model.updateMatrixWorld(true);
    model.skeleton.bones.forEach(bone => {
    bone.userData.bindQuaternion = bone.quaternion.clone();
    });
    createAllOrbControls();

    setupUI();

    setupOrbInteraction();

    setupAxisControls();

    setupMobileControls();

    animate();
    },
    function (xhr) {
    if (xhr.lengthComputable) {
    const percentComplete = (xhr.loaded / xhr.total * 100).toFixed(0);
    document.getElementById('loading-progress').textContent = `${percentComplete}%`;
    }
    },
    function (error) {
    console.error("Error loading model:", error);
    document.getElementById('loading').innerHTML = `
    <div style="color:#ff6b6b">Error loading model</div>
    <div style="margin-top:10px;font-size:14px;">Check browser console for details</div>
    <button onclick="location.reload()" style="margin-top:15px;padding:8px 15px;background:#6c63ff;border:none;border-radius:4px;color:white;cursor:pointer;">Retry</button>
    `;
    }
    );

    window.addEventListener("resize", onWindowResize);
    }

    function setupMobileControls() {
    const orbitBtn = document.getElementById('mobile-orbit');
    const zoomInBtn = document.getElementById('mobile-zoom-in');
    const zoomOutBtn = document.getElementById('mobile-zoom-out');
    const resetCamBtn = document.getElementById('mobile-reset-cam');
    const uiToggle = document.getElementById('mobile-ui-toggle');

    orbitBtn.addEventListener('click', function() {
    showNotification('Drag with one finger to orbit camera');
    });

    zoomInBtn.addEventListener('click', function() {
    controls.scale /= 1.2;
    controls.zoomChanged = true;
    });

    zoomOutBtn.addEventListener('click', function() {
    controls.scale *= 1.2;
    controls.zoomChanged = true;
    });

    resetCamBtn.addEventListener('click', function() {
    controls.onDoubleClick();
    showNotification('Camera reset');
    });

    uiToggle.addEventListener('click', function() {
    const ui = document.getElementById('ui');
    if (ui.style.display === 'none') {
    ui.style.display = 'block';
    } else {
    ui.style.display = 'none';
    }
    });
    }

    function createAllOrbControls() {
    orbGroup.children = [];
    boneOrbs = [];

    if (!model || !model.skeleton) return;

    const bones = model.skeleton.bones;

    Object.keys(ESSENTIAL_BONES).forEach(japaneseName => {
    const bone = bones.find(b => b.name === japaneseName);
    if (bone) {
    createOrbForBone(bone, ESSENTIAL_BONES[japaneseName]);
    }
    });

    console.log(`Created ${boneOrbs.length} orbs for essential bones`);
    }

    function createOrbForBone(bone, englishName) {
    const boneWorldPos = new THREE.Vector3();
    bone.getWorldPosition(boneWorldPos);

    const orbGeometry = new THREE.SphereGeometry(0.8, 16, 16);

    let orbColor;
    if (englishName.includes('eye')) {
    orbColor = 0x6b6bff;
    } else if (englishName.includes('wrist') || englishName.includes('ankle')) {
    orbColor = 0x6bff6b;
    } else {
    orbColor = 0xff6b6b;
    }

    const orbMaterial = new THREE.MeshBasicMaterial({
    color: orbColor,
    transparent: true,
    opacity: englishName.includes('eye') ? 0.5 : 0.9
    });

    const orb = new THREE.Mesh(orbGeometry, orbMaterial);
    orb.position.copy(boneWorldPos);
    orb.userData = {
    type: 'bone_control',
    bone: bone,
    boneName: englishName,
    originalPosition: boneWorldPos.clone()
    };

    orbGroup.add(orb);
    boneOrbs.push(orb);
    }

    function setupOrbInteraction() {
    renderer.domElement.addEventListener('mousedown', onMouseDown);
    renderer.domElement.addEventListener('mousemove', onMouseMove);
    renderer.domElement.addEventListener('mouseup', onMouseUp);
    renderer.domElement.addEventListener('click', onOrbClick);

    // Touch events for mobile orb interaction
    renderer.domElement.addEventListener('touchstart', onTouchStartOrb, { passive: false });
    renderer.domElement.addEventListener('touchmove', onTouchMoveOrb, { passive: false });
    renderer.domElement.addEventListener('touchend', onTouchEndOrb);
    }

    function onTouchStartOrb(event) {
    if (event.touches.length !== 1) return;

    event.preventDefault();

    const touch = event.touches[0];
    mouse.x = (touch.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1;

    raycaster.setFromCamera(mouse, camera);

    const intersects = raycaster.intersectObjects(boneOrbs);

    if (intersects.length > 0) {
    isTouchDraggingOrb = true;
    touchOrb = intersects[0].object;
    touchOrbStartTime = Date.now();
    lastTouchX = touch.clientX;
    lastTouchY = touch.clientY;

    // Show axis controls on long press (for mobile)
    setTimeout(() => {
    if (isTouchDraggingOrb && touchOrb && Date.now() - touchOrbStartTime > 500) {
    showAxisControls(touchOrb);
    }
    }, 500);
    }
    }

    function onTouchMoveOrb(event) {
    if (!isTouchDraggingOrb || !touchOrb || event.touches.length !== 1) return;

    event.preventDefault();

    const touch = event.touches[0];
    const deltaX = touch.clientX - lastTouchX;
    const deltaY = touch.clientY - lastTouchY;

    const bone = touchOrb.userData.bone;
    const boneName = touchOrb.userData.boneName;

    if (controlMode === 'rotation') {
    // Rotation mode
    if (boneName.includes('eye')) {
    bone.rotation.y += deltaX * 0.01;
    bone.rotation.x += deltaY * 0.01;
    } else {
    bone.rotation.y += deltaX * 0.02;
    bone.rotation.x += deltaY * 0.02;
    }

    bone.rotation.x = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.x));
    bone.rotation.y = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.y));
    bone.rotation.z = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.z));
    } else {
    // Position mode
    if (boneName.includes('wrist') || boneName.includes('ankle') || boneName.includes('eye')) {
    bone.position.x += deltaX * 0.05;
    bone.position.y -= deltaY * 0.05;
    }
    }

    bone.updateMatrixWorld(true);
    updateOrbPositions();

    lastTouchX = touch.clientX;
    lastTouchY = touch.clientY;
    }

    function onTouchEndOrb(event) {
    if (isTouchDraggingOrb) {
    // Check if it was a tap (not a drag) to show axis controls
    if (touchOrb && Date.now() - touchOrbStartTime < 300) {
    showAxisControls(touchOrb);
    }

    isTouchDraggingOrb = false;
    touchOrb = null;
    }
    }

    function onOrbClick(event) {
    if (event.button !== 0) return;

    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

    raycaster.setFromCamera(mouse, camera);

    const intersects = raycaster.intersectObjects(boneOrbs);

    if (intersects.length > 0) {
    const clickedOrb = intersects[0].object;
    showAxisControls(clickedOrb);
    event.stopPropagation();
    } else {
    hideAxisControls();
    }
    }

    function showAxisControls(orb) {
    selectedOrb = orb;
    const axisControls = document.getElementById('axis-controls');
    axisControls.style.display = 'block';

    const screenPosition = new THREE.Vector3();
    orb.getWorldPosition(screenPosition);
    screenPosition.project(camera);

    const x = (screenPosition.x * 0.5 + 0.5) * window.innerWidth;
    const y = -(screenPosition.y * 0.5 - 0.5) * window.innerHeight;

    axisControls.style.left = x + 'px';
    axisControls.style.top = y + 'px';
    }

    function hideAxisControls() {
    selectedOrb = null;
    document.getElementById('axis-controls').style.display = 'none';
    }

    function setupAxisControls() {
    document.getElementById('x-plus').addEventListener('click', () => moveBoneOnAxis('x', 1));
    document.getElementById('x-minus').addEventListener('click', () => moveBoneOnAxis('x', -1));
    document.getElementById('y-plus').addEventListener('click', () => moveBoneOnAxis('y', 1));
    document.getElementById('y-minus').addEventListener('click', () => moveBoneOnAxis('y', -1));
    document.getElementById('z-plus').addEventListener('click', () => moveBoneOnAxis('z', 1));
    document.getElementById('z-minus').addEventListener('click', () => moveBoneOnAxis('z', -1));

    // Add touch events for axis controls on mobile
    const axisButtons = document.querySelectorAll('.axis-btn');
    axisButtons.forEach(btn => {
    btn.addEventListener('touchstart', function(e) {
    e.preventDefault();
    this.click();
    }, { passive: false });
    });
    }

    function moveBoneOnAxis(axis, direction) {
    if (!selectedOrb) return;

    const bone = selectedOrb.userData.bone;
    const boneName = selectedOrb.userData.boneName;

    const rotationStep = isMobile ? 0.05 : 0.1;
    const positionStep = isMobile ? 0.25 : 0.5;

    if (controlMode === 'rotation') {
    // Rotation mode
    if (boneName.includes('eye')) {
    bone.rotation[axis] += direction * rotationStep * 0.1;
    } else {
    bone.rotation[axis] += direction * rotationStep;
    }

    bone.rotation.x = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.x));
    bone.rotation.y = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.y));
    bone.rotation.z = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.z));
    } else {
    // Position mode
    if (boneName.includes('wrist') || boneName.includes('ankle') || boneName.includes('eye')) {
    bone.position[axis] += direction * positionStep;
    }
    }

    bone.updateMatrixWorld(true);
    updateOrbPositions();
    }

    function onMouseDown(event) {
    if (event.button !== 0) return;

    mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
    mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;

    raycaster.setFromCamera(mouse, camera);

    const intersects = raycaster.intersectObjects(boneOrbs);

    if (intersects.length > 0) {
    isDraggingOrb = true;
    currentOrb = intersects[0].object;
    renderer.domElement.classList.add('orb-dragging');
    document.body.style.cursor = 'grabbing';

    event.preventDefault();
    event.stopPropagation();
    }
    }

    function onMouseMove(event) {
    if (!isDraggingOrb || !currentOrb) return;

    const bone = currentOrb.userData.bone;
    const boneName = currentOrb.userData.boneName;

    const deltaX = event.movementX * 0.01;
    const deltaY = event.movementY * 0.01;

    if (controlMode === 'rotation') {
    // Rotation mode
    if (boneName.includes('eye')) {
    bone.rotation.y += deltaX * 0.1;
    bone.rotation.x += deltaY * 0.1;
    } else {
    bone.rotation.y += deltaX;
    bone.rotation.x += deltaY;
    }

    bone.rotation.x = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.x));
    bone.rotation.y = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.y));
    bone.rotation.z = Math.max(-Math.PI, Math.min(Math.PI, bone.rotation.z));
    } else {
    // Position mode
    if (boneName.includes('wrist') || boneName.includes('ankle') || boneName.includes('eye')) {
    bone.position.x += deltaX * 2;
    bone.position.y -= deltaY * 2;
    }
    }

    bone.updateMatrixWorld(true);
    updateOrbPositions();
    }

    function onMouseUp(event) {
    if (event.button === 0 && isDraggingOrb) {
    isDraggingOrb = false;
    currentOrb = null;
    renderer.domElement.classList.remove('orb-dragging');
    document.body.style.cursor = 'default';
    }
    }

    function updateOrbPositions() {
    boneOrbs.forEach(orb => {
    const bone = orb.userData.bone;
    const boneWorldPos = new THREE.Vector3();
    bone.getWorldPosition(boneWorldPos);
    orb.position.copy(boneWorldPos);
    });
    }

    function getCurrentPose() {
    if (!model || !model.skeleton) return {};
    const pose = {};
    const bones = model.skeleton.bones;

    model.updateMatrixWorld(true);

    Object.keys(ESSENTIAL_BONES).forEach(japaneseName => {
    const bone = bones.find(b => b.name === japaneseName);
    if (!bone) return;

    const bindQuat = bone.userData?.bindQuaternion
    ? bone.userData.bindQuaternion.clone()
    : new THREE.Quaternion(); // fallback identity

    // relative rotation: bind^-1 * current
    const invBind = bindQuat.clone();
    if (typeof invBind.invert === "function") invBind.invert();
    else invBind.inverse();

    const relativeQuat = invBind.multiply(bone.quaternion).normalize();

    pose[japaneseName] = {
    quaternion: {
    x: relativeQuat.x,
    y: relativeQuat.y,
    z: relativeQuat.z,
    w: relativeQuat.w
    }
    };
    });

    return pose;
    }

    // Apply a portable pose (rotation only)
    function applyPose(pose) {
    if (!model || !model.skeleton) return;
    const bones = model.skeleton.bones;

    model.updateMatrixWorld(true);

    Object.keys(pose).forEach(japaneseName => {
    const bone = bones.find(b => b.name === japaneseName);
    if (!bone || !pose[japaneseName]) return;

    const relQuat = new THREE.Quaternion(
    pose[japaneseName].quaternion.x,
    pose[japaneseName].quaternion.y,
    pose[japaneseName].quaternion.z,
    pose[japaneseName].quaternion.w
    );

    const bindQuat = bone.userData?.bindQuaternion
    ? bone.userData.bindQuaternion.clone()
    : new THREE.Quaternion();

    bone.quaternion.copy(bindQuat.clone().multiply(relQuat).normalize());
    bone.updateMatrixWorld(true);
    });

    model.skeleton.update();
    updateOrbPositions();
    }

    function exportToVPD() {
    if (!model || !model.skeleton) return;

    const bones = model.skeleton.bones;
    let vpdContent = "Vocaloid Pose Data file\r\n\r\n0;\r\n\r\n";
    const exportBones = [];

    // Update all matrices first
    model.updateMatrixWorld(true);

    Object.keys(ESSENTIAL_BONES).forEach(japaneseName => {
    const bone = bones.find(b => b.name === japaneseName);
    if (!bone) return;

    // Get current bone quaternion
    const currentQuat = bone.quaternion.clone();

    // Get bind pose quaternion (store once on load)
    const bindQuat = bone.userData?.bindQuaternion
    ? bone.userData.bindQuaternion.clone()
    : new THREE.Quaternion(); // default identity if missing

    // Compute relative rotation (Three.js < r150 uses inverse())
    const invBind = bindQuat.clone();
    if (typeof invBind.invert === "function") {
    invBind.invert();
    } else {
    invBind.inverse();
    }

    const relativeQuat = invBind.multiply(currentQuat);
    relativeQuat.normalize();

    const rotationThreshold = 0.0001;
    const hasRotation =
    Math.abs(relativeQuat.x) > rotationThreshold ||
    Math.abs(relativeQuat.y) > rotationThreshold ||
    Math.abs(relativeQuat.z) > rotationThreshold ||
    Math.abs(relativeQuat.w - 1) > rotationThreshold;

    if (hasRotation) {
    exportBones.push({
    name: japaneseName,
    quaternion: relativeQuat,
    index: exportBones.length
    });
    }
    });

    // Update bone count
    vpdContent = vpdContent.replace("0;\r\n\r\n", `${exportBones.length};\r\n\r\n`);

    exportBones.forEach(boneData => {
    vpdContent += `Bone${boneData.index}{${boneData.name}\r\n`;
    vpdContent += " 0.000000,0.000000,0.000000;\r\n";

    // Convert to MMD quaternion (flip handedness only)
    const q = boneData.quaternion;
    const mmdQuat = new THREE.Quaternion(-q.x, q.z, q.y, -q.w);
    mmdQuat.normalize();

    vpdContent += ` ${mmdQuat.x.toFixed(6)},${mmdQuat.y.toFixed(6)},${mmdQuat.z.toFixed(6)},${mmdQuat.w.toFixed(6)};\r\n`;
    vpdContent += "}\r\n";
    });

    // Save file
    const BOM = '\uFEFF';
    const blob = new Blob([BOM + vpdContent], { type: 'text/plain; charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'pose.vpd';
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);

    showNotification(`VPD exported! ${exportBones.length} modified bones.`);

    console.log("VPD Content:\n", vpdContent);
    console.log("Exported relative bone rotations:");
    exportBones.forEach(boneData => {
    console.log(boneData.name, boneData.quaternion);
    });
    }

    function setupUI() {
    const resetBtn = document.getElementById('reset-btn');
    const exportBtn = document.getElementById('export-btn');
    const rotationModeBtn = document.getElementById('rotation-mode');
    const positionModeBtn = document.getElementById('position-mode');
    const modeIndicator = document.getElementById('control-mode-indicator');
    const stor1Btn = document.getElementById('stor1-btn');
    const stor2Btn = document.getElementById('stor2-btn');
    const load1Btn = document.getElementById('load1-btn');
    const load2Btn = document.getElementById('load2-btn');

    // Make buttons touch-friendly on mobile
    if (isMobile) {
    const buttons = document.querySelectorAll('button');
    buttons.forEach(btn => {
    btn.addEventListener('touchstart', function(e) {
    e.preventDefault();
    this.click();
    }, { passive: false });
    });
    }

    resetBtn.addEventListener('click', function() {
    if (model && model.skeleton) {
    const bones = model.skeleton.bones;
    bones.forEach(bone => {
    bone.rotation.set(0, 0, 0);
    bone.position.set(0, 0, 0);
    });
    model.skeleton.update();
    updateOrbPositions();
    hideAxisControls();
    showNotification('Pose reset to default');
    }
    });

    exportBtn.addEventListener('click', exportToVPD);

    rotationModeBtn.addEventListener('click', function() {
    controlMode = 'rotation';
    rotationModeBtn.classList.add('active');
    positionModeBtn.classList.remove('active');
    modeIndicator.textContent = 'Current Mode: Rotation';
    });

    positionModeBtn.addEventListener('click', function() {
    controlMode = 'position';
    positionModeBtn.classList.add('active');
    rotationModeBtn.classList.remove('active');
    modeIndicator.textContent = 'Current Mode: Position';
    });

    stor1Btn.addEventListener('click', function() {
    const pose = getCurrentPose();
    localStorage.setItem('stor1', JSON.stringify(pose));
    showNotification('Pose saved to Slot 1');
    });

    stor2Btn.addEventListener('click', function() {
    const pose = getCurrentPose();
    localStorage.setItem('stor2', JSON.stringify(pose));
    showNotification('Pose saved to Slot 2');
    });

    load1Btn.addEventListener('click', function() {
    const storedPose = localStorage.getItem('stor1');
    if (storedPose) {
    const pose = JSON.parse(storedPose);
    console.log('Loaded from Slot 1:', pose);
    applyPose(pose);
    showNotification('Pose loaded from Slot 1');
    } else {
    console.log('No pose found in Slot 1');
    showNotification('No pose found in Slot 1', 2000);
    }
    });

    load2Btn.addEventListener('click', function() {
    const storedPose = localStorage.getItem('stor2');
    if (storedPose) {
    const pose = JSON.parse(storedPose);
    console.log('Loaded from Slot 2:', pose);
    applyPose(pose);
    showNotification('Pose loaded from Slot 2');
    } else {
    console.log('No pose found in Slot 2');
    showNotification('No pose found in Slot 2', 2000);
    }
    });
    }

    function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
    }

    function animate() {
    requestAnimationFrame(animate);

    controls.update();

    if (!isDraggingOrb && !isTouchDraggingOrb) {
    updateOrbPositions();
    }

    renderer.render(scene, camera);
    }
    </script>
    </body>
    </html>