import sys
import json
from PyQt5.QtCore import QObject, pyqtSlot
from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog, QMessageBox
from PyQt5.QtWebEngineWidgets import QWebEngineView
from PyQt5.QtWebChannel import QWebChannel

STUDIO_HTML = """
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>NovaStudio</title>
    <style>
        body { margin: 0; padding: 0; overflow: hidden; font-family: Arial, sans-serif; background: #6699FF; user-select: none; }
        
        #start-screen {
            position: absolute; top: 0; left: 0; width: 100vw; height: 100vh;
            background: #e0e0e0; z-index: 100; display: flex; flex-direction: column; align-items: center; justify-content: center;
        }
        .menu-box {
            background: white; border: 2px solid #999; width: 600px; padding: 25px; box-shadow: 4px 4px 15px rgba(0,0,0,0.3);
        }
        .menu-title { font-size: 20px; font-weight: bold; color: #004276; margin-bottom: 15px; border-bottom: 2px solid #ccc; padding-bottom: 8px; }
        .template-grid { display: flex; justify-content: space-between; gap: 15px; margin-top: 15px; }
        .template-card {
            flex: 1; border: 2px solid #ccc; padding: 10px; text-align: center; cursor: pointer; background: #f9f9f9; border-radius: 4px;
        }
        .template-card:hover { border-color: #004276; background: #e6f2ff; }

        #editor-ui { display: none; width: 100vw; height: 100vh; position: relative; }
        #toolbar {
            position: absolute; top: 0; left: 0; width: 100%; height: 50px;
            background: #cccccc; border-bottom: 3px solid #999;
            display: flex; align-items: center; justify-content: space-between; z-index: 10; box-sizing: border-box; padding: 0 10px;
        }
        .toolbar-group { display: flex; align-items: center; }
        .btn {
            background: #004276; color: white; border: none; padding: 8px 15px;
            font-weight: bold; cursor: pointer; border-radius: 3px; margin: 0 5px; font-size: 13px;
        }
        .btn:hover { background: #002b4d; }
        .btn-green { background: #28a745; }
        .btn-green:hover { background: #218838; }
        .btn-red { background: #da291c; }
        .btn-red:hover { background: #b52216; }
        .btn-console { background: #333333; }
        .btn-console:hover { background: #555555; }

        #explorer-panel, #properties-panel {
            position: absolute; left: 0; top: 50px; width: 240px;
            height: calc(50vh - 25px); background: #f0f0f0; border-right: 1px solid #999; border-bottom: 1px solid #999;
            z-index: 20; padding: 8px; font-size: 12px; overflow-y: auto; box-sizing: border-box;
        }
        #properties-panel { top: calc(50vh + 25px); border-bottom: none; }
        .panel-header { font-weight: bold; color: #004276; margin-bottom: 8px; border-bottom: 1px solid #ccc; padding-bottom: 4px; }
        
        #explorer-list { list-style: none; padding: 0; margin: 0; }
        .explorer-item { padding: 4px 6px; cursor: pointer; border-radius: 2px; }
        .explorer-item:hover { background: #d0e4ff; }
        .explorer-item.selected { background: #004276; color: white; }

        .prop-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
        .prop-row input, .prop-row select { width: 120px; padding: 3px; font-size: 11px; box-sizing: border-box; }

        #canvas-container { width: 100vw; height: 100vh; position: absolute; top: 0; left: 0; z-index: 1; }
        
        #console-panel {
            position: absolute; right: 0; top: 50px; width: 320px; height: calc(100vh - 50px);
            background: rgba(0, 0, 0, 0.85); color: #00FF00; font-family: monospace;
            padding: 15px; box-sizing: border-box; overflow-y: auto; z-index: 50; display: none; font-size: 12px;
            border-left: 2px solid #555;
        }
        .console-header-bar { font-weight: bold; color: #fff; margin-bottom: 10px; border-bottom: 1px solid #555; padding-bottom: 5px; display: flex; justify-content: space-between; }
        .console-error { color: #ff5555; }
        .console-success { color: #55ff55; }

        #context-menu {
            position: absolute; display: none; background: white; border: 1px solid #777;
            box-shadow: 2px 2px 8px rgba(0,0,0,0.3); z-index: 9999; padding: 5px 0; font-size: 13px;
        }
        .menu-item { padding: 8px 15px; cursor: pointer; }
        .menu-item:hover { background: #004276; color: white; }

        #hint {
            position: absolute; bottom: 10px; right: 10px; background: rgba(0,0,0,0.7);
            color: white; padding: 10px; border-radius: 5px; font-size: 12px; pointer-events: none; z-index: 10;
        }
    </style>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
    <script src="qrc:///qtwebchannel/qwebchannel.js"></script>
</head>
<body>

    <div id="start-screen">
        <div class="menu-box">
            <div class="menu-title">NovaStudio — Выберите шаблон карты</div>
            <div class="template-grid">
                <div class="template-card" onclick="startProject('huge')">
                    <div style="background:#777; height:90px; display:flex; align-items:center; justify-content:center; color:white; font-weight:bold;">Огромная карта</div>
                    <b>Baseplate (Huge)</b>
                </div>
                <div class="template-card" onclick="startProject('parkour')">
                    <div style="background:#228833; height:90px; display:flex; align-items:center; justify-content:center; color:white; font-weight:bold;">Паркур</div>
                    <b>Obby / Паркур</b>
                </div>
                <div class="template-card" onclick="startProject('retro')">
                    <div style="background:#da291c; height:90px; display:flex; align-items:center; justify-content:center; color:white; font-weight:bold;">NOVA Retro</div>
                    <b>Ретро-карта</b>
                </div>
            </div>
        </div>
    </div>

    <div id="editor-ui">
        <div id="toolbar">
            <div class="toolbar-group">
                <button class="btn" onclick="addBlock('box')">➕ Блок</button>
                <button class="btn" onclick="addBlock('cylinder')">⚫ Цилиндр</button>
                <button class="btn" onclick="addBlock('sphere')">🌐 Сфера</button>
                <button id="test-btn" class="btn btn-green" onclick="togglePlayTest()">▶ Тест</button>
            </div>
            <div class="toolbar-group">
                <button class="btn btn-console" onclick="toggleConsole()">🛠 Консоль</button>
                <button class="btn btn-red" onclick="publishGame()">💾 Сохранить на ПК</button>
            </div>
        </div>

        <div id="explorer-panel">
            <div class="panel-header">Explorer</div>
            <ul id="explorer-list"></ul>
        </div>

        <div id="properties-panel">
            <div class="panel-header">Properties</div>
            <div id="props-content" style="color: #444;">Выберите объект</div>
        </div>

        <div id="canvas-container"></div>
        
        <div id="console-panel">
            <div class="console-header-bar">
                <span>Системная консоль</span>
                <span style="cursor:pointer;" onclick="toggleConsole()">[X]</span>
            </div>
            <div id="console-logs"></div>
        </div>

        <div id="hint">
            <b>Управление:</b><br>
            • ПКМ + движение — камера | ЛКМ — выбор/перетаскивание<br>
            • WASD — движение | Пробел — прыжок | Esc — выйти из теста
        </div>
    </div>

    <div id="context-menu">
        <div class="menu-item" onclick="openColorPicker()">🎨 Изменить цвет</div>
        <div class="menu-item" onclick="deleteSelectedBlock()">🗑 Удалить блок</div>
    </div>

    <input type="color" id="native-color-picker" style="display:none;" onchange="applyColor(this.value)">

    <script>
        let pyBridge = null;
        new QWebChannel(qt.webChannelTransport, function(channel) {
            pyBridge = channel.objects.pyBridge;
        });

        const logsContainer = document.getElementById('console-logs');
        function logToConsole(message, type = 'info') {
            const time = new Date().toLocaleTimeString();
            let colorClass = type === 'error' ? 'console-error' : (type === 'success' ? 'console-success' : '');
            logsContainer.innerHTML += `<div class="${colorClass}">[${time}] ${message}</div>`;
            logsContainer.scrollTop = logsContainer.scrollHeight;
        }

        window.onerror = function(msg, url, line) {
            logToConsole(`Ошибка: ${msg} (строка ${line})`, 'error');
        };

        function toggleConsole() {
            const panel = document.getElementById('console-panel');
            panel.style.display = (panel.style.display === 'block') ? 'none' : 'block';
        }

        let scene, camera, renderer;
        let blocks = [];
        let selectedBlock = null;
        let isDraggingBlock = false;
        let raycaster = new THREE.Raycaster();
        let mouse = new THREE.Vector2();

        let isPlaying = false;
        let playerGroup = null;
        let playerVelocityY = 0;
        let isGrounded = false;
        const gravity = -0.015;
        const jumpForce = 0.32;
        const playerSpeed = 0.12;

        let moveState = { forward: false, backward: false, left: false, right: false };
        let isCamRotating = false;
        let prevMouseX = 0, prevMouseY = 0;
        let euler = new THREE.Euler(0, 0, 0, 'YXZ');

        function startProject(type) {
            document.getElementById('start-screen').style.display = 'none';
            document.getElementById('editor-ui').style.display = 'block';
            logToConsole("Запуск проекта: " + type, 'success');
            init3D(type);
        }

        function init3D(mapType) {
            const container = document.getElementById('canvas-container');
            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x6699FF);

            camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
            camera.position.set(0, 5, 12);

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

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

            if (mapType === 'huge') {
                createCustomPart('box', 0, -0.2, 0, 20, 0.4, 20, 0x888888, "Baseplate");
            } else if (mapType === 'parkour') {
                createCustomPart('box', 0, -0.2, 0, 10, 0.4, 10, 0x555555, "Baseplate");
                createCustomPart('box', -3, 1, -4, 2, 0.4, 2, 0x228833, "StartPlatform");
                createCustomPart('cylinder', 0, 2, -8, 2, 0.4, 2, 0x228833, "JumpCylinder");
            } else if (mapType === 'retro') {
                createCustomPart('box', 0, -0.2, 0, 12, 0.4, 12, 0x888888, "Baseplate");
                createCustomPart('box', 0, 0.5, 0, 4, 1, 1.5, 0xda291c, "RetroLogoPart");
            }

            setupEvents();
            animate();
        }

        function createCustomPart(shape, x, y, z, sx, sy, sz, color, name) {
            let geom;
            if (shape === 'cylinder') {
                geom = new THREE.CylinderGeometry(sx/2, sx/2, sy, 32);
            } else if (shape === 'sphere') {
                geom = new THREE.SphereGeometry(sx/2, 32, 32);
            } else {
                geom = new THREE.BoxGeometry(sx, sy, sz);
            }

            const mat = new THREE.MeshStandardMaterial({ color: color, roughness: 0.2 });
            const mesh = new THREE.Mesh(geom, mat);
            mesh.position.set(x, y, z);
            mesh.userData = { 
                shape: shape, 
                name: (name || "Part") + "_" + (blocks.length + 1),
                size: { x: sx, y: sy, z: sz }
            };
            
            scene.add(mesh);
            blocks.push(mesh);
            updateExplorer();
            return mesh;
        }

        function addBlock(shape = 'box') {
            createCustomPart(shape, (Math.random() - 0.5) * 4, 1.5, (Math.random() - 0.5) * 4, 2, 2, 2, 0x004276, "Part");
        }

        function updateExplorer() {
            const list = document.getElementById('explorer-list');
            list.innerHTML = '';
            blocks.forEach((block) => {
                const li = document.createElement('li');
                li.className = 'explorer-item' + (selectedBlock === block ? ' selected' : '');
                li.innerText = block.userData.name;
                li.onclick = () => selectBlock(block);
                list.appendChild(li);
            });
        }

        function selectBlock(block) {
            if (selectedBlock) selectedBlock.material.emissive.set(0x000000);
            selectedBlock = block;
            if (selectedBlock) selectedBlock.material.emissive.set(0x333333);
            updateExplorer();
            updateProperties();
        }

        function updateProperties() {
            const props = document.getElementById('props-content');
            if (!selectedBlock) {
                props.innerHTML = "Выберите объект";
                return;
            }
            const p = selectedBlock.position;
            const s = selectedBlock.userData.size;
            const hexColor = "#" + selectedBlock.material.color.getHexString();
            
            props.innerHTML = `
                <div class="prop-row"><span>Имя:</span> <b>${selectedBlock.userData.name}</b></div>
                <div class="prop-row"><span>Форма:</span> 
                    <select onchange="changeShape(this.value)">
                        <option value="box" ${selectedBlock.userData.shape==='box'?'selected':''}>Блок (Box)</option>
                        <option value="cylinder" ${selectedBlock.userData.shape==='cylinder'?'selected':''}>Цилиндр</option>
                        <option value="sphere" ${selectedBlock.userData.shape==='sphere'?'selected':''}>Сфера</option>
                    </select>
                </div>
                <div class="prop-row"><span>Позиция X:</span> <input type="number" step="0.5" value="${p.x.toFixed(1)}" onchange="selectedBlock.position.x = parseFloat(this.value); updateProperties();"></div>
                <div class="prop-row"><span>Позиция Y:</span> <input type="number" step="0.5" value="${p.y.toFixed(1)}" onchange="selectedBlock.position.y = parseFloat(this.value); updateProperties();"></div>
                <div class="prop-row"><span>Позиция Z:</span> <input type="number" step="0.5" value="${p.z.toFixed(1)}" onchange="selectedBlock.position.z = parseFloat(this.value); updateProperties();"></div>
                <div class="prop-row"><span>Размер X:</span> <input type="number" step="0.5" min="0.2" value="${s.x}" onchange="resizePart('x', this.value)"></div>
                <div class="prop-row"><span>Размер Y:</span> <input type="number" step="0.5" min="0.2" value="${s.y}" onchange="resizePart('y', this.value)"></div>
                <div class="prop-row"><span>Размер Z:</span> <input type="number" step="0.5" min="0.2" value="${s.z}" onchange="resizePart('z', this.value)"></div>
                <div class="prop-row"><span>Цвет:</span> <input type="color" value="${hexColor}" onchange="applyColor(this.value)"></div>
            `;
        }

        function changeShape(newShape) {
            if (!selectedBlock) return;
            selectedBlock.userData.shape = newShape;
            rebuildGeometry();
        }

        function resizePart(axis, val) {
            if (!selectedBlock) return;
            val = Math.max(0.2, parseFloat(val));
            selectedBlock.userData.size[axis] = val;
            rebuildGeometry();
        }

        function rebuildGeometry() {
            if (!selectedBlock) return;
            const s = selectedBlock.userData.size;
            const shape = selectedBlock.userData.shape;
            
            if (shape === 'cylinder') {
                selectedBlock.geometry = new THREE.CylinderGeometry(s.x/2, s.x/2, s.y, 32);
            } else if (shape === 'sphere') {
                selectedBlock.geometry = new THREE.SphereGeometry(s.x/2, 32, 32);
            } else {
                selectedBlock.geometry = new THREE.BoxGeometry(s.x, s.y, s.z);
            }
            updateProperties();
        }

        function applyColor(hexVal) {
            if (selectedBlock) {
                selectedBlock.material.color.set(hexVal);
                selectedBlock.material.needsUpdate = true;
                updateProperties();
            }
        }

        function deleteSelectedBlock() {
            if (selectedBlock) {
                scene.remove(selectedBlock);
                blocks = blocks.filter(b => b !== selectedBlock);
                selectedBlock = null;
                document.getElementById('context-menu').style.display = 'none';
                updateExplorer();
                updateProperties();
            }
        }

        function createNovaGuest() {
            const group = new THREE.Group();
            const canvas = document.createElement('canvas');
            canvas.width = 128; canvas.height = 128;
            const ctx = canvas.getContext('2d');
            ctx.fillStyle = '#000000'; ctx.fillRect(0,0,128,128);
            ctx.fillStyle = '#ffffff'; ctx.font = 'bold 22px Arial'; ctx.textAlign = 'center';
            ctx.fillText("NOVA", 64, 70);
            const torsoTexture = new THREE.CanvasTexture(canvas);

            const torso = new THREE.Mesh(new THREE.BoxGeometry(0.8, 1.0, 0.4), new THREE.MeshStandardMaterial({ map: torsoTexture }));
            torso.position.y = 0.5; group.add(torso);
            
            const head = new THREE.Mesh(new THREE.BoxGeometry(0.6, 0.6, 0.6), new THREE.MeshStandardMaterial({ color: 0xe0e0e0 }));
            head.position.y = 1.3; group.add(head);

            const armMat = new THREE.MeshStandardMaterial({ color: 0x000000 });
            const leftArm = new THREE.Mesh(new THREE.BoxGeometry(0.3, 1.0, 0.4), armMat); leftArm.position.set(-0.6, 0.5, 0); group.add(leftArm);
            const rightArm = new THREE.Mesh(new THREE.BoxGeometry(0.3, 1.0, 0.4), armMat); rightArm.position.set(0.6, 0.5, 0); group.add(rightArm);

            const legMat = new THREE.MeshStandardMaterial({ color: 0x000000 });
            const leftLeg = new THREE.Mesh(new THREE.BoxGeometry(0.35, 1.0, 0.4), legMat); leftLeg.position.set(-0.2, -0.5, 0); group.add(leftLeg);
            const rightLeg = new THREE.Mesh(new THREE.BoxGeometry(0.35, 1.0, 0.4), legMat); rightLeg.position.set(0.2, -0.5, 0); group.add(rightLeg);

            group.position.set(0, 3, 4);
            scene.add(group);
            return group;
        }

        function togglePlayTest() {
            const btn = document.getElementById('test-btn');
            isPlaying = !isPlaying;
            document.getElementById('context-menu').style.display = 'none';

            if (isPlaying) {
                btn.innerText = "⏹ Стоп";
                btn.className = "btn btn-red";
                playerGroup = createNovaGuest();
                playerVelocityY = 0;
                isGrounded = false;
                camera.position.set(0, 4, 7);
                euler.set(0, 0, 0);
            } else {
                btn.innerText = "▶ Тест";
                btn.className = "btn btn-green";
                if (playerGroup) { scene.remove(playerGroup); playerGroup = null; }
                camera.position.set(0, 5, 12);
                euler.set(0,0,0);
                camera.quaternion.setFromEuler(euler);
            }
        }

        function setupEvents() {
            window.addEventListener('mousedown', (e) => {
                if (e.clientX < 240 && e.clientY > 50) return;
                if (e.clientY < 50) return;
                
                mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
                mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
                raycaster.setFromCamera(mouse, camera);
                const intersects = raycaster.intersectObjects(blocks);

                if (e.button === 2) {
                    e.preventDefault();
                    document.getElementById('context-menu').style.display = 'none';
                    if (!isPlaying && intersects.length > 0) {
                        selectBlock(intersects[0].object);
                        const menu = document.getElementById('context-menu');
                        menu.style.display = 'block';
                        menu.style.left = e.clientX + 'px'; menu.style.top = e.clientY + 'px';
                    }
                    isCamRotating = true; prevMouseX = e.clientX; prevMouseY = e.clientY;
                    return;
                }

                if (e.button === 0) {
                    document.getElementById('context-menu').style.display = 'none';
                    if (isPlaying) { isCamRotating = true; prevMouseX = e.clientX; prevMouseY = e.clientY; return; }
                    if (intersects.length > 0) { selectBlock(intersects[0].object); isDraggingBlock = true; } 
                    else { selectBlock(null); }
                }
            });

            window.addEventListener('contextmenu', e => e.preventDefault());

            window.addEventListener('mousemove', (e) => {
                if (isDraggingBlock && selectedBlock && !isPlaying) {
                    mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
                    mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
                    raycaster.setFromCamera(mouse, camera);
                    const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -selectedBlock.position.y);
                    let target = new THREE.Vector3();
                    raycaster.ray.intersectPlane(plane, target);
                    if (target) {
                        selectedBlock.position.x = Math.round(target.x * 2) / 2;
                        selectedBlock.position.z = Math.round(target.z * 2) / 2;
                        updateProperties();
                    }
                    return;
                }

                if (isCamRotating) {
                    const dx = e.clientX - prevMouseX;
                    const dy = e.clientY - prevMouseY;
                    euler.y -= dx * 0.003;
                    euler.x -= dy * 0.003;
                    euler.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, euler.x));
                    camera.quaternion.setFromEuler(euler);
                    prevMouseX = e.clientX; prevMouseY = e.clientY;
                }
            });

            window.addEventListener('mouseup', (e) => {
                isDraggingBlock = false;
                if (e.button === 0 || e.button === 2) isCamRotating = false;
            });

            window.addEventListener('keydown', (e) => {
                if (['w','W','ц','Ц'].includes(e.key)) moveState.forward = true;
                if (['s','S','ы','Ы'].includes(e.key)) moveState.backward = true;
                if (['a','A','f','F','ф','Ф'].includes(e.key)) moveState.left = true;
                if (['d','D','в','В'].includes(e.key)) moveState.right = true;
                
                if (isPlaying && e.code === 'Space' && isGrounded) {
                    playerVelocityY = jumpForce;
                    isGrounded = false;
                }
                if (isPlaying && e.key === 'Escape') togglePlayTest();
            });

            window.addEventListener('keyup', (e) => {
                if (['w','W','ц','Ц'].includes(e.key)) moveState.forward = false;
                if (['s','S','ы','Ы'].includes(e.key)) moveState.backward = false;
                if (['a','A','f','F','ф','Ф'].includes(e.key)) moveState.left = false;
                if (['d','D','в','В'].includes(e.key)) moveState.right = false;
            });
        }

        function openColorPicker() {
            document.getElementById('context-menu').style.display = 'none';
            document.getElementById('native-color-picker').click();
        }

        function publishGame() {
            let sceneData = blocks.map(b => ({
                name: b.userData.name,
                shape: b.userData.shape,
                size: b.userData.size,
                x: b.position.x,
                y: b.position.y,
                z: b.position.z,
                color: b.material.color.getHex()
            }));

            if (pyBridge) {
                pyBridge.save_project(JSON.stringify(sceneData));
            } else {
                alert("Ошибка связи с Python!");
            }
        }

        function animate() {
            requestAnimationFrame(animate);

            if (isPlaying && playerGroup) {
                let camDir = new THREE.Vector3();
                camera.getWorldDirection(camDir);
                let flatCamDir = new THREE.Vector3(camDir.x, 0, camDir.z).normalize();
                let sideCamDir = new THREE.Vector3(-flatCamDir.z, 0, flatCamDir.x).normalize();

                let moveDir = new THREE.Vector3(0,0,0);
                if (moveState.forward) moveDir.add(flatCamDir);
                if (moveState.backward) moveDir.add(flatCamDir.clone().negate());
                if (moveState.left) moveDir.add(sideCamDir);
                if (moveState.right) moveDir.add(sideCamDir.clone().negate());

                if (moveDir.length() > 0) {
                    moveDir.normalize();
                    playerGroup.position.addScaledVector(moveDir, playerSpeed);
                    playerGroup.rotation.y = Math.atan2(moveDir.x, moveDir.z);
                }

                playerVelocityY += gravity;
                playerGroup.position.y += playerVelocityY;

                isGrounded = false;
                const playerBox = new THREE.Box3().setFromObject(playerGroup);
                
                for (let block of blocks) {
                    const blockBox = new THREE.Box3().setFromObject(block);
                    if (playerBox.intersectsBox(blockBox)) {
                        if (playerVelocityY < 0) {
                            playerGroup.position.y = blockBox.max.y + 0.5;
                            playerVelocityY = 0;
                            isGrounded = true;
                            break;
                        }
                    }
                }

                camera.position.x = playerGroup.position.x - Math.sin(euler.y) * 4;
                camera.position.z = playerGroup.position.z - Math.cos(euler.y) * 4;
                camera.position.y = playerGroup.position.y + 1.8 + Math.sin(-euler.x) * 2;
            } else if (!isPlaying) {
                const speed = 0.25;
                let dir = new THREE.Vector3();
                camera.getWorldDirection(dir);
                let sideDir = new THREE.Vector3(-dir.z, 0, dir.x).normalize();
                let flatDir = new THREE.Vector3(dir.x, 0, dir.z).normalize();

                if (moveState.forward) camera.position.addScaledVector(flatDir, speed);
                if (moveState.backward) camera.position.addScaledVector(flatDir, -speed);
                if (moveState.left) camera.position.addScaledVector(sideDir, speed);
                if (moveState.right) camera.position.addScaledVector(sideDir, -speed);
            }

            renderer.render(scene, camera);
        }

        window.addEventListener('resize', () => {
            if(camera && renderer) {
                camera.aspect = window.innerWidth / window.innerHeight;
                camera.updatePolyfillMatrix ? camera.updatePolyfillMatrix() : camera.updateMatrix();
                camera.updateProjectionMatrix();
                renderer.setSize(window.innerWidth, window.innerHeight);
            }
        });
    </script>
</body>
</html>
"""

class PyBridge(QObject):
    def __init__(self, parent_window):
        super().__init__()
        self.parent_window = parent_window

    @pyqtSlot(str)
    def save_project(self, json_data):
        file_path, _ = QFileDialog.getSaveFileName(
            self.parent_window, 
            "Сохранить плейс", 
            "my_place.rbxl", 
            "NOVA Place Files (*.rbxl *.json);;All Files (*)"
        )
        if file_path:
            try:
                with open(file_path, "w", encoding="utf-8") as f:
                    f.write(json_data)
                QMessageBox.information(self.parent_window, "Успех", f"Карта сохранена:\n{file_path}")
            except Exception as e:
                QMessageBox.critical(self.parent_window, "Ошибка", f"Не удалось сохранить файл:\n{e}")

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("NovaStudio")
        self.setGeometry(100, 100, 1100, 700)

        self.browser = QWebEngineView()
        
        # Настройка QWebChannel для связи Python <-> JS
        self.channel = QWebChannel()
        self.bridge = PyBridge(self)
        self.channel.registerObject("pyBridge", self.bridge)
        
        self.browser.page().setWebChannel(self.channel)
        self.browser.setHtml(STUDIO_HTML)
        self.setCentralWidget(self.browser)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())