{% extends 'base.html' %}

{% block content %}
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-2 border-bottom">
    <div>
        <h1 class="h2 mb-1"><i class="bi bi-eye text-primary me-2"></i>{{ _('GCode Preview') }}: {{ file.original_filename }}</h1>
        <div class="text-muted small">
            <span class="me-3"><i class="bi bi-clock-history me-1"></i>{{ _('Estimated Time:') }} <span class="fw-bold">{{ time_info }}</span></span>
            <span class="me-3"><i class="bi bi-layers me-1"></i>{{ _('First Layer Time:') }} <span class="fw-bold">{{ layer1_time }}</span></span>
            <span><i class="bi bi-rulers me-1"></i>{{ _('Filament Used [mm]:') }} <span class="fw-bold">{{ filament_used }}</span></span>
        </div>
    </div>
    <div class="mt-2 mt-md-0">
        <a href="{{ url_for('printer.prepare') }}#file-{{ file.id }}" class="btn btn-warning btn-sm rounded shadow-sm fw-bold"><i class="bi bi-printer"></i> {{ _('Go to Print') }}</a>
        <a href="{{ url_for('main.download_gcode', file_id=file.id) }}" class="btn btn-primary btn-sm rounded shadow-sm ms-2"><i class="bi bi-download"></i> {{ _('Download GCode') }}</a>
        <a href="{{ url_for('main.files') }}" class="btn btn-outline-secondary btn-sm rounded ms-2 shadow-sm"><i class="bi bi-arrow-left"></i> {{ _('Back') }}</a>
    </div>
</div>

<div id="loading-overlay" class="text-center py-5 my-5">
    <div class="spinner-border text-primary shadow-sm" role="status" style="width: 3rem; height: 3rem;"></div>
    <h4 class="mt-4 text-secondary">{{ _('Loading and Parsing GCode Data...') }}</h4>
</div>

<div class="row d-none" id="preview-container" style="height: 75vh;">
    <!-- 3D Canvas Area -->
    <div class="col-md-11 position-relative h-100 p-0 border rounded border-secondary shadow-sm" style="background: #111;">
        <div id="canvas-container" class="w-100 h-100 d-block overflow-hidden"></div>
        
        <!-- Legend Overlay -->
        <div id="legend-overlay" class="position-absolute top-0 start-0 m-3 p-2 rounded shadow bg-dark bg-opacity-75 border border-secondary" style="color: #eee; font-size: 0.85rem; pointer-events: auto; z-index: 10;">
        </div>
        
        <!-- Bottom Slider (Intra-Layer Progress) -->
        <div class="position-absolute bottom-0 start-0 w-100 p-3" style="background: linear-gradient(180deg, transparent 0%, rgba(0,0,0,0.8) 100%); z-index: 10;">
            <div class="d-flex align-items-center gap-3">
                <span class="text-white fw-medium text-nowrap user-select-none"><i class="bi bi-play-circle me-1"></i>{{ _('Layer Progress:') }}</span>
                <input type="range" class="form-range flex-grow-1" id="progress-slider" min="0" max="100" value="100" step="0.1">
            </div>
        </div>
    </div>
    
    <!-- Right Sidebar (Layer Slider) -->
    <div class="col-md-1 h-100 d-flex flex-column align-items-center justify-content-center bg-white border rounded shadow-sm position-relative">
        <label class="form-label mb-3 fw-bold text-center text-primary mt-3">{{ _('Layer') }}<br>
            <span id="layer-display" class="badge bg-primary fs-6 mt-1 shadow-sm px-3 rounded-pill">0</span>
        </label>
        <div class="flex-grow-1 w-100 d-flex justify-content-center pb-4 py-2">
            <input type="range" class="form-range h-100" id="layer-slider" min="0" max="0" value="0" style="writing-mode: bt-lr; -webkit-appearance: slider-vertical; cursor: ns-resize;">
        </div>
    </div>
</div>

<script src="{{ url_for('static', filename='js/three.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/OrbitControls.js') }}"></script>

