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


画像
画像

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


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Exp3D 1.4.4</title>
    <style>
        :root {
            --cyan: #00ffff;
            --magenta: #ff00ff;
            --orange: #ffaa00; /* STOCK color */
            --yellow: #ffee00;
            --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; }

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

        /* Sidebar */
        #sidebar {
            position: absolute; top: 0; left: 0; bottom: 0; width: 280px;
            background: var(--panel); border-right: 1px solid var(--cyan);
            display: flex; flex-direction: column; z-index: 50; backdrop-filter: blur(10px);
            transition: transform 0.3s; transform: translateX(0);
        }
        #sidebar.closed { transform: translateX(-280px); }
        #sb-tab {
            position: absolute; top: 50%; right: -30px; width: 30px; height: 100px; margin-top: -50px;
            background: var(--panel); border: 1px solid var(--cyan); border-left: none;
            display: flex; align-items: center; justify-content: center; cursor: pointer;
            writing-mode: vertical-rl; color: var(--cyan); font-weight:bold;
            border-radius: 0 10px 10px 0;
        }
        #sb-header { padding: 15px; color: var(--cyan); border-bottom: 1px solid rgba(0,255,255,0.3); font-weight: bold; letter-spacing: 2px; }
        #tree-container { flex: 1; overflow-y: auto; padding: 10px; }
        .tree-node { display: flex; align-items: center; padding: 6px; cursor: pointer; color: #888; transition:0.2s; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-size: 14px; }
        .tree-node:hover { color: #fff; background: rgba(0,255,255,0.1); }
        .icon-tree { margin-right: 8px; display: inline-block; }

        /* Main View */
        #main { flex: 1; position: relative; overflow: hidden; }
        #canvas-wrap { width: 100%; height: 100%; position: absolute; }
        #hud { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
        #hud-canvas { width: 100%; height: 100%; display: block; }
        
        #mode-info {
            position: absolute; top: 20px; 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;
        }

        /* Async 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: 100;
            display: none;
        }
        #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 (Old Shelf) */
        #stock {
            position: absolute; bottom: 0; left: 0; right: 0; height: 160px;
            background: linear-gradient(to top, rgba(20,10,0,0.95), transparent); /* Orange tint */
            border-top: 2px solid var(--orange); display: flex; align-items: center; padding: 0 50px; gap: 20px;
            z-index: 40; transition: bottom 0.3s; overflow-x: auto;
        }
        #stock.closed { bottom: -160px; }
        #stock-tab {
            position: absolute; bottom: 160px; left: 50%; transform: translateX(-50%);
            background: var(--orange); color: #fff; padding: 2px 30px; font-weight: bold;
            cursor: pointer; clip-path: polygon(15% 0, 85% 0, 100% 100%, 0% 100%); z-index: 41; transition: bottom 0.3s;
        }
        #stock.closed + #stock-tab { bottom: 0; }
        .stock-item {
            width: 100px; height: 120px; background: rgba(0,0,0,0.6); border: 1px solid var(--orange);
            display: flex; flex-direction: column; align-items: center; justify-content: center;
            cursor: grab; color: var(--orange); flex-shrink: 0; position: relative;
        }

        /* 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; animation: menuSlide 0.15s ease-out; padding: 5px 0;
        }
        @keyframes menuSlide { 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; }
        .ctx-item:hover { background: var(--cyan); color: #000; }
        .ctx-item:last-child { border-bottom: none; }

        /* Preview (Acrobatic Backflip) */
        #preview {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9);
            display: flex; justify-content: center; align-items: center; z-index: 300;
            opacity: 0; pointer-events: none; visibility: hidden; transition: opacity 0.3s;
            perspective: 1500px;
        }
        #preview.active { opacity: 1; pointer-events: auto; visibility: visible; }
        
        #p-box { 
            width: 90%; height: 90%; 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;
            /* Default State (Closed) */
            transform: translateZ(-1000px) scale(0.1) rotateX(1080deg);
            opacity: 0;
        }
        
        /* Opening Animation: Enlarge & Rotate Forward */
        #preview.opening #p-box {
            animation: animOpen 1.2s cubic-bezier(0.19, 1, 0.22, 1) forwards;
        }
        @keyframes animOpen {
            0% { transform: translateZ(-1000px) scale(0.1) rotateX(1080deg); opacity: 0; }
            100% { transform: translateZ(0) scale(1) rotateX(0deg); opacity: 1; }
        }

        /* Closing Animation: Shrink & Rotate Backward (Reverse) */
        #preview.closing #p-box {
            animation: animClose 0.8s cubic-bezier(0.19, 1, 0.22, 1) forwards;
        }
        @keyframes animClose {
            0% { transform: translateZ(0) scale(1) rotateX(0deg); opacity: 1; }
            100% { transform: translateZ(-1000px) scale(0.1) rotateX(-1080deg); opacity: 0; }
        }

        #p-head { padding: 10px; background: rgba(0,255,255,0.15); color: var(--cyan); display: flex; justify-content: space-between; align-items: center; }
        #p-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; }
        .hex-view { width: 100%; height: 100%; padding: 15px; color: #0f0; background: #000; border: none; resize: none; font-family: 'Courier New', monospace; }

        .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: 9999; 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="sidebar">
            <div id="sb-header">SYSTEM // NAV</div>
            <div style="padding:10px;"><button id="btn-mount" class="p-btn" style="width:100%">MOUNT DRIVE</button></div>
            <div id="tree-container"></div>
            <div id="sb-tab">TREE</div>
        </div>

        <div id="main">
            <div id="canvas-wrap"></div>
            <div id="hud"><canvas id="hud-canvas"></canvas></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="stock">
                <div style="color:var(--orange); opacity:0.6;">[ DRAG FILES HERE ]</div>
            </div>
            <div id="stock-tab">STOCK</div>

            <div id="ctx-obj" class="ctx-menu">
                <div class="ctx-item" id="cm-open">Open</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-delete" style="color:#ff5555">Delete</div>
                <div class="ctx-item" id="cm-prop">Properties</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>
    </div>

    <div id="preview">
        <div id="p-box">
            <div id="p-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"></div>
        </div>
    </div>
    <div class="scanlines"></div>

    <script type="module">
        import * as THREE from 'three';
        import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
        import { EffectComposer } from 'three/addons/postprocessing/EffectComposer.js';
        import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
        import { UnrealBloomPass } from 'three/addons/postprocessing/UnrealBloomPass.js';

        const CONFIG = {
            colFolder: 0xffee00, colFile: 0x00ffff, colCursor: 0xffee00, colStock: 0xffaa00
        };
        const state = {
            rootHandle: null, currentHandle: null, currentPath: [],
            objects: [], stockData: [], dragging: null, hovered: null, selected: null,
            clipboard: null, currentEntries: [],
            viewMode: 'F1', 
            targetRotX: 0, currentRotX: 0,
            targetRotY: 0, currentRotY: 0,
            targetPosX: 0, currentPosX: 0,
            targetPosY: 0, currentPosY: 0,
            isRotating: false, lastMouse: new THREE.Vector2(),
            isTransitioning: false, contextTarget: null,
            dragStart: new THREE.Vector2(), isLoading: false,
            animPopup: [] // Animation handlers for popup text
        };

        // --- Sound ---
        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);}
            }
        };

        // --- Three.js ---
        const container = document.getElementById('canvas-wrap');
        const scene = new THREE.Scene();
        scene.fog = new THREE.FogExp2(0x020205, 0.01);

        const camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 0.1, 1000);
        camera.position.set(0, 0, 0.1);

        const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
        renderer.setSize(container.clientWidth, container.clientHeight);
        renderer.setPixelRatio(window.devicePixelRatio);
        container.appendChild(renderer.domElement);

        scene.add(new THREE.AmbientLight(0xffffff, 1.5));
        const dl = new THREE.DirectionalLight(0xffffff, 2);
        dl.position.set(0, 50, 0);
        scene.add(dl);
        
        // Background
        const starGroup = new THREE.Group();
        const starsGeo = new THREE.BufferGeometry();
        const starPos = new Float32Array(5000 * 3);
        for(let i=0; i<15000; i++) starPos[i] = (Math.random()-0.5)*500;
        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);
        scene.add(starGroup);

        const composer = new EffectComposer(renderer);
        composer.addPass(new RenderPass(scene, camera));
        const bloom = new UnrealBloomPass(new THREE.Vector2(container.clientWidth, container.clientHeight), 2.0, 0.3, 0.85);
        composer.addPass(bloom);

        const orbitControls = new OrbitControls(camera, renderer.domElement);
        orbitControls.enableDamping = true;
        orbitControls.enablePan = false; orbitControls.enableRotate = false; orbitControls.enableZoom = false; 
        orbitControls.target.set(0,0,-1);

        const filesGroup = new THREE.Group();
        scene.add(filesGroup);

        // --- Cyber Cursor (Sharp & Slim) ---
        const reticleGroup = new THREE.Group();
        scene.add(reticleGroup);
        reticleGroup.visible = false;

        function createSegmentedRing(rIn, rOut, count, speed, dir) {
            const g = new THREE.Group();
            const angle = (Math.PI * 2) / count;
            const gap = 0.2; // 太めの隙間 (Half width request -> Sharp)
            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 mat = new THREE.MeshBasicMaterial({ color: CONFIG.colCursor, transparent: true, opacity: 0.8, side: THREE.DoubleSide });
                g.add(new THREE.Mesh(geo, mat));
            }
            g.userData = { speed: speed, dir: dir };
            return g;
        }

        // Slimmer rings
        // Inner: Small, Fast
        const r1 = createSegmentedRing(1.4, 1.45, 3, 0.08, 1); 
        reticleGroup.add(r1);

        // Middle: Medium, Reverse
        const r2 = createSegmentedRing(1.55, 1.6, 4, 0.05, -1);
        reticleGroup.add(r2);

        // Outer: Large, Slow
        const r3 = createSegmentedRing(1.7, 1.75, 6, 0.02, 1);
        reticleGroup.add(r3);

        reticleGroup.userData = { layers: [r1, r2, r3] };

        // --- HUD ---
        const hudC = document.getElementById('hud-canvas');
        const hudX = hudC.getContext('2d');
        function resizeHUD() {
            const dpr = window.devicePixelRatio || 1;
            hudC.width = container.clientWidth * dpr; hudC.height = container.clientHeight * dpr;
            hudX.scale(dpr, dpr);
        }
        window.onresize = () => {
            camera.aspect=container.clientWidth/container.clientHeight; camera.updateProjectionMatrix();
            renderer.setSize(container.clientWidth, container.clientHeight);
            composer.setSize(container.clientWidth, container.clientHeight);
            resizeHUD();
        }; 
        resizeHUD();

        // --- Logic ---
        document.getElementById('btn-mount').onclick = async () => {
            sfx.init(); sfx.play('click');
            try {
                const h = await window.showDirectoryPicker();
                state.rootHandle = h;
                enterFolder(h, false); 
                updateTree(h);
            } catch(e) {}
        };

        window.addEventListener('keydown', e => {
            if(e.key === 'F1') { e.preventDefault(); setMode('F1'); }
            if(e.key === 'F2') { e.preventDefault(); setMode('F2'); }
            if(e.key === 'F3') { e.preventDefault(); setMode('F3'); } // Spiral
        });

        function setMode(mode) {
            state.viewMode = mode;
            let label = "VERTICAL";
            if(mode==='F2') label="HORIZONTAL";
            if(mode==='F3') label="SPIRAL";
            document.getElementById('mode-info').innerText = `MODE: ${mode} (${label})`;
            sfx.play('click');
            rebuildCylinder();
        }

        // --- Async Loader ---
        document.getElementById('btn-cancel-load').onclick = () => {
            state.isLoading = false; 
            document.getElementById('loader').classList.remove('active');
        };

        // フォルダ移動:非同期ロードを待ってからアニメーション完了させる
        async function enterFolder(handle, animate=true) {
            state.currentHandle = handle;
            
            // 履歴追加(重複チェック)
            if(state.currentPath.length === 0 || state.currentPath[state.currentPath.length-1] !== handle) {
                state.currentPath.push(handle);
            }
            
            state.isLoading = true;
            document.getElementById('loader').classList.add('active');
            
            if(!animate) {
                // 初期ロードやリフレッシュ時は即時クリア
                state.objects.forEach(o => filesGroup.remove(o));
                state.objects = [];
            }
            reticleGroup.visible = false; state.selected = null; updatePopup(null);
            
            const entries = [];
            try {
                for await (const e of handle.values()) {
                    if (!state.isLoading) break;
                    entries.push(e);
                    if (entries.length % 20 === 0) await new Promise(r => setTimeout(r, 0));
                }
            } catch(e) {}

            document.getElementById('loader').classList.remove('active');
            state.isLoading = false;
            state.currentEntries = entries;
            
            if(animate) {
                // アニメーション付き再構築は animateTransition 内で行うのでここでは呼ばない
                // 呼び出し元が animateTransition('in', ...) などを呼ぶ想定
                rebuildCylinder(); // フォールバック
            } else {
                rebuildCylinder();
            }
        }

        // --- Up Directory (修正版) ---
        async function goUp() {
            if(state.currentPath.length > 1) {
                // 現在(最後尾)を取り除く
                state.currentPath.pop();
                const parent = state.currentPath[state.currentPath.length-1];
                
                // アニメーション開始 -> データロード待機 -> 再構築
                animateTransition('out', async () => {
                    // ここでデータをロードするが、enterFolderのanimate=falseを使う
                    // enterFolder内で currentPath.push されるのを防ぐため、一時的にフラグ制御するか
                    // 単に enterFolder を呼ぶと push されるので、手動で path 調整が必要だが
                    // 上記 enterFolder で「末尾と同じならpushしない」ようにしたので大丈夫。
                    await enterFolder(parent, false); 
                });
            }
        }

        // --- Transitions (SCATTER / GATHER) ---
        function animateTransition(dir, asyncLoadCallback) {
            if (state.isTransitioning) return;
            state.isTransitioning = true;
            sfx.play(dir==='in'?'open':'back');
            
            const startT = Date.now();
            const duration = 800; 

            const oldObjects = [...state.objects];
            oldObjects.forEach(o => {
                o.userData.startPos = o.position.clone();
                const v = new THREE.Vector3(o.position.x, o.position.y, 0).normalize();
                if(v.lengthSq()===0) v.set(1,0,0);
                o.userData.scatterDir = v;
            });

            // Phase 1: Exit Old Objects
            function loop1() {
                const p = Math.min((Date.now() - startT) / duration, 1);
                const ease = p * p; 
                
                oldObjects.forEach(o => {
                    const sp = o.userData.startPos;
                    const sd = o.userData.scatterDir;
                    
                    if (dir === 'in') { // Scatter Outwards (Zoom In)
                        o.position.z = sp.z + (300 * ease);
                        o.position.x = sp.x + (sd.x * 100 * ease);
                        o.position.y = sp.y + (sd.y * 100 * ease);
                    } else { // Implode Inwards (Zoom Out)
                        o.position.z = sp.z - (400 * ease);
                        o.position.x = sp.x * (1 - ease);
                        o.position.y = sp.y * (1 - ease);
                    }
                    if(o.material) o.material.opacity = 0.3 * (1 - p);
                    o.children.forEach(c => { if(c.material) c.material.opacity = (1 - p); });
                });

                if (p < 1) {
                    requestAnimationFrame(loop1);
                } else {
                    // Phase 2: Load Data & Rebuild
                    oldObjects.forEach(o => filesGroup.remove(o));
                    state.objects = [];
                    
                    // 非同期ロードを実行
                    if (asyncLoadCallback) {
                        asyncLoadCallback().then(() => {
                            startPhase3();
                        });
                    } else {
                        startPhase3();
                    }
                }
            }
            loop1();

            function startPhase3() {
                // 新しいオブジェクトは enterFolder(..., false) -> rebuildCylinder() で既に配置されているはず
                // 配置されたオブジェクトをアニメーション開始位置へ移動
                const newObjects = state.objects;
                newObjects.forEach(o => {
                    o.userData.targetPos = o.position.clone();
                    const v = new THREE.Vector3(o.position.x, o.position.y, 0).normalize();
                    if(v.lengthSq()===0) v.set(1,0,0);
                    
                    if (dir === 'in') { // Appear from Center (Zoom In)
                        o.position.z = -400;
                        o.position.x = 0; o.position.y = 0;
                    } else { // Gather from Outside (Zoom Out)
                        o.position.z = 300;
                        o.position.x = o.userData.targetPos.x + (v.x * 100);
                        o.position.y = o.userData.targetPos.y + (v.y * 100);
                    }
                    if(o.material) o.material.opacity = 0;
                    o.children.forEach(c => { if(c.material) c.material.opacity = 0; });
                });

                const startT2 = Date.now();
                function loop2() {
                    const p2 = Math.min((Date.now() - startT2) / duration, 1);
                    const ease2 = 1 - Math.pow(1 - p2, 3); 
                    
                    newObjects.forEach(o => {
                        const tp = o.userData.targetPos;
                        if (dir === 'in') {
                            o.position.z = -400 + (tp.z - (-400)) * ease2;
                            o.position.x = tp.x * ease2;
                            o.position.y = tp.y * ease2;
                        } else {
                            const v = new THREE.Vector3(tp.x, tp.y, 0).normalize();
                            if(v.lengthSq()===0) v.set(1,0,0);
                            o.position.z = 300 + (tp.z - 300) * ease2;
                            o.position.x = (tp.x + v.x * 100) + (tp.x - (tp.x + v.x * 100)) * ease2;
                            o.position.y = (tp.y + v.y * 100) + (tp.y - (tp.y + v.y * 100)) * ease2;
                        }
                        if(o.material) o.material.opacity = 0.3 * p2;
                        o.children.forEach(c => { if(c.material) c.material.opacity = p2; });
                    });

                    if(p2 < 1) requestAnimationFrame(loop2);
                    else {
                        state.isTransitioning = false;
                        newObjects.forEach(o => {
                            o.position.copy(o.userData.targetPos);
                            if(o.material) o.material.opacity = 0.3;
                            o.children.forEach(c => { if(c.material) c.material.opacity = 1; });
                        });
                    }
                }
                loop2();
            }
        }

        // --- Layout Rebuild (Spiral Mode Added) ---
        function rebuildCylinder() {
            state.objects.forEach(o => filesGroup.remove(o));
            state.objects = [];
            const entries = state.currentEntries || [];
            const count = entries.length;
            const radius = 22; 

            if(state.viewMode === 'F1') {
                // Vertical Cylinder
                const spacing = 5.0; 
                const circumference = 2 * Math.PI * radius;
                const maxCols = Math.floor(circumference / spacing);
                const cols = Math.max(1, Math.min(count, maxCols)); 
                const rowHeight = 7.0; 
                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(state.viewMode === 'F2') {
                // Horizontal Cylinder
                const spacing = 16.0;
                const circumference = 2 * Math.PI * radius;
                const maxRows = Math.floor(circumference / spacing); 
                const rows = Math.max(1, Math.min(count, maxRows));
                const colWidth = 16.0; 
                entries.forEach((e, i) => {
                    const ringIndex = i % rows;
                    const colIndex = Math.floor(i / rows);
                    const angle = ringIndex * ((Math.PI * 2) / Math.max(rows, 10));
                    const y = radius * Math.sin(angle);
                    const z = radius * Math.cos(angle);
                    const x = (colIndex * colWidth) - (Math.floor(count/rows)*colWidth)/2;
                    createMonolith(e, x, y, z, angle, i, 'F2');
                });
            } else if(state.viewMode === 'F3') {
                // Spiral Mode (Spiral Staircase)
                const stepY = 2.0;
                const angleStep = 0.3; 
                const totalH = count * stepY;
                entries.forEach((e, i) => {
                    const angle = i * angleStep;
                    const x = radius * Math.sin(angle);
                    const z = radius * Math.cos(angle);
                    const y = (i * stepY) - (totalH / 2);
                    createMonolith(e, x, y, z, angle, i, 'F3');
                });
            }
            
            if(!state.isTransitioning) {
                filesGroup.scale.set(1,1,1);
                filesGroup.rotation.set(0,0,0);
                filesGroup.position.set(0,0,0);
            }
        }

        function drawStrokeText(ctx, txt, x, y, size, align='left') {
            ctx.font = `bold ${size}px "Segoe UI"`; // Font size larger
            ctx.textAlign = align; ctx.textBaseline = 'middle';
            ctx.lineWidth = 6; ctx.strokeStyle = '#000000'; ctx.strokeText(txt, x, y);
            ctx.fillStyle = '#ffffff'; ctx.fillText(txt, x, y);
        }

        function createMonolith(handle, x, y, z, angle, index, mode) {
            const isF = handle.kind === 'directory';
            const col = isF ? CONFIG.colFolder : 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(0, y, 0); 
            } else {
                mesh.lookAt(x, 0, 0); 
                mesh.rotateZ(-Math.PI/2); 
            }

            mesh.userData = { handle, isFolder:isF, isRoot:true, thumbTex:null };

            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);

            // Extension / Icon (Moved further back)
            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);
            drawStrokeText(ctx, isF ? 'DIR' : ext, 128, 128, 100, 'center'); // Font size up
            
            const defTex = new THREE.CanvasTexture(cvs);
            const extSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: defTex }));
            extSprite.scale.set(2, 2, 1);
            // Positioned behind the panel (closer to cylinder center)
            extSprite.position.z = -1.5; 
            mesh.add(extSprite);

            // Thumbnail (On top of panel)
            const tCvs = document.createElement('canvas'); tCvs.width=256; tCvs.height=256;
            const thumbTex = new THREE.CanvasTexture(tCvs); // Empty initial
            const thumbSprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: thumbTex, opacity: 0 })); // Hidden initially
            thumbSprite.scale.set(2, 2, 1);
            thumbSprite.position.z = 0.2;
            mesh.add(thumbSprite);

            if (!isF) {
                const n = handle.name.toLowerCase();
                const loadT = (t) => {
                    // Update main mesh to show thumbnail with glow
                    mesh.material = new THREE.MeshBasicMaterial({map:t, color:0xffffff}); 
                    // Hide extension sprite? No, keep it visible in background as requested
                    // extSprite.visible = true; 
                };

                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(()=>{});
                    });
                }
            }

            filesGroup.add(mesh);
            state.objects.push(mesh);
        }

        // --- Interaction ---
        const raycaster = new THREE.Raycaster();
        const mouse = new THREE.Vector2();
        let popupMesh = null;
        let infoMesh = null; // New detailed info mesh

        function getRoot(o) { while(o){if(o.userData?.isRoot)return o; o=o.parent;} return null; }
        function updateMouse(e) {
            const r = renderer.domElement.getBoundingClientRect();
            mouse.x = ((e.clientX-r.left)/r.width)*2-1;
            mouse.y = -((e.clientY-r.top)/r.height)*2+1;
        }

        function updatePopup(target) {
            if(popupMesh) { filesGroup.remove(popupMesh); popupMesh=null; }
            if(infoMesh) { filesGroup.remove(infoMesh); infoMesh=null; }
            state.animPopup = [];

            if(!target) return;

            // 1. Zoomed Thumbnail Popup
            const geo = new THREE.PlaneGeometry(6, 8); 
            const mat = new THREE.MeshBasicMaterial({
                map: target.material.map || null, // Use current thumb
                color: target.material.map ? 0xffffff : (target.userData.isFolder?CONFIG.colFolder:CONFIG.colFile),
                transparent: true, opacity: 0.95, side: THREE.DoubleSide
            });
            popupMesh = new THREE.Mesh(geo, mat);
            popupMesh.position.copy(target.position);
            
            // Calc direction to center for orientation
            const v = target.position.clone().setY(0).normalize();
            if(state.viewMode==='F2') v.set(0,1,0); // approximate
            
            // Move towards camera (outwards from cylinder)
            const forward = target.position.clone().sub(new THREE.Vector3(0, target.position.y, 0)).normalize();
            if(state.viewMode==='F2') forward.set(0,0,1).applyAxisAngle(new THREE.Vector3(1,0,0), target.rotation.x);

            // Simple popup position: scaled outward
            if(state.viewMode === 'F1' || state.viewMode === 'F3') {
               const dir = new THREE.Vector3(target.position.x, 0, target.position.z).normalize();
               popupMesh.position.add(dir.multiplyScalar(5));
               popupMesh.lookAt(0, target.position.y, 0);
            } else {
               const dir = new THREE.Vector3(target.position.x, target.position.y, 0).normalize();
               popupMesh.position.add(dir.multiplyScalar(5));
               popupMesh.lookAt(target.position.x, target.position.y, 0); // rough
               popupMesh.rotation.z -= Math.PI/2;
            }
            
            popupMesh.add(new THREE.LineSegments(new THREE.EdgesGeometry(geo), new THREE.LineBasicMaterial({color:0xffffff})));
            filesGroup.add(popupMesh);
            reticleGroup.scale.set(3,3,1);

            // 2. Info Text Mesh (Right side of popup)
            const iCvs = document.createElement('canvas'); iCvs.width=512; iCvs.height=512;
            const iCtx = iCvs.getContext('2d');
            const iTex = new THREE.CanvasTexture(iCvs);
            const iGeo = new THREE.PlaneGeometry(8, 8);
            const iMat = new THREE.MeshBasicMaterial({ map: iTex, transparent: true, opacity: 1, side: THREE.DoubleSide });
            infoMesh = new THREE.Mesh(iGeo, iMat);
            
            // Position to the right of popup
            infoMesh.position.copy(popupMesh.position);
            const right = new THREE.Vector3(1,0,0).applyQuaternion(popupMesh.quaternion);
            infoMesh.position.add(right.multiplyScalar(7.5)); 
            infoMesh.quaternion.copy(popupMesh.quaternion);

            filesGroup.add(infoMesh);

            // Animation Logic for Text
            const lines = [
                `NAME: ${target.userData.handle.name}`,
                `KIND: ${target.userData.handle.kind.toUpperCase()}`,
                `DATE: ${new Date().toLocaleString()}`,
                `SIZE: Calculating...`
            ];
            
            let lineIdx = 0;
            const drawLines = () => {
                if(!infoMesh) return;
                iCtx.clearRect(0,0,512,512);
                iCtx.font = "bold 40px 'Courier New'";
                iCtx.fillStyle = "#00ffff";
                iCtx.shadowColor="#00ffff"; iCtx.shadowBlur=5;
                
                for(let i=0; i<=lineIdx && i<lines.length; i++) {
                    iCtx.fillText(lines[i], 10, 50 + i*60);
                }
                iTex.needsUpdate = true;
                
                if(lineIdx < lines.length - 1) {
                    lineIdx++;
                    setTimeout(drawLines, 150); // Delay per line
                }
            };
            drawLines();
        }

        window.addEventListener('pointerdown', e => {
            sfx.init();
            if(e.button!==0 || !e.target.closest('canvas')) return;
            updateMouse(e);
            state.dragStart.set(e.clientX, e.clientY);
            
            raycaster.setFromCamera(mouse, camera);
            const hits = raycaster.intersectObjects(state.objects, true);
            document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none');
            
            if(hits.length>0) {
                const t = getRoot(hits[0].object);
                state.selected = t;
                state.dragging = { mesh:t, isMove: false }; 
                orbitControls.enabled = false;
            } else {
                state.selected = null; reticleGroup.visible = false; updatePopup(null);
                state.isRotating = true; state.lastMouse.set(e.clientX, e.clientY);
                document.body.style.cursor = 'grabbing';
            }
        });

        window.addEventListener('pointermove', e => {
            updateMouse(e);
            if(state.isRotating) {
                const deltaX = e.clientX - state.lastMouse.x;
                const deltaY = e.clientY - state.lastMouse.y;
                if(state.viewMode === 'F1' || state.viewMode === 'F3') {
                    state.targetRotY -= deltaX * 0.005; state.targetPosY += deltaY * 0.05; 
                } else {
                    state.targetPosX += deltaX * 0.05; state.targetRotX -= deltaY * 0.005; 
                }
                state.lastMouse.set(e.clientX, e.clientY);
            } else if(state.dragging) {
                if(state.dragStart.distanceTo(new THREE.Vector2(e.clientX, e.clientY)) > 5) state.dragging.isMove = true;
            } else {
                checkHover();
            }
        });

        window.addEventListener('pointerup', (e) => {
            if(state.dragging) {
                if(!state.dragging.isMove) {
                    activateReticle(state.dragging.mesh); updatePopup(state.dragging.mesh); sfx.play('lock');
                } else {
                    const m = state.dragging.mesh;
                    if(e.clientY > window.innerHeight - 160) moveToStock(m);
                }
                state.dragging = null; orbitControls.enabled = true;
            }
            state.isRotating = false; document.body.style.cursor = 'default';
        });

        window.addEventListener('wheel', e => {
            if(e.target.closest('canvas')) {
                if(state.viewMode==='F1' || state.viewMode==='F3') state.targetRotY += e.deltaY * 0.001;
                else state.targetRotX += e.deltaY * 0.001;
            }
        });

        function activateReticle(mesh) { reticleGroup.visible = true; }

        function checkHover() {
            raycaster.setFromCamera(mouse, camera);
            const hits = raycaster.intersectObjects(state.objects, true);
            if(hits.length>0) {
                const t = getRoot(hits[0].object);
                if(state.hovered !== t) sfx.play('hover');
                state.hovered = t; document.body.style.cursor = 'pointer';
            } else {
                state.hovered = null; document.body.style.cursor = 'default';
            }
        }

        function animate() {
            requestAnimationFrame(animate);
            orbitControls.update();
            
            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;
            
            if(state.viewMode === 'F1' || state.viewMode === 'F3') {
                filesGroup.rotation.y = state.currentRotY; filesGroup.position.y = state.currentPosY;
                filesGroup.rotation.x = 0; filesGroup.position.x = 0;
            } else {
                filesGroup.rotation.x = state.currentRotX; filesGroup.position.x = state.currentPosX;
                filesGroup.rotation.y = 0; filesGroup.position.y = 0;
            }

            starGroup.rotation.y += 0.0005; 

            if(reticleGroup.visible && (state.selected || state.hovered)) {
                const t = state.selected || state.hovered;
                const wp = new THREE.Vector3(); t.getWorldPosition(wp);
                const wq = new THREE.Quaternion(); t.getWorldQuaternion(wq);
                reticleGroup.position.copy(wp); reticleGroup.quaternion.copy(wq);
                reticleGroup.translateZ(0.2);

                const layers = reticleGroup.userData.layers;
                layers.forEach(l => {
                    l.rotation.z += l.userData.speed * l.userData.dir;
                });

                const s = (state.selected && popupMesh) ? 3 : 1;
                reticleGroup.scale.set(s,s,1);
            } else {
                reticleGroup.visible = false;
            }
            drawHUD(); composer.render();
        }
        animate();

        // --- HUD / Menu ---
        function drawHUD() {
            const dpr = window.devicePixelRatio||1;
            hudX.clearRect(0,0,hudC.width/dpr, hudC.height/dpr);
            const t = state.hovered || state.selected;
            if(!t || state.dragging) return;
            const p = new THREE.Vector3(); t.getWorldPosition(p);
            p.y += 2.0; p.project(camera);
            if(p.z > 1) return;
            const x = (p.x*0.5+0.5)*(hudC.width/dpr);
            const y = (p.y*-0.5+0.5)*(hudC.height/dpr);
            hudX.shadowBlur = 10; hudX.shadowColor = CONFIG.colCursor;
            hudX.strokeStyle = '#ffee00'; hudX.lineWidth = 2;
            hudX.beginPath(); hudX.moveTo(x,y); hudX.lineTo(x+40,y-40); hudX.lineTo(x+160,y-40); hudX.stroke();
            hudX.font="bold 24px 'Segoe UI'"; hudX.fillStyle='#fff'; hudX.fillText(t.userData.handle.name, x+45, y-45);
            hudX.shadowBlur = 0;
        }

        const ctxObj = document.getElementById('ctx-obj');
        const ctxBg = document.getElementById('ctx-bg');
        // Prevent menu closing on click inside
        [ctxObj, ctxBg].forEach(m => m.addEventListener('pointerdown', e => e.stopPropagation()));

        window.addEventListener('contextmenu', e => {
            e.preventDefault(); 
            if(e.target.closest('.tree-node')) { showMenu(ctxObj, e.clientX, e.clientY); return; }
            if(!e.target.closest('canvas')) return;
            updateMouse(e); raycaster.setFromCamera(mouse, camera);
            const hits = raycaster.intersectObjects(state.objects, true);
            document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none');
            sfx.play('click');
            if(hits.length>0) {
                const t = getRoot(hits[0].object);
                state.selected = t; state.contextTarget = t.userData.handle;
                activateReticle(t); updatePopup(t);
                showMenu(ctxObj, e.clientX, e.clientY);
            } else {
                state.selected = null; state.contextTarget = null;
                updatePopup(null); showMenu(ctxBg, e.clientX, e.clientY);
            }
        });
        function showMenu(el, x, y) {
            el.style.display='flex';
            if(x+200>window.innerWidth) x-=200; if(y+200>window.innerHeight) y-=200;
            el.style.left=x+'px'; el.style.top=y+'px';
        }
        function getActionTarget() { return state.contextTarget || (state.selected ? state.selected.userData.handle : null); }
        document.getElementById('cm-open').onclick = () => {
            const t = getActionTarget();
            if(t) { if(t.kind==='directory') { 
                // Context Menu Open: Animate In
                animateTransition('in', async ()=> await enterFolder(t, false));
            } else openPreview(t); }
            document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none');
        };
        // Context Menu Handlers (Standard)
        document.getElementById('cm-copy').onclick = () => { state.clipboard = getActionTarget(); document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none'); };
        document.getElementById('cm-delete').onclick = async () => {
            const t = getActionTarget();
            if(t && confirm('Delete?')) { await state.currentHandle.removeEntry(t.name, {recursive:true}); enterFolder(state.currentHandle, false); }
            document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none');
        };
        document.getElementById('cm-rename').onclick = async () => {
            const t = getActionTarget();
            if(t) { const n = prompt("Rename:", t.name); if(n && t.move) { await t.move(n); enterFolder(state.currentHandle, false); } }
            document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none');
        };
        document.getElementById('cm-prop').onclick = async () => {
            const t = getActionTarget(); if(t) alert(`Name: ${t.name}\nKind: ${t.kind}`);
            document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none');
        };
        document.getElementById('cm-new').onclick = async () => {
            const n = prompt("New Folder Name:"); if(n) { await state.currentHandle.getDirectoryHandle(n, {create:true}); enterFolder(state.currentHandle, false); }
            document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none');
        };
        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();
                enterFolder(state.currentHandle, false);
            } document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none');
        };
        
        // Up Directory Trigger
        document.getElementById('cm-up').onclick = () => { 
            goUp(); 
            document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none'); 
        };
        document.getElementById('cm-refresh').onclick = () => { 
            enterFolder(state.currentHandle, false); 
            document.querySelectorAll('.ctx-menu').forEach(m=>m.style.display='none'); 
        };

        window.addEventListener('dblclick', e => {
            if(!e.target.closest('canvas')) return;
            updateMouse(e); raycaster.setFromCamera(mouse, camera);
            const hits = raycaster.intersectObjects(state.objects, true);
            if(hits.length>0) {
                const t = getRoot(hits[0].object);
                if(t.userData.isFolder) { 
                    animateTransition('in', async ()=> await enterFolder(t.userData.handle, false));
                }
                else openPreview(t.userData.handle);
            }
        });

        document.getElementById('sb-tab').onclick=()=>document.getElementById('sidebar').classList.toggle('closed');
        document.getElementById('stock-tab').onclick=()=>document.getElementById('stock').classList.toggle('closed');
        
        function moveToStock(m) {
            filesGroup.remove(m); state.objects=state.objects.filter(o=>o!==m);
            if(popupMesh) { filesGroup.remove(popupMesh); popupMesh=null; }
            if(infoMesh) { filesGroup.remove(infoMesh); infoMesh=null; }
            state.stockData.push(m.userData.handle); renderStock();
        }
        function renderStock() {
            const s = document.getElementById('stock'); s.innerHTML='';
            state.stockData.forEach((h,i)=>{
                const d=document.createElement('div'); d.className='stock-item'; d.draggable=true;
                d.innerHTML=`<div style="font-size:30px">📦</div><div style="font-size:10px">${h.name}</div>`;
                d.ondragstart=e=>e.dataTransfer.setData('i',i);
                s.appendChild(d);
            });
        }
        container.ondragover=e=>e.preventDefault();
        container.ondrop=e=>{
            e.preventDefault(); const i=e.dataTransfer.getData('i');
            if(i!==''){
                const h=state.stockData[i]; state.stockData.splice(i,1); renderStock();
                // Refresh folder to place item correctly
                enterFolder(state.currentHandle, false);
            }
        };

        const pModal=document.getElementById('preview'), pBody=document.getElementById('p-body');
        const btnSave = document.getElementById('p-save');
        let currentFileHandle = null;

        document.getElementById('p-close').onclick=()=>{
            pModal.classList.remove('opening');
            pModal.classList.add('closing');
            setTimeout(() => {
                pModal.classList.remove('active', 'closing');
            }, 800); // Wait for animation
        };
        btnSave.onclick = async () => {
            const ta = document.getElementById('editor-area');
            if(currentFileHandle && ta) {
                try {
                    const w = await currentFileHandle.createWritable();
                    await w.write(ta.value); await w.close();
                    alert("Saved!");
                } catch(e) { alert("Save failed."); }
            }
        };

        async function openPreview(handle) {
            currentFileHandle = handle;
            pModal.classList.remove('closing');
            pModal.classList.add('active', 'opening'); // Add opening class
            
            document.getElementById('p-title').innerText=handle.name;
            pBody.innerHTML='Loading...'; btnSave.style.display='none';
            try {
                const f = await handle.getFile();
                const u = URL.createObjectURL(f);
                if(f.name.match(/\.(png|jpg|webp|gif|bmp)$/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 if(f.name.match(/\.(fbx|obj|glb)$/i)) pBody.innerHTML='[3D Model View]';
                else {
                    const t = await f.text();
                    pBody.innerHTML = `<textarea id="editor-area" class="hex-view">${t}</textarea>`;
                    btnSave.style.display = 'block';
                }
            } catch(e) { pBody.innerText="Error"; }
        }
        const treeC=document.getElementById('tree-container');
        async function updateTree(r) { treeC.innerHTML=''; treeC.appendChild(mkNode(r,0)); for await(const e of r.values()) if(e.kind==='directory') treeC.appendChild(mkNode(e,1)); }
        function mkNode(h,d) {
            const e=document.createElement('div'); e.className='tree-node'; e.style.paddingLeft=(d*15)+'px';
            e.innerHTML=`<span class="icon-tree">${h.kind==='directory'?'📂':'📄'}</span>${h.name}`;
            e.onclick=()=>{
                animateTransition('in', async ()=> await enterFolder(h, false));
            };
            e.oncontextmenu=(ev)=>{ ev.preventDefault(); ev.stopPropagation(); state.contextTarget=h; showMenu(ctxObj, ev.clientX, ev.clientY); };
            return e;
        }
    </script>
</body>
</html>

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

コメント

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