SF映画っぽいエクスプローラ「Exp3D 1.5」


画像
画像

ダウンロードされる方はこちら。↓


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Exp3D 1.5.1</title>
    <style>
        :root {
            --cyan: #00ffff;
            --magenta: #ff00ff;
            --orange: #ffaa00;
            --yellow: #ffee00;
            --ocher: #ccaa00;
            --dark-ocher: #886600;
            --blue: #0088ff;
            --bg: #010103;
            --panel: rgba(5, 10, 15, 0.95);
        }
        body { margin: 0; overflow: hidden; background-color: var(--bg); font-family: 'Segoe UI', monospace; user-select: none; color: #fff; }

        /* App Layout */
        #app { display: flex; width: 100vw; height: 100vh; position: relative; overflow: hidden; }

        /* Panes */
        #pane-tree { 
            flex: 0 0 25%; position: relative; overflow: hidden; 
            border-right: 1px solid #333; 
            background: rgba(0,0,0,0.2); 
            z-index: 10; pointer-events: none; 
            display: flex; flex-direction: column;
        }
        #pane-file { 
            flex: 1; position: relative; overflow: hidden; 
            background: transparent; 
            z-index: 10; pointer-events: none; 
        }
        
        /* Interactive UI Elements */
        .pane-label, .sys-btn, .stock-item, .stock-tab-item, #stock, #stock-tabs-container, #pagination-ctrl, .tree-node, .view-switch-btn, #btn-select-root {
            pointer-events: auto;
        }

        /* Splitter */
        #splitter {
            width: 6px; cursor: col-resize; background: #111; border-left: 1px solid #444; border-right: 1px solid #444;
            display: flex; flex-direction: column; justify-content: center; align-items: center; z-index: 100;
            transition: background 0.2s; pointer-events: auto;
        }
        #splitter:hover, #splitter.active { background: var(--orange); }
        .grip { width: 2px; height: 4px; background: #666; margin: 2px 0; }

        /* Canvas Container */
        #canvas-wrap {
            position: absolute; top: 0; left: 0; width: 100%; height: 100%; 
            z-index: 0; pointer-events: auto; 
        }
        canvas { display: block; outline: none; }

        /* HUD */
        #hud { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 50; }
        #hud-canvas { width: 100%; height: 100%; display: block; }

        /* Tree Pane Header */
        #tree-header {
            height: 40px; background: linear-gradient(to bottom, rgba(0,20,40,0.9), transparent);
            display: flex; align-items: center; padding: 0 10px; flex-shrink: 0;
            border-bottom: 1px solid rgba(255,255,255,0.1);
            gap: 10px;
        }
        .tree-title { font-size: 14px; font-weight: bold; color: var(--cyan); letter-spacing: 1px; white-space: nowrap; }
        
        #btn-select-root {
            background: rgba(0,0,0,0.5); border: 1px solid var(--cyan); color: var(--cyan);
            padding: 2px 8px; font-size: 11px; cursor: pointer; transition: 0.2s; white-space: nowrap; font-weight: bold;
        }
        #btn-select-root:hover { background: var(--cyan); color: #000; box-shadow: 0 0 5px var(--cyan); }

        .view-switch { display: flex; gap: 2px; }
        .view-switch-btn {
            background: rgba(0,0,0,0.5); border: 1px solid #444; color: #666;
            padding: 2px 8px; font-size: 11px; cursor: pointer; transition: 0.2s;
        }
        .view-switch-btn:hover { color: #fff; border-color: #888; }
        .view-switch-btn.active { background: var(--ocher); color: #000; border-color: var(--ocher); font-weight: bold; box-shadow: 0 0 5px var(--ocher); }

        /* Top Bar (Right Pane Header) */
        #top-bar {
            position: absolute; top: 0; left: 0; width: 100%; height: 40px;
            display: flex; align-items: center; justify-content: flex-end; padding: 0 20px;
            z-index: 60; pointer-events: none;
        }

        .pane-label {
            position: absolute; bottom: 10px; left: 10px; font-size: 12px; 
            color: rgba(255,255,255,0.3); pointer-events: none; z-index: 10;
        }
        #mode-info {
            position: absolute; top: 50px; right: 20px; text-align: right; pointer-events: none;
            color: var(--yellow); text-shadow: 0 0 5px var(--yellow); font-weight: bold; font-size: 18px; z-index: 20;
        }

        /* Tree View Items (2D) */
        #tree-container { 
            padding: 10px; overflow-y: auto; flex: 1; box-sizing: border-box; 
            display: none; /* Hidden by default (3D mode active) */
            background: rgba(0,0,0,0.3);
        }
        .tree-node {
            padding: 4px 8px; color: #aaa; cursor: pointer; display: flex; align-items: center;
            border-bottom: 1px solid rgba(255,255,255,0.05); transition: background 0.2s; font-size: 13px;
        }
        .tree-node:hover { background: rgba(0,255,255,0.1); color: #fff; }
        .tree-icon { margin-right: 8px; font-size: 14px; color: var(--ocher); }

        /* Pagination Control */
        #pagination-ctrl {
            position: absolute; top: 80px; right: 20px;
            display: flex; align-items: center; gap: 10px;
            z-index: 60;
        }
        .page-btn {
            background: rgba(0,0,0,0.5); border: 1px solid var(--blue); color: var(--blue);
            width: 30px; height: 30px; display: flex; align-items: center; justify-content: center;
            cursor: pointer; font-weight: bold; transition: 0.2s;
        }
        .page-btn:hover { background: var(--blue); color: #000; }
        .page-btn.disabled { opacity: 0.3; pointer-events: none; border-color: #555; color: #555; }
        #page-display {
            font-size: 16px; font-weight: bold; color: var(--blue); min-width: 60px; text-align: center;
        }

        /* Loader */
        #loader {
            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.85); display: flex; flex-direction: column;
            justify-content: center; align-items: center; z-index: 200;
            display: none; pointer-events: auto;
        }
        #loader.active { display: flex; }
        .loader-text { color: var(--cyan); font-size: 24px; margin-bottom: 20px; animation: blink 1s infinite; letter-spacing: 2px; }
        @keyframes blink { 50% { opacity: 0.5; } }

        /* STOCK PANEL */
        #stock {
            position: absolute; bottom: 0; left: 0; right: 0; height: 180px;
            background: linear-gradient(to top, rgba(20,10,0,0.95), transparent);
            border-top: 2px solid var(--orange); display: flex; align-items: center; 
            padding: 0 50px; gap: 40px; 
            z-index: 40; transition: bottom 0.3s; overflow-x: hidden;
            cursor: grab; pointer-events: auto;
        }
        #stock.active { cursor: grabbing; }
        #stock.closed { bottom: -180px; }
        
        #stock-tabs-container {
            position: absolute; bottom: 180px; left: 50%; transform: translateX(-50%);
            display: flex; gap: 5px; z-index: 41; transition: bottom 0.3s; pointer-events: auto;
        }
        #stock.closed + #stock-tabs-container { bottom: 0; }
        
        .stock-tab-item {
            background: rgba(50, 20, 0, 0.8); color: #aaa; 
            padding: 2px 20px; font-weight: bold; font-size: 12px;
            cursor: pointer; clip-path: polygon(10% 0, 90% 0, 100% 100%, 0% 100%);
            border-bottom: none; transition: 0.2s; min-width: 60px; text-align: center;
            border-top: 2px solid #553300; pointer-events: auto;
        }
        .stock-tab-item:hover { color: #fff; background: rgba(100, 50, 0, 0.8); }
        .stock-tab-item.active { background: var(--orange); color: #000; border-top: 2px solid #fff; }
        
        .stock-item {
            width: 100px; height: 130px; 
            background: rgba(0,0,0,0.3); border: 1px solid var(--orange);
            display: flex; flex-direction: column; align-items: center; 
            cursor: grab; color: var(--orange); flex-shrink: 0; position: relative;
            transition: 0.2s; user-select: none; pointer-events: auto;
        }
        .stock-item:hover { background: rgba(255,170,0,0.1); box-shadow: 0 0 10px rgba(255,170,0,0.2); }
        .stock-thumb { width: 90px; height: 90px; margin-top: 5px; object-fit: contain; background: #000; display:flex; justify-content:center; align-items:center; overflow:hidden; }
        .stock-label { font-size: 11px; margin-top: 5px; width: 90px; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-weight: bold; }

        /* Context Menu */
        .ctx-menu {
            position: fixed; display: none; flex-direction: column;
            background: rgba(10, 15, 20, 0.98); border: 1px solid var(--cyan);
            box-shadow: 0 0 20px rgba(0,255,255,0.3); min-width: 200px; z-index: 9999;
            transform-origin: top; padding: 5px 0; overflow: hidden; pointer-events: auto;
        }
        .ctx-menu.opening { animation: menuOpen 0.2s ease-out forwards; }
        @keyframes menuOpen { from { transform: scaleY(0); opacity: 0; } to { transform: scaleY(1); opacity: 1; } }
        .ctx-item { padding: 10px 15px; cursor: pointer; color: #ccc; display: flex; justify-content: space-between; border-bottom: 1px solid #333; transition: background 0.1s; }
        .ctx-item:hover { background: var(--cyan); color: #000; }
        .ctx-menu.closing .ctx-item { animation: itemFold 0.2s forwards ease-in; }
        @keyframes itemFold { 0% { transform: translateY(0); opacity: 1; height: 40px; } 100% { transform: translateY(-20px); opacity: 0; height: 0; padding: 0; border: none; } }

        /* Modals */
        .modal-overlay {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8);
            display: flex; justify-content: center; align-items: center; z-index: 300;
            opacity: 0; pointer-events: none; visibility: hidden; transition: opacity 0.3s;
            perspective: 1500px;
        }
        .modal-overlay.active { opacity: 1; pointer-events: auto; visibility: visible; }
        
        .modal-box { 
            border: 1px solid var(--cyan); background: #000; 
            display: flex; flex-direction: column; box-shadow: 0 0 50px rgba(0,255,255,0.2); 
            transform-style: preserve-3d; opacity: 0; transform-origin: center;
        }
        .modal-overlay.opening .modal-box { animation: animFlipOpen 0.6s cubic-bezier(0.19, 1, 0.22, 1) forwards; }
        .modal-overlay.closing .modal-box { animation: animFlipClose 0.5s cubic-bezier(0.19, 1, 0.22, 1) forwards; }
        @keyframes animFlipOpen { 0% { transform: translateZ(-500px) rotateX(180deg); opacity: 0; } 100% { transform: translateZ(0) rotateX(0deg); opacity: 1; } }
        @keyframes animFlipClose { 0% { transform: translateZ(0) rotateX(0deg); opacity: 1; } 100% { transform: translateZ(-500px) rotateX(-180deg); opacity: 0; } }

        .modal-head { padding: 10px; background: rgba(0,255,255,0.15); color: var(--cyan); display: flex; justify-content: space-between; align-items: center; font-weight: bold; }
        .modal-body { flex: 1; padding: 0; overflow: hidden; position: relative; background: #050505; display: flex; justify-content: center; align-items: center; }
        .p-btn { background: transparent; border: 1px solid var(--cyan); color: var(--cyan); padding: 5px 15px; margin-left: 10px; cursor: pointer; transition: 0.2s; }
        .p-btn:hover { background: var(--cyan); color: #000; }
        .p-btn-save { display: none; border-color: #0f0; color: #0f0; }

        #p-box { width: 90%; height: 90%; }
        .hex-view { width: 100%; height: 100%; padding: 15px; color: #0f0; background: #000; border: none; resize: none; font-family: 'Courier New', monospace; outline: none; }
        #d-box { width: 400px; height: 200px; }
        #d-body { flex-direction: column; padding: 20px; gap: 20px; }
        .d-input { width: 100%; background: #111; border: 1px solid #333; color: #fff; padding: 10px; font-family: 'Segoe UI', monospace; font-size: 16px; outline: none; text-align: center; }
        .d-input:focus { border-color: var(--cyan); }
        .d-actions { display: flex; gap: 10px; width: 100%; justify-content: center; }

        .scanlines {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;
            background: linear-gradient(to bottom, rgba(255,255,255,0) 50%, rgba(0,0,0,0.1) 50%);
            background-size: 100% 3px; z-index: 9000; opacity: 0.15;
        }
    </style>
    <script type="importmap">
        {
            "imports": {
                "three": "https://unpkg.com/three@0.160.0/build/three.module.js",
                "three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
            }
        }
    </script>
</head>
<body>
    <div id="app">
        <div id="canvas-wrap"></div>

        <div id="pane-tree">
            <div id="tree-header">
                <span class="tree-title">EXP3D SYSTEM</span>
                <button id="btn-select-root">SELECT ROOT</button>
                <div class="view-switch">
                    <button id="btn-view-2d" class="view-switch-btn">2D</button>
                    <button id="btn-view-3d" class="view-switch-btn active">3D</button>
                </div>
            </div>
            <div id="tree-container"></div>
        </div>

        <div id="splitter">
            <div class="grip"></div><div class="grip"></div><div class="grip"></div>
        </div>

        <div id="pane-file">
            <div class="pane-label">SYSTEM // OBJECTS</div>
            
            <div id="hud"><canvas id="hud-canvas"></canvas></div>

            <div id="stock" class="closed">
                <div id="stock-hint" style="color:var(--orange); opacity:0.6; pointer-events:none;">[ EMPTY STOCK ]</div>
            </div>
            <div id="stock-tabs-container">
                <div id="stock-tab-placeholder" class="stock-tab-item" style="opacity:0.5; pointer-events:none;">STOCK</div>
            </div>

            <div id="pagination-ctrl">
                <div id="btn-prev" class="page-btn">&lt;</div>
                <div id="page-display">1 / 1</div>
                <div id="btn-next" class="page-btn">&gt;</div>
            </div>
        </div>

        <div id="top-bar"></div>
        <div id="mode-info">MODE: F1 (VERTICAL)</div>

        <div id="loader">
            <div class="loader-text">READING DATA...</div>
            <button id="btn-cancel-load" class="p-btn" style="border-color:#ff5555; color:#ff5555;">CLOSE</button>
        </div>

        <div id="ctx-obj" class="ctx-menu">
            <div class="ctx-item" id="cm-open">Open</div>
            <div class="ctx-item" id="cm-expand" style="color:var(--yellow); display:none;">Expand (Force)</div>
            <div class="ctx-item" id="cm-collapse" style="color:var(--yellow); display:none;">Collapse</div>
            <div class="ctx-item" id="cm-copy">Copy</div>
            <div class="ctx-item" id="cm-rename">Rename</div>
            <div class="ctx-item" id="cm-add-stock" style="color:var(--orange)">Add to Stock</div>
            <div class="ctx-item" id="cm-delete" style="color:#ff5555">Delete</div>
        </div>
        <div id="ctx-bg" class="ctx-menu">
            <div class="ctx-item" id="cm-new">New Folder</div>
            <div class="ctx-item" id="cm-paste">Paste</div>
            <div class="ctx-item" id="cm-up">Up Directory</div>
            <div class="ctx-item" id="cm-refresh">Refresh</div>
        </div>
        <div id="ctx-tab" class="ctx-menu">
            <div class="ctx-item" id="cm-tab-hide" style="color:var(--orange)">Hide</div>
            <div class="ctx-item" id="cm-tab-cancel" style="color:#ff5555">Cancel</div>
        </div>
    </div>

    <div id="preview" class="modal-overlay">
        <div id="p-box" class="modal-box">
            <div class="modal-head">
                <span id="p-title">FILE</span>
                <div>
                    <button id="p-save" class="p-btn p-btn-save">SAVE</button>
                    <button id="p-close" class="p-btn">CLOSE</button>
                </div>
            </div>
            <div id="p-body" class="modal-body"></div>
        </div>
    </div>

    <div id="dialog" class="modal-overlay">
        <div id="d-box" class="modal-box">
            <div class="modal-head">
                <span id="d-title">INPUT</span>
            </div>
            <div id="d-body" class="modal-body">
                <input type="text" id="d-input" class="d-input" spellcheck="false" autocomplete="off">
                <div class="d-actions">
                    <button id="d-ok" class="p-btn" style="border-color:var(--cyan); color:var(--cyan); width:100px;">OK</button>
                    <button id="d-cancel" class="p-btn" style="border-color:#ff5555; color:#ff5555; width:100px;">CANCEL</button>
                </div>
            </div>
        </div>
    </div>

    <div class="scanlines"></div>

    <script type="module">
        import * as THREE from 'three';

        // --- Configuration ---
        const CONFIG = {
            colFolderIdle: 0x886600, // Dark Ocher
            colFolderActive: 0xffff00,
            colFile: 0x0088ff, 
            treeCamOffset: { x:0, y:5, z:40 },
            maxTreeChildren: 100, 
            itemsPerPage: 20
        };

        const state = {
            splitPercent: 30, draggingSplit: false, activePane: 'none',
            treeMode: '3D',
            rootHandle: null, currentHandle: null,
            treeNodes: [], fileObjects: [], 
            currentAllEntries: [], currentPage: 1, totalPages: 1,
            activeNode: null, 
            stockList: [], activeStockIdx: -1, clipboard: null,
            viewMode: 'F1',
            targetRotX: 0, currentRotX: 0, targetRotY: 0, currentRotY: 0,
            targetPosX: 0, currentPosX: 0, targetPosY: 0, currentPosY: 0,
            targetPosZ: 0, currentPosZ: 0,
            treeScrollZ: 0, targetTreeScrollZ: 0, treeScrollX: 0, targetTreeScrollX: 0,
            mouse: new THREE.Vector2(), lastMouse: new THREE.Vector2(),
            hovered: null, selected: null, dragging: null, dragStart: new THREE.Vector2(),
            contextTarget: null, tabContextTargetIdx: -1, isLoading: false
        };

        const sfx = {
            ctx: null,
            init: () => { if (!sfx.ctx) sfx.ctx = new (window.AudioContext || window.webkitAudioContext)(); },
            play: (type) => {
                if (!sfx.ctx) return;
                const t = sfx.ctx.currentTime;
                const o = sfx.ctx.createOscillator(); const g = sfx.ctx.createGain();
                o.connect(g); g.connect(sfx.ctx.destination);
                if(type==='click'){o.frequency.setValueAtTime(600,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.1);o.start(t);o.stop(t+0.1);}
                else if(type==='hover'){o.frequency.setValueAtTime(300,t);g.gain.setValueAtTime(0.05,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.05);o.start(t);o.stop(t+0.05);}
                else if(type==='open'){o.type='triangle';o.frequency.setValueAtTime(200,t);o.frequency.linearRampToValueAtTime(600,t+0.2);g.gain.exponentialRampToValueAtTime(0.001,t+0.2);o.start(t);o.stop(t+0.2);}
                else if(type==='back'){o.type='triangle';o.frequency.setValueAtTime(600,t);o.frequency.linearRampToValueAtTime(200,t+0.2);g.gain.exponentialRampToValueAtTime(0.001,t+0.2);o.start(t);o.stop(t+0.2);}
                else if(type==='lock'){o.type='square';o.frequency.setValueAtTime(1000,t);g.gain.setValueAtTime(0.05,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.1);o.start(t);o.stop(t+0.1);}
                else if(type==='error'){o.type='sawtooth';o.frequency.setValueAtTime(100,t);g.gain.exponentialRampToValueAtTime(0.001,t+0.3);o.start(t);o.stop(t+0.3);}
            }
        };

        const container = document.getElementById('canvas-wrap');
        const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(window.innerWidth, window.innerHeight);
        renderer.setScissorTest(true); 
        renderer.sortObjects = true;
        container.appendChild(renderer.domElement);

        // Scene 1: TREE
        const sceneTree = new THREE.Scene();
        sceneTree.fog = new THREE.FogExp2(0x020205, 0.008);
        const camTree = new THREE.PerspectiveCamera(60, 1, 0.1, 2000);
        const groupTree = new THREE.Group(); sceneTree.add(groupTree);
        
        // Scene 2: FILE
        const sceneFile = new THREE.Scene();
        sceneFile.fog = new THREE.FogExp2(0x020205, 0.015);
        const camFile = new THREE.PerspectiveCamera(60, 1, 0.1, 1000);
        camFile.position.set(0, 0, 0.1); 
        const groupFile = new THREE.Group(); sceneFile.add(groupFile);

        // Groups for Overlays
        const reticleFile = new THREE.Group(); 
        reticleFile.renderOrder = 10000; 
        
        const reticleTree = new THREE.Group(); sceneTree.add(reticleTree); reticleTree.visible = false;

        [sceneTree, sceneFile].forEach(s => {
            s.add(new THREE.AmbientLight(0xffffff, 1.0));
            const dl = new THREE.DirectionalLight(0xffffff, 2);
            dl.position.set(10, 50, 20); s.add(dl);
        });

        const starGroup = new THREE.Group();
        const starsGeo = new THREE.BufferGeometry();
        const starPos = new Float32Array(3000 * 3); 
        for(let i=0; i<9000; i++) starPos[i] = (Math.random()-0.5)*1000;
        starsGeo.setAttribute('position', new THREE.BufferAttribute(starPos, 3));
        const starMat = new THREE.PointsMaterial({color:0xffffff, size:0.5, transparent:true, opacity:0.8});
        starGroup.add(new THREE.Points(starsGeo, starMat));
        const orbitGroup = new THREE.Group();
        for(let i=0; i<8; i++) {
            const r = 40 + i*15;
            const geo = new THREE.TorusGeometry(r, 0.1, 16, 100);
            const mat = new THREE.MeshBasicMaterial({color:0x00ffff, side:THREE.DoubleSide, transparent:true, opacity:0.15});
            const ring = new THREE.Mesh(geo, mat);
            ring.rotation.set(Math.random()*Math.PI, Math.random()*Math.PI, 0);
            orbitGroup.add(ring);
        }
        starGroup.add(orbitGroup);
        sceneFile.add(starGroup);

        const gridHelperTree = new THREE.GridHelper(500, 100, 0x004488, 0x001122);
        gridHelperTree.position.y = -20; sceneTree.add(gridHelperTree);
        const gridHelperFile = new THREE.GridHelper(500, 100, 0x00ffff, 0x111111);
        gridHelperFile.position.y = -15; gridHelperFile.visible = false; sceneFile.add(gridHelperFile);

        function createSegmentedRing(rIn, rOut, count, speed, dir) {
            const g = new THREE.Group();
            const angle = (Math.PI * 2) / count;
            const gap = 0.2; 
            for(let i=0; i<count; i++) {
                const s = i * angle + gap/2;
                const l = angle - gap;
                const geo = new THREE.RingGeometry(rIn, rOut, 32, 1, s, l);
                const color = new THREE.Color(0xffff00); color.multiplyScalar(1.5); 
                const mat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 1.0, side: THREE.DoubleSide, depthTest: false });
                const mesh = new THREE.Mesh(geo, mat);
                g.add(mesh);
            }
            g.userData = { speed: speed, dir: dir };
            return g;
        }
        function buildReticle(grp) {
            const r1 = createSegmentedRing(1.4, 1.45, 3, 0.08, 1); grp.add(r1);
            const r2 = createSegmentedRing(1.55, 1.6, 4, 0.05, -1); grp.add(r2);
            const r3 = createSegmentedRing(1.7, 1.75, 6, 0.02, 1); grp.add(r3);
            grp.userData = { layers: [r1, r2, r3] };
        }
        buildReticle(reticleFile); 
        buildReticle(reticleTree);

        const hudC = document.getElementById('hud-canvas');
        const hudX = hudC.getContext('2d');
        function resizeRenderer() {
            const width = window.innerWidth;
            const height = window.innerHeight;
            renderer.setSize(width, height);
            const dpr = window.devicePixelRatio || 1;
            hudC.width = width * dpr; hudC.height = height * dpr; hudX.scale(dpr, dpr);
        }
        window.addEventListener('resize', resizeRenderer);
        resizeRenderer();

        // 3D Overlays for File View
        const magMesh = new THREE.Mesh(
            new THREE.PlaneGeometry(3.6, 4.8), 
            new THREE.MeshBasicMaterial({ map: null, transparent: true, opacity: 0, depthTest: false, side: THREE.DoubleSide })
        );
        magMesh.renderOrder = 9998; 

        const infoMesh = new THREE.Mesh(
            new THREE.PlaneGeometry(12, 12), 
            new THREE.MeshBasicMaterial({ map: null, transparent: true, opacity: 0, depthTest: false, side: THREE.DoubleSide })
        );
        infoMesh.renderOrder = 9998;

        const arrowLineGeo = new THREE.BufferGeometry();
        const arrowLine = new THREE.Line(arrowLineGeo, new THREE.LineBasicMaterial({ color: 0xffff00, depthTest: false, opacity: 0.8, transparent: true }));
        arrowLine.renderOrder = 9999;

        // TREE VIEW IMPLEMENTATION
        const treeC = document.getElementById('tree-container');
        
        class TreeNode {
            constructor(handle, parent = null) {
                this.handle = handle; this.parent = parent; this.children = [];
                this.mesh = null; this.depth = parent ? parent.depth + 1 : 0;
                this.expanded = false; this.tooManyChildren = false;
            }
        }

        function createTreeMesh(node) {
            const grp = new THREE.Group();
            const geo = new THREE.BoxGeometry(3, 3, 3);
            const mat = new THREE.MeshPhongMaterial({
                color: CONFIG.colFolderIdle, emissive: 0x000000, wireframe: false,
                transparent: true, opacity: 0.9, flatShading: true
            });
            const mesh = new THREE.Mesh(geo, mat); grp.add(mesh);
            const edges = new THREE.LineSegments(new THREE.EdgesGeometry(geo), new THREE.LineBasicMaterial({ color: 0xffaa00, transparent:true, opacity:0.8 }));
            mesh.add(edges);
            
            // Label
            const cvs = document.createElement('canvas'); cvs.width=256; cvs.height=64;
            const ctx = cvs.getContext('2d');
            ctx.font = 'bold 36px "Segoe UI"'; ctx.fillStyle='#fff'; ctx.textAlign='center';
            ctx.shadowBlur=4; ctx.shadowColor='#00ffff'; ctx.fillText(node.handle.name, 128, 45);
            const tex = new THREE.CanvasTexture(cvs);
            const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: tex }));
            sprite.scale.set(6, 1.5, 1); sprite.position.y = -2.5; grp.add(sprite);
            
            grp.userData = { isNode: true, node: node };
            return grp;
        }

        function renderHtmlTree() {
            treeC.innerHTML = '';
            if(state.treeNodes.length > 0) {
                const root = state.treeNodes[0];
                treeC.appendChild(mkNode(root));
            }
        }

        function mkNode(node) {
            const container = document.createElement('div');
            const row = document.createElement('div');
            row.className = 'tree-node';
            row.style.paddingLeft = (node.depth * 20 + 10) + 'px';
            row.innerHTML = `<span class="tree-icon">${node.expanded?'📂':'📁'}</span> ${node.handle.name}`;
            row._node = node; 

            row.onclick = (e) => {
                e.stopPropagation();
                selectNode(node);
            };
            
            container.appendChild(row);
            
            if(node.expanded) {
                node.children.forEach(c => {
                    container.appendChild(mkNode(c));
                });
            }
            return container;
        }

        function updateTreeLayout() {
            // Update 3D
            groupTree.children.filter(c => c.userData.isLine).forEach(c => groupTree.remove(c));
            if (!state.treeNodes[0] || !state.treeNodes[0].mesh) return;
            const depthSpacing = 50; const spreadX = 20;
            const traverse = (node, levelIdx, levelCount) => {
                if (!node.mesh) { node.mesh = createTreeMesh(node); groupTree.add(node.mesh); state.treeNodes.push(node); }
                const z = -(node.depth * depthSpacing); let x = 0;
                if (node.parent) {
                    const offset = (levelIdx - (levelCount-1)/2) * spreadX;
                    x = node.parent.mesh.position.x + offset;
                    const pts = [node.parent.mesh.position.clone(), new THREE.Vector3(x, 0, z)];
                    const line = new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), new THREE.LineBasicMaterial({ color: 0x004466 }));
                    line.userData = { isLine: true }; groupTree.add(line);
                }
                node.mesh.position.set(x, 0, z); 
                node.mesh.visible = (state.treeMode === '3D'); 
                
                if (node.expanded && node.children.length > 0) node.children.forEach((c, i) => traverse(c, i, node.children.length));
                else { const hide = (n) => { if(n.mesh) n.mesh.visible = false; n.children.forEach(hide); }; node.children.forEach(hide); }
            };
            traverse(state.treeNodes[0], 0, 1);
            renderHtmlTree();
        }

        // Tree Mode Toggle
        const btn2D = document.getElementById('btn-view-2d');
        const btn3D = document.getElementById('btn-view-3d');
        
        btn2D.onclick = () => {
            state.treeMode = '2D';
            btn2D.classList.add('active'); btn3D.classList.remove('active');
            document.getElementById('tree-container').style.display = 'block';
            groupTree.visible = false;
            sfx.play('click');
        };
        btn3D.onclick = () => {
            state.treeMode = '3D';
            btn3D.classList.add('active'); btn2D.classList.remove('active');
            document.getElementById('tree-container').style.display = 'none';
            groupTree.visible = true;
            updateTreeLayout();
            sfx.play('click');
        };

        async function selectNode(node, forceExpand = false) {
            if(!node) return;
            state.activeNode = node; state.currentHandle = node.handle;
            state.treeNodes.forEach(n => { if(n.mesh) { n.mesh.children[0].material.color.setHex(CONFIG.colFolderIdle); n.mesh.children[0].material.emissive.setHex(0x000000); } });
            if(node.mesh) {
                node.mesh.children[0].material.color.setHex(CONFIG.colFolderActive); node.mesh.children[0].material.emissive.setHex(0x332200);
                reticleTree.visible = true; reticleTree.scale.set(3.0, 3.0, 3.0); reticleTree.position.copy(node.mesh.position);
            }
            
            if(!node.expanded || forceExpand) {
                if (!forceExpand && !node.expanded) {
                    let count = 0; try { for await (const e of node.handle.values()) { if(e.kind==='directory') count++; if(count>CONFIG.maxTreeChildren) break; } } catch(e){}
                    if (count > CONFIG.maxTreeChildren) {
                        node.tooManyChildren = true; alert(`Warning: >${CONFIG.maxTreeChildren} subfolders.`);
                        loadFilesForPane(node.handle);
                        if(node.mesh) { state.targetTreeScrollZ = node.mesh.position.z + CONFIG.treeCamOffset.z; state.targetTreeScrollX = node.mesh.position.x; }
                        sfx.play('click'); return;
                    }
                }
                const children = []; try { for await (const e of node.handle.values()) if (e.kind === 'directory') children.push(new TreeNode(e, node)); } catch(e){}
                node.children = children; node.expanded = true; node.tooManyChildren = false; updateTreeLayout();
            }
            loadFilesForPane(node.handle);
            if(node.mesh) { state.targetTreeScrollZ = node.mesh.position.z + CONFIG.treeCamOffset.z; state.targetTreeScrollX = node.mesh.position.x; }
            sfx.play('click');
        }

        async function reloadCurrentLevel() { if(state.activeNode) await selectNode(state.activeNode, true); }

        async function loadFilesForPane(handle) {
            state.isLoading = true; document.getElementById('loader').classList.add('active');
            state.currentAllEntries = []; reticleFile.visible = false; state.selected = null; state.hovered = null; hideOverlays();
            try { for await (const e of handle.values()) { state.currentAllEntries.push(e); if(state.currentAllEntries.length % 100 === 0) await new Promise(r=>setTimeout(r,0)); } } catch(e){}
            document.getElementById('loader').classList.remove('active'); state.isLoading = false;
            state.currentPage = 1; state.totalPages = Math.ceil(state.currentAllEntries.length / CONFIG.itemsPerPage) || 1;
            renderCurrentPage();
        }

        function renderCurrentPage() {
            document.getElementById('page-display').innerText = `${state.currentPage} / ${state.totalPages}`;
            document.getElementById('btn-prev').classList.toggle('disabled', state.currentPage <= 1);
            document.getElementById('btn-next').classList.toggle('disabled', state.currentPage >= state.totalPages);
            state.currentRotX=0; state.targetRotX=0; state.currentRotY=0; state.targetRotY=0;
            state.currentPosX=0; state.targetPosX=0; state.currentPosY=0; state.targetPosY=0; state.currentPosZ=0; state.targetPosZ=0;
            const start = (state.currentPage - 1) * CONFIG.itemsPerPage; const end = start + CONFIG.itemsPerPage;
            setMode(state.viewMode, state.currentAllEntries.slice(start, end));
        }

        document.getElementById('btn-prev').onclick = () => { if(state.currentPage > 1) { state.currentPage--; renderCurrentPage(); sfx.play('click'); } };
        document.getElementById('btn-next').onclick = () => { if(state.currentPage < state.totalPages) { state.currentPage++; renderCurrentPage(); sfx.play('click'); } };

        function createMonolith(handle, x, y, z, angle, index, mode) {
            const isF = handle.kind === 'directory'; 
            const col = isF ? CONFIG.colFolderIdle : CONFIG.colFile;
            
            const geo = new THREE.BoxGeometry(2.4, 3.2, 0.1);
            const mat = new THREE.MeshPhysicalMaterial({ color: col, transparent: true, opacity: 0.3, side: THREE.DoubleSide });
            const mesh = new THREE.Mesh(geo, mat);
            mesh.position.set(x, y, z);
            
            if(mode === 'F1' || mode === 'F3') mesh.lookAt(x*2, y, z*2);
            else if (mode === 'F2') mesh.lookAt(x, 0, 0);
            else if (mode === 'F4') mesh.rotation.set(0, 0, 0);
            
            mesh.userData = { handle, isFolder:isF, isFileObj:true, baseColor: col };

            const edges = new THREE.EdgesGeometry(geo);
            const line = new THREE.LineSegments(edges, new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.8 }));
            mesh.add(line);

            // Thumbnail (Backplane)
            const cvs = document.createElement('canvas'); cvs.width=256; cvs.height=256;
            const ctx = cvs.getContext('2d');
            const ext = handle.name.split('.').pop().toUpperCase().slice(0,4);
            ctx.font = `bold 100px "Segoe UI"`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
            ctx.lineWidth = 6; ctx.strokeStyle = '#000000'; ctx.strokeText(isF?'DIR':ext, 128, 128); ctx.fillStyle = '#ffffff'; ctx.fillText(isF?'DIR':ext, 128, 128);
            const defTex = new THREE.CanvasTexture(cvs);
            
            // Extension Sprite
            const extSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: defTex }));
            extSprite.scale.set(2, 2, 1); 
            // In F4 mode, put it in front (positive Z in local, which is forward here)
            extSprite.position.z = (mode === 'F4') ? 0.6 : -1.5; 
            mesh.add(extSprite);

            // Name Label (Always Visible)
            const nCvs = document.createElement('canvas'); nCvs.width=512; nCvs.height=128;
            const nCtx = nCvs.getContext('2d');
            nCtx.fillStyle = '#ffffff'; nCtx.font = 'bold 50px "Segoe UI"';
            nCtx.shadowColor = 'black'; nCtx.shadowBlur = 6; nCtx.textAlign = 'center'; nCtx.fillText(handle.name, 256, 50);
            const nTex = new THREE.CanvasTexture(nCvs);
            const nameSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: nTex, transparent: true }));
            nameSprite.scale.set(4, 1, 1); nameSprite.position.set(0, -2.2, 0); 
            mesh.add(nameSprite);

            if (!isF) {
                const n = handle.name.toLowerCase();
                const loadT = (t) => { mesh.material = new THREE.MeshBasicMaterial({map:t, color:0xffffff}); mesh.userData.thumbTex = t; };
                if(n.match(/\.(jpg|png|webp|gif)$/)) handle.getFile().then(f => new THREE.TextureLoader().load(URL.createObjectURL(f), loadT));
                else if(n.match(/\.bmp$/)) handle.getFile().then(f => createImageBitmap(f)).then(bmp => loadT(new THREE.CanvasTexture(bmp)));
                else if(n.match(/\.(mp4|webm)$/i)) handle.getFile().then(f => {
                    const v = document.createElement('video'); v.src=URL.createObjectURL(f); v.muted=true; v.loop=true; v.play().then(()=>{ loadT(new THREE.VideoTexture(v)); }).catch(()=>{});
                });
            }
            groupFile.add(mesh); state.fileObjects.push(mesh);
        }

        function setMode(mode, entries) {
            state.viewMode = mode; document.getElementById('mode-info').innerText = `MODE: ${mode}`;
            gridHelperFile.visible = false;
            state.fileObjects.forEach(o => groupFile.remove(o)); state.fileObjects = [];
            hideOverlays();
            
            // Reset transforms
            state.targetRotX=0; state.currentRotX=0;
            state.targetRotY=0; state.currentRotY=0;
            state.targetPosX=0; state.currentPosX=0;
            state.targetPosY=0; state.currentPosY=0;
            state.targetPosZ=0; state.currentPosZ=0;

            if(!entries) return; 

            const count = entries.length; const radius = 22; 
            
            if (mode === 'F1') {
                const spacing = 5.0; const rowHeight = 7.0; const circumference = 2 * Math.PI * radius;
                const cols = Math.max(1, Math.min(count, Math.floor(circumference/spacing)));
                entries.forEach((e, i) => {
                    const col = i % cols; const row = Math.floor(i / cols);
                    const angle = col * ((Math.PI * 2) / Math.max(cols, 10)) + Math.PI; 
                    const x = radius * Math.sin(angle); const z = radius * Math.cos(angle);
                    let y = -(row * rowHeight) + (Math.floor(count/cols)*rowHeight)/2; if (col % 2 !== 0) y -= rowHeight * 0.5;
                    createMonolith(e, x, y, z, angle, i, 'F1');
                });
            } else if (mode === 'F2') {
                const itemsPerRing = 12; const spacingX = 6.0; const totalRings = Math.ceil(count / itemsPerRing);
                entries.forEach((e, i) => {
                    const ring = Math.floor(i / itemsPerRing); const ang = (i % itemsPerRing) * ((Math.PI*2)/itemsPerRing);
                    const x = (ring * spacingX) - (totalRings * spacingX) / 2;
                    createMonolith(e, x, radius * Math.sin(ang), radius * Math.cos(ang), ang, i, 'F2');
                });
            } else if (mode === 'F3') {
                state.targetPosY = 20; 
                entries.forEach((e, i) => { 
                    const ang = i * 0.3; 
                    createMonolith(e, radius*Math.sin(ang), -(i*2.0), radius*Math.cos(ang), ang, i, 'F3'); 
                });
            } else if (mode === 'F4') {
                gridHelperFile.visible = true; 
                const gridW = 5; 
                const spX = 7.0; 
                const spZ = 12.0; // Large spacing
                state.targetRotX = -Math.PI / 6; state.currentRotX = -Math.PI / 6; 
                state.targetPosY = 0; state.currentPosY = 0;
                entries.forEach((e, i) => {
                    const col = i % gridW; const row = Math.floor(i / gridW);
                    const x = (col - gridW/2 + 0.5) * spX; 
                    const z = -row * spZ; // Depth
                    createMonolith(e, x, 0, z, 0, i, 'F4');
                });
            }
        }

        const splitter = document.getElementById('splitter'); const paneTree = document.getElementById('pane-tree'); const stockDiv = document.getElementById('stock');
        let isStockDragging = false; let stockStartX, stockScrollLeft;
        stockDiv.addEventListener('pointerdown', e => { if(e.target.closest('.stock-item')) return; isStockDragging=true; stockDiv.classList.add('active'); stockStartX = e.pageX - stockDiv.offsetLeft; stockScrollLeft = stockDiv.scrollLeft; e.stopPropagation(); });
        splitter.addEventListener('pointerdown', e => { state.draggingSplit = true; splitter.classList.add('active'); e.preventDefault(); });
        
        window.addEventListener('pointerup', async e => {
            isStockDragging = false; stockDiv.classList.remove('active');
            if (state.draggingSplit) { state.draggingSplit = false; splitter.classList.remove('active'); return; }
            if (state.dragging) {
                if(e.clientY > window.innerHeight - 180 && state.dragging.mesh) await moveToStock(state.dragging.mesh.userData.handle);
                else if (!state.dragging.isMove && state.dragging.mesh) { if(state.dragging.mesh.userData.isFileObj) handleFileSelection(state.dragging.mesh); }
                state.dragging = null; 
            }
            document.body.style.cursor = 'default';
        });

        function handleFileSelection(mesh) {
            state.fileObjects.forEach(o => { if(o.userData.isFolder) o.material.color.setHex(CONFIG.colFolderIdle); });
            state.selected = mesh;
            if(mesh.userData.isFolder) mesh.material.color.setHex(CONFIG.colFolderActive);
            updateOverlays(mesh); sfx.play('lock');
        }

        function hideOverlays() {
            magMesh.opacity = 0; magMesh.visible = false; magMesh.removeFromParent();
            infoMesh.opacity = 0; infoMesh.visible = false; infoMesh.removeFromParent();
            arrowLine.visible = false; arrowLine.removeFromParent();
            reticleFile.visible = false; reticleFile.removeFromParent();
        }

        function updateOverlays(target) {
            target.add(reticleFile);
            target.add(magMesh);
            target.add(infoMesh);
            target.add(arrowLine);

            const isF = target.userData.isFolder;

            const iCvs = document.createElement('canvas'); iCvs.width=512; iCvs.height=512;
            const iCtx = iCvs.getContext('2d');
            iCtx.clearRect(0,0,512,512); iCtx.font = "bold 40px 'Courier New'";
            iCtx.fillStyle = "#00ffff"; iCtx.shadowColor="#00ffff"; iCtx.shadowBlur=5;
            const lines = [ 
                `NAME: ${target.userData.handle.name}`, 
                `KIND: ${target.userData.handle.kind.toUpperCase()}`, 
                `DATE: ${new Date().toLocaleDateString()}`,
                `SIZE: ${isF ? '<DIR>' : (Math.random()*5+1).toFixed(1)+' MB'}` 
            ];
            lines.forEach((l, i) => iCtx.fillText(l, 10, 50 + i*60));
            infoMesh.material.map = new THREE.CanvasTexture(iCvs); infoMesh.material.needsUpdate = true;
            infoMesh.visible = true; infoMesh.material.opacity = 1;

            if(isF) {
                magMesh.material = new THREE.MeshBasicMaterial({ color: 0xffff00, side: THREE.DoubleSide });
            } else {
                magMesh.material = new THREE.MeshBasicMaterial({ map: target.userData.thumbTex || null, side: THREE.DoubleSide });
            }
            magMesh.visible = true; magMesh.material.opacity = 1;

            const Z_FRONT = 4.0;
            
            // Reticle: Enclose MagMesh
            reticleFile.position.set(0, 0, Z_FRONT + 0.1); 
            reticleFile.rotation.set(0, 0, 0); 
            reticleFile.scale.set(3.8, 3.8, 1); 
            reticleFile.visible = true;

            // MagMesh: Ensure it's in front of everything
            magMesh.position.set(0, 0, Z_FRONT);
            magMesh.rotation.set(0, 0, 0); 
            magMesh.scale.set(1.25, 1.25, 1); 

            // InfoMesh
            infoMesh.position.set(6.0, 0, Z_FRONT);
            infoMesh.rotation.set(0, 0, 0);
            infoMesh.scale.set(1, 1, 1); 

            const yLine = -2.8; 
            const pts = [
                new THREE.Vector3(2.3, 0, Z_FRONT),    
                new THREE.Vector3(4.0, yLine, Z_FRONT), 
                new THREE.Vector3(10.0, yLine, Z_FRONT) 
            ];
            arrowLine.geometry.setFromPoints(pts);
            arrowLine.visible = true;
        }

        window.addEventListener('pointermove', e => {
            if(isStockDragging) { e.preventDefault(); stockDiv.scrollLeft = stockScrollLeft - (e.pageX - stockDiv.offsetLeft)*1.5; return; }
            if (state.draggingSplit) { const pct = (e.clientX/window.innerWidth)*100; if(pct>10 && pct<90){ state.splitPercent=pct; paneTree.style.flex=`0 0 ${pct}%`; resizeRenderer(); } return; }
            
            const treeW = window.innerWidth * (state.splitPercent / 100);
            const isTree = e.clientX < treeW;
            state.activePane = isTree ? 'tree' : 'file';
            state.mouse.x = isTree ? (e.clientX/treeW)*2-1 : ((e.clientX-treeW)/(window.innerWidth-treeW))*2-1;
            state.mouse.y = -(e.clientY/window.innerHeight)*2+1;

            if (e.buttons === 1 && !state.dragging) {
                if(e.target.closest('#stock')) return;
                const dx = e.clientX - state.lastMouse.x; const dy = e.clientY - state.lastMouse.y;
                if(isTree) { state.targetTreeScrollX -= dx*0.2; state.targetTreeScrollZ -= dy*0.5; }
                else {
                    if(state.viewMode==='F1') { state.targetRotY -= dx*0.005; state.targetPosY -= dy*0.1; }
                    else if(state.viewMode==='F2') { state.targetPosX += dx*0.05; state.targetRotX -= dy*0.005; }
                    else if(state.viewMode==='F3') { state.targetRotY -= dx*0.005; state.targetPosY -= dy*0.2; }
                    else if(state.viewMode==='F4') { state.targetPosX += dx*0.1; state.targetPosZ += dy*0.1; }
                }
            } else if (!state.dragging) {
                const raycaster = new THREE.Raycaster();
                if (isTree) {
                    raycaster.setFromCamera(state.mouse, camTree);
                    const hits = raycaster.intersectObjects(groupTree.children, true);
                    let hitNode = null; if(hits.length>0) { let obj=hits[0].object; while(obj&&!obj.userData.isNode)obj=obj.parent; hitNode=obj; }
                    if (hitNode && state.hovered !== hitNode) { sfx.play('hover'); state.hovered = hitNode; document.body.style.cursor='pointer'; }
                    else if (!hitNode) { state.hovered = null; document.body.style.cursor='default'; }
                } else {
                    raycaster.setFromCamera(state.mouse, camFile);
                    const hits = raycaster.intersectObjects(state.fileObjects, true);
                    let hitObj = null; if(hits.length>0) { let obj=hits[0].object; while(obj&&!obj.userData.isFileObj)obj=obj.parent; hitObj=obj; }
                    if(hitObj) {
                        if (state.hovered !== hitObj) sfx.play('hover');
                        state.hovered = hitObj; document.body.style.cursor='pointer';
                        updateOverlays(hitObj);
                    } else {
                        state.hovered = null; document.body.style.cursor='default';
                        if(!state.selected) hideOverlays();
                        else updateOverlays(state.selected); 
                    }
                }
            }
            state.lastMouse.set(e.clientX, e.clientY);
        });

        window.addEventListener('pointerdown', e => {
            sfx.init(); if(e.target.closest('.ctx-menu')||e.target.closest('.stock-item')||e.target.closest('.stock-tab-item')||e.target.closest('.page-btn')||e.target.closest('#stock')||e.target.closest('#tree-header')) return;
            closeAllMenus(); state.dragStart.set(e.clientX, e.clientY);
            // Click handling for Tree is done in mkNode onclick
            if(state.activePane === 'tree') {
                if(state.hovered) selectNode(state.hovered.userData.node);
            }
            if(state.activePane === 'file') {
                if(state.hovered) state.dragging = { mesh: state.hovered, isMove: false };
                else { state.selected = null; state.fileObjects.forEach(o=>{if(o.userData.isFolder)o.material.color.setHex(CONFIG.colFolderIdle);}); hideOverlays(); }
            }
        });
        
        window.addEventListener('dblclick', e => {
            // Tree 3D DblClick
            if(state.activePane === 'tree' && state.treeMode === '3D') {
                const raycaster = new THREE.Raycaster();
                raycaster.setFromCamera(state.mouse, camTree);
                const hits = raycaster.intersectObjects(groupTree.children, true);
                if(hits.length > 0) {
                    let obj = hits[0].object;
                    while(obj && !obj.userData.isNode) obj = obj.parent;
                    if(obj) {
                        selectNode(obj.userData.node);
                        sfx.play('click');
                    }
                }
            }
            // File View DblClick
            else if(state.selected && state.selected.userData.isFileObj) {
                 const h = state.selected.userData.handle;
                 if(h.kind==='directory') {
                      if(state.activeNode) {
                          const child = state.activeNode.children.find(c => c.handle.name === h.name);
                          if(child) selectNode(child);
                          else selectNode(new TreeNode(h, state.activeNode));
                      }
                 } else openPreview(h);
             }
        });

        window.addEventListener('wheel', e => {
            if(state.activePane === 'tree') state.targetTreeScrollZ += e.deltaY*0.2;
            else {
                if(state.viewMode==='F1'||state.viewMode==='F3') state.targetRotY += e.deltaY*0.001;
                else if(state.viewMode==='F2') state.targetRotX += e.deltaY*0.001;
                else if(state.viewMode==='F4') state.targetPosZ -= e.deltaY*0.1;
                if(state.viewMode==='F3'||state.viewMode==='F1') state.targetPosY += e.deltaY*0.05;
            }
        });

        window.addEventListener('keydown', e => { if(e.key.startsWith('F') && ['F1','F2','F3','F4'].includes(e.key)) { e.preventDefault(); setMode(e.key, state.currentAllEntries.slice((state.currentPage-1)*CONFIG.itemsPerPage, state.currentPage*CONFIG.itemsPerPage)); } });

        function animate() {
            requestAnimationFrame(animate);

            state.treeScrollZ += (state.targetTreeScrollZ - state.treeScrollZ)*0.1; state.treeScrollX += (state.targetTreeScrollX - state.treeScrollX)*0.1;
            camTree.position.z = state.treeScrollZ; camTree.position.x = state.treeScrollX; camTree.position.y = CONFIG.treeCamOffset.y; camTree.lookAt(state.treeScrollX, 0, state.treeScrollZ - 100);

            state.currentRotY += (state.targetRotY - state.currentRotY)*0.1; state.currentPosY += (state.targetPosY - state.currentPosY)*0.1;
            state.currentRotX += (state.targetRotX - state.currentRotX)*0.1; state.currentPosX += (state.targetPosX - state.currentPosX)*0.1; state.currentPosZ += (state.targetPosZ - state.currentPosZ)*0.1;

            if(state.viewMode === 'F1' || state.viewMode === 'F3') { groupFile.rotation.y = state.currentRotY; groupFile.position.y = state.currentPosY; groupFile.rotation.x = 0; groupFile.position.x = 0; groupFile.position.z = 0; }
            else if (state.viewMode === 'F2') { groupFile.rotation.x = state.currentRotX; groupFile.position.x = state.currentPosX; groupFile.rotation.y = 0; groupFile.position.y = 0; groupFile.position.z = 0; }
            else if (state.viewMode === 'F4') { groupFile.rotation.set(state.currentRotX, 0, 0); groupFile.position.set(state.currentPosX, state.currentPosY, state.currentPosZ); }

            starGroup.rotation.y = state.currentRotY * 0.2 + Date.now()*0.00005; starGroup.position.y = state.currentPosY * 0.5;

            [reticleFile, reticleTree].forEach(grp => { if(grp.visible) grp.userData.layers.forEach(l => l.rotation.z += l.userData.speed * l.userData.dir); });

            if(state.hovered || state.selected) {
                 magMesh.lookAt(camFile.position);
                 infoMesh.lookAt(camFile.position);
            }

            const width = window.innerWidth; const height = window.innerHeight;
            const treeW = width * (state.splitPercent / 100); const leftW = Math.floor(treeW);
            renderer.setViewport(0, 0, leftW, height); renderer.setScissor(0, 0, leftW, height); renderer.render(sceneTree, camTree);
            renderer.setViewport(leftW, 0, width-leftW, height); renderer.setScissor(leftW, 0, width-leftW, height); renderer.render(sceneFile, camFile);
        }
        animate();

        // --- UI & Context Menu ---
        const ctxObj = document.getElementById('ctx-obj'), ctxBg = document.getElementById('ctx-bg'), ctxTab = document.getElementById('ctx-tab');
        function closeAllMenus() { document.querySelectorAll('.ctx-menu').forEach(m => { if(m.style.display !== 'none' && !m.classList.contains('closing')) { m.classList.remove('opening'); m.classList.add('closing'); setTimeout(() => { m.style.display = 'none'; m.classList.remove('closing'); }, 400); } }); }
        function showMenu(el, x, y) { el.style.display='flex'; el.classList.remove('closing'); el.classList.add('opening'); if(x+200>window.innerWidth) x-=200; if(y+250>window.innerHeight) y-=250; el.style.left=x+'px'; el.style.top=y+'px'; }

        window.addEventListener('contextmenu', e => {
            e.preventDefault(); 
            if(e.target.closest('.stock-tab-item')) { const idx = e.target.closest('.stock-tab-item').dataset.idx; if (idx !== undefined) { state.tabContextTargetIdx = parseInt(idx); showMenu(ctxTab, e.clientX, e.clientY); } return; }
            if(state.activePane === 'tree' && state.hovered) { 
                state.contextTarget = state.hovered.userData.node.handle; 
                document.getElementById('cm-expand').style.display='flex'; 
                document.getElementById('cm-collapse').style.display='flex'; 
                document.getElementById('cm-add-stock').style.display = 'flex'; 
                document.getElementById('cm-add-stock').innerText = 'Add Folder to Stock';
                showMenu(ctxObj, e.clientX, e.clientY); 
                return; 
            }
            if(state.activePane === 'file' && state.hovered) { 
                state.contextTarget = state.hovered.userData.handle; 
                handleFileSelection(state.hovered); 
                document.getElementById('cm-expand').style.display='none'; 
                document.getElementById('cm-collapse').style.display='none'; 
                if(state.contextTarget.kind === 'directory') {
                    document.getElementById('cm-add-stock').style.display = 'flex'; 
                    document.getElementById('cm-add-stock').innerText = 'Add Folder to Stock';
                } else {
                    document.getElementById('cm-add-stock').style.display = 'none'; 
                }
                showMenu(ctxObj, e.clientX, e.clientY); 
            }
            else { state.contextTarget = null; showMenu(ctxBg, e.clientX, e.clientY); }
        });

        document.getElementById('cm-expand').onclick = () => { if(state.activePane==='tree') { const node = state.treeNodes.find(n=>n.handle===state.contextTarget); if(node) selectNode(node, true); } closeAllMenus(); };
        document.getElementById('cm-collapse').onclick = () => { if(state.activePane==='tree') { const node = state.treeNodes.find(n=>n.handle===state.contextTarget); if(node) { node.expanded=false; updateTreeLayout(); } } closeAllMenus(); };
        document.getElementById('cm-open').onclick = () => { const t = state.contextTarget; if(t) { if(t.kind==='directory') loadFilesForPane(t); else openPreview(t); } closeAllMenus(); };
        document.getElementById('cm-up').onclick = () => { if(state.activeNode && state.activeNode.parent) selectNode(state.activeNode.parent); closeAllMenus(); };
        document.getElementById('cm-refresh').onclick = () => { reloadCurrentLevel(); closeAllMenus(); };
        
        document.getElementById('cm-add-stock').onclick = async () => { 
            const t=state.contextTarget; 
            if(t&&t.kind==='directory') { 
                if(state.stockList.length === 0) {
                    try {
                        const stockDirHandle = await state.rootHandle.getDirectoryHandle('STOCK', {create: true});
                        state.stockList.push({handle: stockDirHandle, entries: []});
                        state.activeStockIdx = 0;
                        if(t.move) { await t.move(stockDirHandle); reloadCurrentLevel(); }
                    } catch(e) { console.error(e); }
                } 
                else if(state.stockList.find(s=>s.handle.name===t.name)) { state.activeStockIdx=state.stockList.findIndex(s=>s.handle.name===t.name); } 
                else { state.stockList.push({handle:t, entries:[]}); state.activeStockIdx=state.stockList.length-1; } 
                renderStockTabs(); refreshStock(); document.getElementById('stock').classList.remove('closed'); 
            } 
            closeAllMenus(); 
        };

        document.getElementById('cm-tab-hide').onclick=()=>{ document.getElementById('stock').classList.add('closed'); closeAllMenus(); };
        document.getElementById('cm-tab-cancel').onclick=()=>{ state.stockList.splice(state.tabContextTargetIdx, 1); if(state.stockList.length===0){state.activeStockIdx=-1;document.getElementById('stock').classList.add('closed');} else state.activeStockIdx=Math.max(0, state.stockList.length-1); renderStockTabs(); refreshStock(); closeAllMenus(); };
        document.getElementById('cm-new').onclick = async () => { closeAllMenus(); const n = await customPrompt("NEW FOLDER", "New Folder"); if(n) { await state.currentHandle.getDirectoryHandle(n, {create:true}); reloadCurrentLevel(); } };
        document.getElementById('cm-rename').onclick = async () => { closeAllMenus(); const t = state.contextTarget; if(t) { const n = await customPrompt("RENAME", t.name); if(n && n!==t.name && t.move) { await t.move(n); reloadCurrentLevel(); } } };
        document.getElementById('cm-paste').onclick = async () => { if(state.clipboard && state.clipboard.kind === 'file') { const f = await state.clipboard.getFile(); const h = await state.currentHandle.getFileHandle('Copy_'+f.name, {create:true}); const w = await h.createWritable(); await w.write(f); await w.close(); reloadCurrentLevel(); } closeAllMenus(); };
        document.getElementById('cm-copy').onclick = () => { state.clipboard = state.contextTarget; closeAllMenus(); };
        document.getElementById('cm-delete').onclick = async () => { closeAllMenus(); const t = state.contextTarget; if(t && confirm('Delete?')) { await state.currentHandle.removeEntry(t.name, {recursive:true}); reloadCurrentLevel(); } };

        function renderStockTabs() { const c = document.getElementById('stock-tabs-container'); c.innerHTML = ''; if(state.stockList.length === 0) { c.innerHTML = '<div class="stock-tab-item" style="opacity:0.5; pointer-events:none;">STOCK</div>'; return; } state.stockList.forEach((item, i) => { const div = document.createElement('div'); div.className = 'stock-tab-item'; if(i === state.activeStockIdx) div.classList.add('active'); div.innerText = item.handle.name.toUpperCase(); div.dataset.idx = i; div.onclick = () => { state.activeStockIdx = i; document.getElementById('stock').classList.remove('closed'); renderStockTabs(); refreshStock(); sfx.play('click'); }; c.appendChild(div); }); }
        async function refreshStock() { const s = document.getElementById('stock'); s.innerHTML=''; if(state.activeStockIdx < 0 || !state.stockList[state.activeStockIdx]) { s.innerHTML = '<div style="color:var(--orange); opacity:0.6; pointer-events:none;">[ EMPTY STOCK ]</div>'; return; } const currentStock = state.stockList[state.activeStockIdx]; currentStock.entries = []; try { for await (const entry of currentStock.handle.values()) currentStock.entries.push(entry); if(currentStock.entries.length === 0) s.innerHTML = '<div style="color:var(--orange); opacity:0.6; pointer-events:none;">[ EMPTY FOLDER ]</div>'; else { for (let i=0; i<currentStock.entries.length; i++) { const h = currentStock.entries[i]; const d = document.createElement('div'); d.className = 'stock-item'; d.draggable = true; const thumb = document.createElement('div'); thumb.className = 'stock-thumb'; if(h.kind === 'directory') thumb.innerHTML = '<span style="font-size:40px">📂</span>'; else { const n = h.name.toLowerCase(); if(n.match(/\.(jpg|png|webp|gif)$/)) h.getFile().then(f => { const img=document.createElement('img'); img.src=URL.createObjectURL(f); img.style.maxWidth='100%'; img.style.maxHeight='100%'; thumb.appendChild(img); }); else thumb.innerHTML = '<span style="font-size:30px">📄</span>'; } d.appendChild(thumb); const lbl = document.createElement('div'); lbl.className = 'stock-label'; lbl.innerText = h.name; d.appendChild(lbl); d.ondragstart = e => { e.dataTransfer.setData('stockIndex', i); e.stopPropagation(); }; s.appendChild(d); } } } catch(e) { s.innerHTML = '<div style="color:red">ACCESS ERROR</div>'; } }
        async function moveToStock(handle) { if(state.activeStockIdx < 0 || !state.stockList[state.activeStockIdx]) { alert("No Active Stock Tab!"); return; } const dest = state.stockList[state.activeStockIdx].handle; if(handle.move) { try { await handle.move(dest); refreshStock(); reloadCurrentLevel(); } catch(e){ sfx.play('error'); alert(e.message); } } }
        async function moveFromStock(idx) { if(state.activeStockIdx<0) return; const h = state.stockList[state.activeStockIdx].entries[idx]; if(h && h.move) { try { await h.move(state.currentHandle); refreshStock(); reloadCurrentLevel(); } catch(e){ sfx.play('error'); alert(e.message); } } }
        container.ondragover = e => e.preventDefault(); container.ondrop = async e => { e.preventDefault(); const idx = e.dataTransfer.getData('stockIndex'); if(idx) moveFromStock(parseInt(idx)); };

        const dModal=document.getElementById('dialog'), dInput=document.getElementById('d-input'); let dResolve=null;
        function customPrompt(title, val) { return new Promise(r => { document.getElementById('d-title').innerText=title; dInput.value=val; dResolve=r; dModal.classList.remove('closing'); dModal.classList.add('active','opening'); dInput.focus(); sfx.play('open'); }); }
        function closeDialog(res){ dModal.classList.remove('opening'); dModal.classList.add('closing'); sfx.play('back'); setTimeout(()=>{dModal.classList.remove('active','closing'); if(dResolve) dResolve(res); dResolve=null;},500); }
        document.getElementById('d-ok').onclick=()=>closeDialog(dInput.value); document.getElementById('d-cancel').onclick=()=>closeDialog(null); dInput.onkeydown=e=>{if(e.key==='Enter')closeDialog(dInput.value);if(e.key==='Escape')closeDialog(null);};

        const pModal=document.getElementById('preview'), pBody=document.getElementById('p-body'); let curPH=null;
        async function openPreview(h) { curPH=h; pModal.classList.remove('closing'); pModal.classList.add('active','opening'); sfx.play('open'); document.getElementById('p-title').innerText=h.name; pBody.innerHTML='Loading...'; try { const f = await h.getFile(); const u = URL.createObjectURL(f); if(f.name.match(/\.(png|jpg|webp|gif)$/i)) pBody.innerHTML=`<img src="${u}" style="max-width:100%;max-height:100%">`; else if(f.name.match(/\.(mp4|webm)$/i)) pBody.innerHTML=`<video src="${u}" controls autoplay style="max-width:100%"></video>`; else { const t = await f.text(); pBody.innerHTML=`<textarea id="editor-area" class="hex-view">${t}</textarea>`; document.getElementById('p-save').style.display='block'; } } catch(e){ pBody.innerText="Error"; } }
        document.getElementById('p-close').onclick=()=>{ pModal.classList.remove('opening'); pModal.classList.add('closing'); sfx.play('back'); setTimeout(()=>{pModal.classList.remove('active','closing');},500); };
        document.getElementById('p-save').onclick=async()=>{ const ta=document.getElementById('editor-area'); if(curPH && ta) { try { const w=await curPH.createWritable(); await w.write(ta.value); await w.close(); alert("Saved"); } catch(e){alert("Error");} } };

        document.getElementById('btn-select-root').onclick = async () => { sfx.init(); sfx.play('click'); try { const h = await window.showDirectoryPicker(); state.rootHandle = h; state.treeNodes = []; state.activeNode = null; groupTree.children.slice().forEach(c => { if(c.userData.isNode || c.userData.isLine) groupTree.remove(c); }); const root = new TreeNode(h); root.mesh = createTreeMesh(root); groupTree.add(root.mesh); state.treeNodes.push(root); await selectNode(root); updateTreeLayout(); } catch(e) { console.error(e); } };
    </script>
</body>
</html>

いいなと思ったら応援しよう!

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
SF映画っぽいエクスプローラ「Exp3D 1.5」|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word

mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1
mmMwWLliI0fiflO&1