<script>
document.addEventListener('DOMContentLoaded', async function() {
    
    // Inject printer machine dimensions via Jinja
    const bedWidth = {{ machine_width | default(220) }};
    const bedDepth = {{ machine_depth | default(220) }};
    const bedHeight = {{ machine_height | default(250) }};
    const offsetX = {{ offset_x | default(0.0) }};
    const offsetY = {{ offset_y | default(0.0) }};
    
    // Type indices for shader visibility filtering
    let COLORS = {};
    let TYPE_INDEX = {};
    let gcodeMat = null;

    const SLICER_CONFIGS = {
        'Cura': [
            { id: 'TRAVEL', label: '{{ _("Travel (Move)") }}', color: 0x405060, defaultShow: false },
            { id: 'WALL-OUTER', label: '{{ _("Outer Wall") }}', color: 0xeb8b38, defaultShow: true },
            { id: 'WALL-INNER', label: '{{ _("Inner Wall") }}', color: 0x4080cf, defaultShow: true },
            { id: 'FILL', label: '{{ _("Infill") }}', color: 0xccc04b, defaultShow: true },
            { id: 'SKIN', label: '{{ _("Skin/TopBottom") }}', color: 0x9e60b3, defaultShow: true },
            { id: 'SUPPORT', label: '{{ _("Support") }}', color: 0x57b357, defaultShow: true },
            { id: 'SKIRT', label: '{{ _("Skirt") }}', color: 0x00ffff, defaultShow: true },
            { id: 'SUPPORT-INTERFACE', label: '{{ _("Support Interface") }}', color: 0x2b6b2b, defaultShow: true },
            { id: 'DEFAULT', label: '{{ _("Others") }}', color: 0xaaaaaa, defaultShow: true }
        ],
        'Prusa': [
            { id: 'TRAVEL', label: '{{ _("Travel (Move)") }}', color: 0x405060, defaultShow: false },
            { id: 'Custom', label: '{{ _("Custom") }}', color: 0xd0e0ff, defaultShow: true },
            { id: 'Skirt/Brim', label: '{{ _("Skirt/Brim") }}', color: 0x00FFFF, defaultShow: true },
            { id: 'Support material', label: '{{ _("Support material") }}', color: 0x90EE90, defaultShow: true },
            { id: 'Perimeter', label: '{{ _("Perimeter") }}', color: 0xFFFFE0, defaultShow: true },
            { id: 'External perimeter', label: '{{ _("External perimeter") }}', color: 0xFFA500, defaultShow: true },
            { id: 'Solid infill', label: '{{ _("Solid infill") }}', color: 0x800080, defaultShow: true },
            { id: 'Overhang perimeter', label: '{{ _("Overhang perimeter") }}', color: 0x00008B, defaultShow: true },
            { id: 'Internal infill', label: '{{ _("Internal infill") }}', color: 0x8B0000, defaultShow: true },
            { id: 'Bridge infill', label: '{{ _("Bridge infill") }}', color: 0x0000FF, defaultShow: true },
            { id: 'Top solid infill', label: '{{ _("Top solid infill") }}', color: 0xFF0000, defaultShow: true },
            { id: 'Support material interface', label: '{{ _("Support Interface") }}', color: 0x2b6b2b, defaultShow: true },
            { id: 'DEFAULT', label: '{{ _("Others") }}', color: 0xaaaaaa, defaultShow: true }
        ]
    };

    let layers = [];
    let scene, camera, renderer, controls;
    let group = new THREE.Group();
    
    const layerSlider = document.getElementById('layer-slider');
    const layerDisplay = document.getElementById('layer-display');
    const progressSlider = document.getElementById('progress-slider');
    
    function setupSlicerConfig(text) {
        let slicerType = 'Cura'; // default
        if (text.substring(0, 500).includes('generated by PrusaSlicer')) {
            slicerType = 'Prusa';
        }
        
        const config = SLICER_CONFIGS[slicerType];
        
        // 1. Build uniforms & shader strings dynamically
        let uniformsObj = {};
        let fragmentUniformsDecl = '';
        let fragmentUniformsLogic = '';
        
        let overlayHTML = '';
        
        config.forEach((c, idx) => {
            COLORS[c.id] = new THREE.Color(c.color);
            TYPE_INDEX[c.id] = idx;
            
            const uniformName = 'uShow' + idx;
            uniformsObj[uniformName] = { value: c.defaultShow ? 1.0 : 0.0 };
            
            fragmentUniformsDecl += `uniform float ${uniformName};\n`;
            
            if (idx === 0) {
                fragmentUniformsLogic += `if (t == 0) show = ${uniformName};\n`;
            } else {
                fragmentUniformsLogic += `                else if (t == ${idx}) show = ${uniformName};\n`;
            }
            
            // Build Legend UI
            const hexColor = '#' + c.color.toString(16).padStart(6, '0');
            const opacityStyle = c.defaultShow ? '1.0' : '0.4';
            overlayHTML += `
            <div class="mb-1 legend-item user-select-none" data-id="${c.id}" data-uniform="${uniformName}" style="cursor: pointer; transition: opacity 0.2s; opacity: ${opacityStyle};"><span class="d-inline-block rounded-circle me-2 border border-dark" style="width: 12px; height: 12px; background: ${hexColor};"></span>${c.label}</div>`;
        });
        
        // Add fallback condition
        fragmentUniformsLogic += `                else show = 1.0;\n`;
        
        document.getElementById('legend-overlay').innerHTML = overlayHTML;
        
        gcodeMat = new THREE.ShaderMaterial({
            uniforms: uniformsObj,
            vertexShader: `
                attribute float pType;
                varying vec3 vColor;
                varying float vType;
                void main() {
                    vColor = color;
                    vType = pType;
                    gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
                }
            `,
            fragmentShader: `
                varying vec3 vColor;
                varying float vType;
${fragmentUniformsDecl}
                void main() {
                    float show = 1.0;
                    int t = int(vType + 0.5);
                    ${fragmentUniformsLogic}
                    if (show < 0.5) discard;
                    gl_FragColor = vec4(vColor, 1.0);
                }
            `,
            vertexColors: true,
            side: THREE.DoubleSide,
            linewidth: 1
        });
        
        // Legend binding
        document.querySelectorAll('.legend-item').forEach(el => {
            el.addEventListener('click', function() {
                const uniformName = this.dataset.uniform;
                if (uniformName && gcodeMat.uniforms[uniformName]) {
                    const currentVal = gcodeMat.uniforms[uniformName].value;
                    const newVal = currentVal > 0.5 ? 0.0 : 1.0;
                    gcodeMat.uniforms[uniformName].value = newVal;
                    this.style.opacity = newVal > 0.5 ? "1.0" : "0.4";
                }
            });
        });
    }

    function init3D() {
        const container = document.getElementById('canvas-container');
        scene = new THREE.Scene();
        scene.background = new THREE.Color(0x1a1a1a);
        
        scene.add(group);
        
        camera = new THREE.PerspectiveCamera(45, container.clientWidth / container.clientHeight, 1, 5000);
        camera.up.set(0, 0, 1);
        
        renderer = new THREE.WebGLRenderer({ antialias: true });
        renderer.setSize(container.clientWidth, container.clientHeight);
        renderer.setPixelRatio(window.devicePixelRatio);
        container.appendChild(renderer.domElement);
        
        controls = new THREE.OrbitControls(camera, renderer.domElement);
        controls.enableDamping = false;
        controls.mouseButtons.MIDDLE = THREE.MOUSE.PAN;
        
        window.addEventListener('resize', onWindowResize);
    }
    
    function onWindowResize() {
        const container = document.getElementById('canvas-container');
        camera.aspect = container.clientWidth / container.clientHeight;
        camera.updateProjectionMatrix();
        renderer.setSize(container.clientWidth, container.clientHeight);
    }
    
    function animate() {
        requestAnimationFrame(animate);
        controls.update();
        renderer.render(scene, camera);
    }
    
    async function loadGCode() {
        try {
            const url = '{{ url_for("main.download_gcode", file_id=file.id) }}';
            const response = await fetch(url);
            if (!response.ok) throw new Error("GCode Request Failed");
            
            const gcodeText = await response.text();
            
            document.getElementById('loading-overlay').classList.add('d-none');
            document.getElementById('preview-container').classList.remove('d-none');
            
            setupSlicerConfig(gcodeText);
            init3D();
            parseGCode(gcodeText);
            
            // Add grid matching printer size
            setupMachineEnvironment();
            
            animate();
            
            // Init controls
            layerSlider.max = Math.max(0, layers.length - 1);
            layerSlider.value = Math.max(0, layers.length - 1);
            updateUI();
            
            layerSlider.addEventListener('input', updateUI);
            progressSlider.addEventListener('input', updateUI);
            
        } catch(e) {
            console.error("Error Loading GCode", e);
            document.getElementById('loading-overlay').innerHTML = `
                <div class="text-danger my-5 py-5">
                    <i class="bi bi-exclamation-triangle display-1"></i>
                    <h3 class="mt-3">{{ _('Failed to load GCode preview.') }}</h3>
                    <p class="text-muted">${e.toString()}</p>
                </div>`;
        }
    }
    
    function parseGCode(text) {
        const lines = text.split('\n');
        
        let current = { x: 0, y: 0, z: 0, e: 0 };
        let relativeE = false; // Track M83 (relative) vs M82 (absolute)
        
        // Dynamically compute width and layer height based on gcode info if possible
        let extWidth = 0.4;
        let layerHeight = 0.2;
        let pWidth = extWidth;
        
        let currentTypeStr = 'DEFAULT';
        
        let currentExtrudePoints = [];
        let currentExtrudeColors = [];
        let currentExtrudeTypes = [];
        
        let currentTravelPoints = [];
        let currentTravelColors = [];
        let currentTravelTypes = [];
        
        function flushLayer() {
            if (currentExtrudePoints.length === 0 && currentTravelPoints.length === 0) return;
            
            let layerGroup = new THREE.Group();
            
            if (currentExtrudePoints.length > 0) {
                const geo = new THREE.BufferGeometry();
                geo.setAttribute('position', new THREE.Float32BufferAttribute(currentExtrudePoints, 3));
                geo.setAttribute('color', new THREE.Float32BufferAttribute(currentExtrudeColors, 3));
                geo.setAttribute('pType', new THREE.Float32BufferAttribute(currentExtrudeTypes, 1));
                const mesh = new THREE.Mesh(geo, gcodeMat);
                mesh.userData.isExtrude = true;
                layerGroup.add(mesh);
                currentExtrudePoints = []; currentExtrudeColors = []; currentExtrudeTypes = [];
            }
            
            if (currentTravelPoints.length > 0) {
                const geo = new THREE.BufferGeometry();
                geo.setAttribute('position', new THREE.Float32BufferAttribute(currentTravelPoints, 3));
                geo.setAttribute('color', new THREE.Float32BufferAttribute(currentTravelColors, 3));
                geo.setAttribute('pType', new THREE.Float32BufferAttribute(currentTravelTypes, 1));
                const lineSeg = new THREE.LineSegments(geo, gcodeMat);
                lineSeg.userData.isTravel = true;
                layerGroup.add(lineSeg);
                currentTravelPoints = []; currentTravelColors = []; currentTravelTypes = [];
            }
            
            layers.push(layerGroup);
            group.add(layerGroup);
        }
        
        for (let i = 0; i < lines.length; i++) {
            let chunk = lines[i].trim();
            if (!chunk) continue;
            let upperChunk = chunk.toUpperCase();
            
            if (upperChunk.startsWith('M82')) relativeE = false;
            else if (upperChunk.startsWith('M83')) relativeE = true;
            
            if (upperChunk.startsWith(';LAYER:') || upperChunk.startsWith(';LAYER_CHANGE')) {
                flushLayer();
            } else if (upperChunk.startsWith(';LAYER_HEIGHT:')) {
                let lh = parseFloat(chunk.substring(14));
                if (!isNaN(lh) && lh > 0) layerHeight = lh;
            } else if (upperChunk.startsWith(';HEIGHT:')) {
                let lh = parseFloat(chunk.substring(8));
                if (!isNaN(lh) && lh > 0) layerHeight = lh;
            } else if (upperChunk.startsWith(';WIDTH:')) {
                let w = parseFloat(chunk.substring(7));
                if (!isNaN(w) && w > 0) pWidth = w;
            } else if (upperChunk.startsWith(';TYPE:')) {
                currentTypeStr = chunk.substring(6).trim();
            } else if (chunk.startsWith(';') && COLORS[chunk.substring(1).trim()] !== undefined) {
                currentTypeStr = chunk.substring(1).trim();
            } else if (upperChunk.startsWith(';') && chunk.includes(' perimeter')) {
                currentTypeStr = chunk.substring(1).trim();
            } else if (upperChunk.startsWith(';') && chunk.includes(' infill')) {
                // Heuristics for Prusa/Slic3r specific comments like `; Internal infill`
                currentTypeStr = chunk.substring(1).trim();
            } else if (upperChunk.startsWith(';') && chunk.includes(' material')) {
                // Support material
                currentTypeStr = chunk.substring(1).trim();
            } else if (upperChunk.startsWith(';') && chunk.includes('Skirt/Brim')) {
                // Skirt/Brim
                currentTypeStr = 'Skirt/Brim';
            } else if (upperChunk.startsWith('G0') || upperChunk.startsWith('G1')) {
                let next = { x: current.x, y: current.y, z: current.z, e: current.e };
                let parts = upperChunk.split(/\s+/);
                let hasMove = false;
                let hasE = false;
                let eVal = 0;
                
                for (let p of parts) {
                    if (p.startsWith('X')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) { next.x = v; hasMove = true; } }
                    if (p.startsWith('Y')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) { next.y = v; hasMove = true; } }
                    if (p.startsWith('Z')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) { next.z = v; hasMove = true; } }
                    if (p.startsWith('E')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) { eVal = v; hasE = true; } }
                }
                
                if (hasMove && !isNaN(next.x) && !isNaN(next.y) && !isNaN(next.z)) {
                    let isExtrude = false;
                    if (hasE) {
                        if (relativeE) {
                            next.e = current.e + eVal;
                            isExtrude = eVal > 0;
                        } else {
                            next.e = eVal;
                            isExtrude = next.e > current.e;
                        }
                    }
                    // Cura uses G0 for travel generally
                    if (upperChunk.startsWith('G0') && !upperChunk.includes('E')) isExtrude = false;
                    
                    let activeType = isExtrude ? currentTypeStr : 'TRAVEL';
                    let resolvedType = activeType;
                    
                    if (isExtrude && COLORS[activeType] === undefined) {
                        resolvedType = 'DEFAULT';
                    }
                    
                    let col = COLORS[resolvedType] || COLORS['DEFAULT'];
                    let tIdx = TYPE_INDEX[resolvedType] !== undefined ? TYPE_INDEX[resolvedType] : TYPE_INDEX['DEFAULT'];
                    
                    if (isExtrude) {
                        let dx = next.x - current.x;
                        let dy = next.y - current.y;
                        let dist = Math.sqrt(dx*dx + dy*dy);
                        if (dist > 0.0001) {
                            let hw = pWidth / 2.0; 
                            let hh = layerHeight / 2.0; 
                            
                            let nx = -(dy / dist) * hw;
                            let ny = (dx / dist) * hw;
                            
                            let p1x = current.x + nx, p1y = current.y + ny; // current-left
                            let p2x = current.x - nx, p2y = current.y - ny; // current-right
                            let p3x = next.x + nx, p3y = next.y + ny;       // next-left
                            let p4x = next.x - nx, p4y = next.y - ny;       // next-right
                            
                            // Top face
                            currentExtrudePoints.push(
                                p1x, p1y, current.z + hh,
                                p3x, p3y, next.z + hh,
                                p2x, p2y, current.z + hh,
                                p3x, p3y, next.z + hh,
                                p4x, p4y, next.z + hh,
                                p2x, p2y, current.z + hh
                            );
                            for(let k=0; k<6; k++) { currentExtrudeColors.push(col.r, col.g, col.b); currentExtrudeTypes.push(tIdx); }
                            
                            // Bottom face
                            currentExtrudePoints.push(
                                p1x, p1y, current.z - hh,
                                p2x, p2y, current.z - hh,
                                p3x, p3y, next.z - hh,
                                p2x, p2y, current.z - hh,
                                p4x, p4y, next.z - hh,
                                p3x, p3y, next.z - hh
                            );
                            for(let k=0; k<6; k++) { currentExtrudeColors.push(col.r*0.4, col.g*0.4, col.b*0.4); currentExtrudeTypes.push(tIdx); }
                            
                            // Left face
                            currentExtrudePoints.push(
                                p1x, p1y, current.z - hh,
                                p3x, p3y, next.z - hh,
                                p1x, p1y, current.z + hh,
                                p3x, p3y, next.z - hh,
                                p3x, p3y, next.z + hh,
                                p1x, p1y, current.z + hh
                            );
                            // Fake lighting based on normal side
                            for(let k=0; k<6; k++) { currentExtrudeColors.push(col.r*0.6, col.g*0.6, col.b*0.6); currentExtrudeTypes.push(tIdx); }
                            
                            // Right face
                            currentExtrudePoints.push(
                                p2x, p2y, current.z - hh,
                                p2x, p2y, current.z + hh,
                                p4x, p4y, next.z - hh,
                                p2x, p2y, current.z + hh,
                                p4x, p4y, next.z + hh,
                                p4x, p4y, next.z - hh
                            );
                            for(let k=0; k<6; k++) { currentExtrudeColors.push(col.r*0.8, col.g*0.8, col.b*0.8); currentExtrudeTypes.push(tIdx); }
                        }
                    } else {
                        // Travel lines get slight vertical offset for visibility
                        let zOff = 0.05;
                        currentTravelPoints.push(current.x, current.y, current.z + zOff);
                        currentTravelPoints.push(next.x, next.y, next.z + zOff);
                        currentTravelColors.push(col.r, col.g, col.b, col.r, col.g, col.b);
                        currentTravelTypes.push(tIdx, tIdx);
                    }
                    
                    // Update E based on parsed G-code execution type
                    if (hasE) {
                        if (relativeE) current.e += eVal;
                        else current.e = eVal;
                    }
                    
                    current.x = next.x; current.y = next.y; current.z = next.z;
                }
            } else if (upperChunk.startsWith('G92')) {
                let parts = chunk.split(/\s+/);
                for (let p of parts) {
                    if (p.startsWith('E')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) current.e = v; }
                    if (p.startsWith('X')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) current.x = v; }
                    if (p.startsWith('Y')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) current.y = v; }
                    if (p.startsWith('Z')) { let v = parseFloat(p.substring(1)); if(!isNaN(v)) current.z = v; }
                }
            }
        }
        flushLayer();
    }
    
    function setupMachineEnvironment() {
        if (layers.length === 0) return;
        
        let bbox = new THREE.Box3();
        for (let layerGrp of layers) {
            layerGrp.children.forEach(child => {
                child.geometry.computeBoundingBox();
                bbox.union(child.geometry.boundingBox);
            });
        }
        
        // The GCode coordinates for the actual print bed are from (0,0) to (W,H).
        // The GCode trajectory is ALREADY offset by plater.html during slicing.
        // We just need to place the grid exactly in the center of the bed: (W/2, H/2).
        let gridOffsetX = (bedWidth / 2);
        let gridOffsetY = (bedDepth / 2);
        
        // Add Grid
        const gridDivisions = Math.ceil(Math.max(bedWidth, bedDepth) / 10);
        const gridHelper = new THREE.GridHelper(Math.max(bedWidth, bedDepth), gridDivisions, 0x444444, 0x242424);
        gridHelper.rotation.x = Math.PI / 2;
        gridHelper.position.set(gridOffsetX, gridOffsetY, 0);
        scene.add(gridHelper);

        // Add Printer Volume Outline
        const boxGeo = new THREE.BoxGeometry(bedWidth, bedDepth, bedHeight);
        const edges = new THREE.EdgesGeometry(boxGeo);
        const boxOutline = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0x444444 }));
        boxOutline.position.set(gridOffsetX, gridOffsetY, bedHeight/2);
        scene.add(boxOutline);
        
        // Align Camera to target the center of the bed grid
        controls.target.set(gridOffsetX, gridOffsetY, 0);
        camera.position.set(gridOffsetX, gridOffsetY - (bedDepth * 1.5), bedHeight * 0.8);
    }
    
    function updateUI() {
        if (layers.length === 0) return;
        let activeIdx = parseInt(layerSlider.value);
        let intraProg = parseFloat(progressSlider.value);
        
        layerDisplay.innerText = activeIdx + " / " + (layers.length - 1);
        
        for (let i = 0; i < layers.length; i++) {
            let layerGrp = layers[i];
            if (i < activeIdx) {
                layerGrp.visible = true;
                layerGrp.children.forEach(child => child.geometry.setDrawRange(0, Infinity));
            } else if (i === activeIdx) {
                layerGrp.visible = true;
                layerGrp.children.forEach(child => {
                    let totalVertices = child.geometry.attributes.position.count;
                    let elementsPerUnit = child.userData.isTravel ? 2 : 24; 
                    let totalUnits = totalVertices / elementsPerUnit;
                    let drawCount = Math.floor(totalUnits * (intraProg / 100)) * elementsPerUnit;
                    child.geometry.setDrawRange(0, drawCount);
                });
            } else {
                layerGrp.visible = false;
            }
        }
    }
    
    loadGCode();
});
</script>
{% endblock %}