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


画像
画面

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

・ソースコードはこちら。↓

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Exp3D 1.1</title>
    <style>
        :root {
            --cyan: #00ffff;
            --magenta: #ff00ff;
            --bg: #050508;
            --panel: rgba(10, 15, 20, 0.95);
            --menu-bg: rgba(15, 20, 25, 0.98);
        }
        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; }

        /* Sidebar */
        #sidebar {
            width: 280px; background: var(--panel); border-right: 1px solid var(--cyan);
            display: flex; flex-direction: column; z-index: 20; backdrop-filter: blur(10px);
        }
        #sb-header {
            padding: 15px; color: var(--cyan); font-weight: bold; letter-spacing: 2px;
            border-bottom: 1px solid rgba(0,255,255,0.3); background: linear-gradient(90deg, rgba(0,255,255,0.1), transparent);
        }
        #tree-container { flex: 1; overflow-y: auto; padding: 10px; }
        .tree-node {
            display: flex; align-items: center; padding: 4px 8px; cursor: pointer; color: #888;
            font-size: 13px; transition: 0.2s; border-left: 2px solid transparent;
        }
        .tree-node:hover { background: rgba(0,255,255,0.1); color: #fff; }
        .icon-3d {
            width: 12px; height: 14px; margin-right: 8px; position: relative;
            transform-style: preserve-3d; transform: rotateX(-10deg) rotateY(25deg);
            background: rgba(255,255,255,0.1); border: 1px solid currentColor; flex-shrink: 0;
        }
        .icon-folder { color: var(--cyan); }
        .icon-file { color: var(--magenta); border-radius: 2px; }

        /* Main View */
        #main { flex: 1; position: relative; overflow: hidden; }
        #canvas-wrap { width: 100%; height: 100%; }

        /* HUD & Callout */
        #hud { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
        #hud-label {
            position: absolute; display: none; color: var(--cyan); font-family: 'Courier New'; font-weight: bold;
            font-size: 18px; text-shadow: 0 0 5px var(--cyan); white-space: nowrap; pointer-events: none;
            background: rgba(0,0,0,0.8); padding: 4px 8px; border: 1px solid var(--cyan);
        }

        /* Context Menu */
        .ctx-menu {
            position: absolute; display: none; flex-direction: column;
            background: var(--menu-bg); border: 1px solid var(--cyan);
            box-shadow: 0 0 15px rgba(0,255,255,0.2); min-width: 180px;
            z-index: 200; pointer-events: auto; padding: 5px 0;
        }
        .ctx-item {
            padding: 8px 15px; cursor: pointer; color: #ccc; font-size: 13px;
            display: flex; justify-content: space-between; transition: 0.2s;
        }
        .ctx-item:hover { background: var(--cyan); color: #000; }
        .ctx-item.disabled { color: #555; pointer-events: none; }
        .ctx-sep { height: 1px; background: #333; margin: 4px 0; }
        .shortcut { font-size: 10px; opacity: 0.7; }

        /* Bottom Shelf */
        #shelf {
            position: absolute; bottom: 0; left: 280px; right: 0; height: 150px;
            background: linear-gradient(to top, rgba(0,20,30,0.98), transparent);
            border-top: 2px solid var(--magenta); display: flex; align-items: center; justify-content: flex-start;
            padding: 0 20px; gap: 15px; z-index: 30; transition: bottom 0.3s;
        }
        #shelf.closed { bottom: -150px; }
        #shelf-tab {
            position: absolute; bottom: 150px; left: 50%; transform: translateX(-50%);
            background: var(--magenta); color: #000; padding: 2px 20px; font-weight: bold;
            cursor: pointer; clip-path: polygon(10% 0, 90% 0, 100% 100%, 0% 100%); z-index: 31; transition: bottom 0.3s;
        }
        #shelf.closed + #shelf-tab { bottom: 0; }
        .shelf-item {
            width: 80px; height: 100px; background: rgba(0,0,0,0.6); border: 1px solid var(--magenta);
            display: flex; flex-direction: column; align-items: center; justify-content: center;
            cursor: grab; color: var(--magenta); position: relative; flex-shrink: 0;
        }
        .shelf-img { font-size: 30px; margin-bottom: 5px; }
        .shelf-text { font-size: 10px; text-align: center; width: 90%; overflow: hidden; text-overflow: ellipsis; }

        /* Preview Modal */
        #preview {
            position: absolute; 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; transition: 0.3s; visibility: hidden;
        }
        #preview.active { opacity: 1; pointer-events: auto; visibility: visible; }
        #p-box { width: 85%; height: 85%; border: 1px solid var(--cyan); background: #000; display: flex; flex-direction: column; box-shadow: 0 0 30px rgba(0,255,255,0.1); }
        #p-head { padding: 10px; background: rgba(0,255,255,0.15); color: var(--cyan); display: flex; justify-content: space-between; font-weight: bold;}
        #p-body { flex: 1; padding: 0; overflow: hidden; position: relative; background: #050505; }
        
        .hex-view { 
            width: 100%; height: 100%; padding: 15px; color: #0f0; background: #000; 
            font-family: 'Courier New', monospace; white-space: pre; overflow: auto; border: none; outline: none; resize: none;
        }

        .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% 4px; z-index: 9999; opacity: 0.3;
        }
    </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" style="width:100%; background:transparent; border:1px solid var(--cyan); color:var(--cyan); padding:8px; cursor:pointer;">MOUNT DRIVE</button>
            </div>
            <div id="tree-container"></div>
        </div>

        <div id="main">
            <div id="canvas-wrap"></div>
            
            <div id="hud">
                <canvas id="hud-canvas"></canvas>
                <div id="hud-label">FILENAME</div>
            </div>

            <div id="shelf">
                <div style="color:var(--magenta); font-size:12px; opacity:0.6;">[ DRAG FILES & FOLDERS HERE ]</div>
            </div>
            <div id="shelf-tab">▼ DOCK</div>

            <div id="ctx-obj" class="ctx-menu">
                <div class="ctx-item" id="cm-open">Open <span class="shortcut">DblClk</span></div>
                <div class="ctx-sep"></div>
                <div class="ctx-item" id="cm-copy">Copy <span class="shortcut">Ctrl+C</span></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-sep"></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 <span class="shortcut">Ctrl+V</span></div>
                <div class="ctx-sep"></div>
                <div class="ctx-item" id="cm-up">Up Directory</div>
                <div class="ctx-item" id="cm-refresh">Refresh Scan</div>
            </div>
        </div>
    </div>

    <div id="preview">
        <div id="p-box">
            <div id="p-head"><span id="p-title"></span><span id="p-close" style="cursor:pointer;">[CLOSE]</span></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';

        // --- Config ---
        const CONFIG = {
            colFolder: 0x00ffff,
            colFile: 0xff00ff,
            grid: 0x002233
        };

        const state = {
            rootHandle: null,
            currentHandle: null,
            currentPath: [],
            objects: [],      
            shelfData: [],    
            dragging: null,   
            hovered: null,
            clipboard: null, // { handle, type: 'copy' }
            shelfOpen: true
        };

        // --- Scene Setup ---
        const container = document.getElementById('canvas-wrap');
        const scene = new THREE.Scene();
        scene.fog = new THREE.FogExp2(0x050508, 0.02);

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

        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(0x404040, 5));
        const dl = new THREE.DirectionalLight(0xffffff, 3);
        dl.position.set(5, 10, 5);
        scene.add(dl);
        scene.add(new THREE.GridHelper(100, 100, CONFIG.grid, 0x001122));

        const composer = new EffectComposer(renderer);
        composer.addPass(new RenderPass(scene, camera));
        const bloom = new UnrealBloomPass(new THREE.Vector2(container.clientWidth, container.clientHeight), 1.5, 0.4, 0.85);
        bloom.threshold = 0.1; bloom.strength = 1.2; bloom.radius = 0.5;
        composer.addPass(bloom);

        const orbitControls = new OrbitControls(camera, renderer.domElement);
        orbitControls.enableDamping = true;
        orbitControls.maxPolarAngle = Math.PI / 2.1;

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

        // --- File System ---
        document.getElementById('btn-mount').onclick = async () => {
            try {
                const h = await window.showDirectoryPicker();
                state.rootHandle = h;
                enterFolder(h);
                updateTree(h);
            } catch(e) { console.log(e); }
        };

        async function enterFolder(handle) {
            state.currentHandle = handle;
            if(!state.currentPath.includes(handle)) state.currentPath.push(handle);
            
            state.objects.forEach(o => filesGroup.remove(o));
            state.objects = [];

            const entries = [];
            for await (const e of handle.values()) entries.push(e);
            layoutFiles(entries);
        }

        function layoutFiles(entries) {
            const cols = Math.ceil(Math.sqrt(entries.length));
            const spacing = 3;
            const offset = (cols * spacing) / 2;
            entries.forEach((e, i) => {
                const x = (i % cols) * spacing - offset;
                const z = Math.floor(i / cols) * spacing - offset;
                createMonolith(e, x, z);
            });
        }

        function createMonolith(handle, x, z) {
            const isFolder = handle.kind === 'directory';
            const color = isFolder ? CONFIG.colFolder : CONFIG.colFile;

            const geo = new THREE.BoxGeometry(1.6, 2.2, 0.1);
            const mat = new THREE.MeshPhysicalMaterial({
                color: color, metalness: 0.1, roughness: 0.1, transmission: 0.5, opacity: 0.9, transparent: true,
                emissive: color, emissiveIntensity: 0.2
            });

            const mesh = new THREE.Mesh(geo, mat);
            mesh.position.set(x, 1.5, z);
            mesh.userData = { handle, isFolder, isRoot: true };

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

            const canvas = document.createElement('canvas');
            const ctx = canvas.getContext('2d');
            canvas.width = 256; canvas.height = 256;
            ctx.fillStyle = isFolder ? '#00ffff' : '#ff00ff';
            ctx.font = '80px Arial';
            ctx.textAlign = 'center';
            ctx.fillText(isFolder ? '📂' : '📄', 128, 100);
            ctx.fillStyle = '#ffffff';
            ctx.font = 'bold 28px Courier New';
            ctx.fillText(handle.name.substring(0,10), 128, 180);
            if(handle.name.length > 10) ctx.fillText(handle.name.substring(10,20)+'...', 128, 220);

            const tex = new THREE.CanvasTexture(canvas);
            const sprite = new THREE.Sprite(new THREE.SpriteMaterial({ map: tex }));
            sprite.scale.set(1.5, 1.5, 1);
            sprite.position.z = 0.15;
            mesh.add(sprite);

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

        // --- Interaction Logic (Drag & Click) ---
        const raycaster = new THREE.Raycaster();
        const mouse = new THREE.Vector2();
        let selectedObj = null;

        const onPointerDown = (e) => {
            if (e.button !== 0) return; // Left click only for drag
            updateMouse(e);
            raycaster.setFromCamera(mouse, camera);
            const intersects = raycaster.intersectObjects(state.objects, true);
            
            if (intersects.length > 0) {
                let target = getRootObject(intersects[0].object);
                if (target) {
                    state.dragging = {
                        mesh: target,
                        dist: camera.position.distanceTo(target.position),
                    };
                    orbitControls.enabled = false;
                    target.material.opacity = 0.5;
                }
            }
            hideContextMenus();
        };

        const onPointerMove = (e) => {
            updateMouse(e);
            if (state.dragging) {
                raycaster.setFromCamera(mouse, camera);
                const ray = raycaster.ray;
                // Move on a virtual plane
                const targetY = 1.5;
                const plane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -targetY);
                const target = new THREE.Vector3();
                ray.intersectPlane(plane, target);
                if(target) state.dragging.mesh.position.copy(target);
            } else {
                checkHover();
            }
        };

        const onPointerUp = () => {
            if (state.dragging) {
                const mesh = state.dragging.mesh;
                mesh.material.opacity = 0.9;
                
                // Drop to Shelf Check
                const screenPos = mesh.position.clone().project(camera);
                if (screenPos.y < -0.5) {
                    moveToShelf(mesh);
                }
                
                state.dragging = null;
                orbitControls.enabled = true;
            }
        };

        function getRootObject(obj) {
            let target = obj;
            while (target) {
                if (target.userData && target.userData.isRoot) return target;
                target = target.parent;
            }
            return null;
        }

        window.addEventListener('pointerdown', onPointerDown);
        window.addEventListener('pointermove', onPointerMove);
        window.addEventListener('pointerup', onPointerUp);

        function updateMouse(e) {
            const rect = renderer.domElement.getBoundingClientRect();
            mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;
            mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
        }

        // --- Context Menu & Logic ---
        const ctxObj = document.getElementById('ctx-obj');
        const ctxBg = document.getElementById('ctx-bg');

        window.addEventListener('contextmenu', (e) => {
            e.preventDefault();
            updateMouse(e);
            raycaster.setFromCamera(mouse, camera);
            const intersects = raycaster.intersectObjects(state.objects, true);

            hideContextMenus();

            if (intersects.length > 0) {
                selectedObj = getRootObject(intersects[0].object);
                showMenu(ctxObj, e.clientX, e.clientY);
            } else {
                selectedObj = null;
                // Update Paste state
                document.getElementById('cm-paste').classList.toggle('disabled', !state.clipboard);
                showMenu(ctxBg, e.clientX, e.clientY);
            }
        });

        function showMenu(el, x, y) {
            el.style.display = 'flex';
            if(x + 180 > window.innerWidth) x -= 180;
            if(y + 200 > window.innerHeight) y -= 200;
            el.style.left = x + 'px';
            el.style.top = y + 'px';
        }

        function hideContextMenus() {
            ctxObj.style.display = 'none';
            ctxBg.style.display = 'none';
        }

        // Context Menu Actions
        document.getElementById('cm-open').onclick = () => performOpen(selectedObj);
        
        document.getElementById('cm-copy').onclick = () => {
            if(selectedObj) state.clipboard = selectedObj.userData.handle;
            hideContextMenus();
        };
        
        document.getElementById('cm-delete').onclick = async () => {
            if(selectedObj && confirm('Delete ' + selectedObj.userData.handle.name + '?')) {
                await state.currentHandle.removeEntry(selectedObj.userData.handle.name, { recursive: true });
                enterFolder(state.currentHandle);
            }
            hideContextMenus();
        };

        document.getElementById('cm-rename').onclick = async () => {
            if(selectedObj) {
                const newName = prompt("Rename:", selectedObj.userData.handle.name);
                if(newName && selectedObj.userData.handle.move) {
                    await selectedObj.userData.handle.move(newName);
                    enterFolder(state.currentHandle);
                }
            }
            hideContextMenus();
        };

        document.getElementById('cm-prop').onclick = async () => {
            if(selectedObj) {
                const h = selectedObj.userData.handle;
                let info = `Name: ${h.name}\nKind: ${h.kind}`;
                if(h.kind === 'file') {
                    const f = await h.getFile();
                    info += `\nSize: ${f.size} bytes\nType: ${f.type}\nLast Mod: ${new Date(f.lastModified).toLocaleString()}`;
                }
                alert(info);
            }
            hideContextMenus();
        };

        document.getElementById('cm-new').onclick = async () => {
            const name = prompt("Folder Name:", "New Folder");
            if(name) {
                await state.currentHandle.getDirectoryHandle(name, { create: true });
                enterFolder(state.currentHandle);
            }
            hideContextMenus();
        };

        document.getElementById('cm-paste').onclick = async () => {
            if(state.clipboard) {
                const src = state.clipboard;
                if(src.kind === 'file') {
                    const f = await src.getFile();
                    const newHandle = await state.currentHandle.getFileHandle('Copy_' + src.name, { create: true });
                    const w = await newHandle.createWritable();
                    await w.write(f);
                    await w.close();
                    enterFolder(state.currentHandle);
                } else {
                    alert("Folder copy not implemented in browser sandbox.");
                }
            }
            hideContextMenus();
        };
        
        document.getElementById('cm-up').onclick = () => {
            if(state.currentPath.length > 1) {
                state.currentPath.pop();
                enterFolder(state.currentPath[state.currentPath.length-1]);
            }
            hideContextMenus();
        };

        document.getElementById('cm-refresh').onclick = () => {
            if(state.currentHandle) enterFolder(state.currentHandle);
            hideContextMenus();
        };

        // --- Double Click ---
        window.addEventListener('dblclick', (e) => {
            updateMouse(e);
            raycaster.setFromCamera(mouse, camera);
            const intersects = raycaster.intersectObjects(state.objects, true);
            if(intersects.length > 0) {
                const root = getRootObject(intersects[0].object);
                if(root) performOpen(root);
            }
        });

        function performOpen(mesh) {
            if(!mesh) return;
            const h = mesh.userData.handle;
            if(mesh.userData.isFolder) {
                enterFolder(h);
                updateTree(state.rootHandle);
            } else {
                openPreview(h);
            }
            hideContextMenus();
        }


        // --- HUD Logic ---
        const hudCanvas = document.getElementById('hud-canvas');
        const hudCtx = hudCanvas.getContext('2d');
        const hudLabel = document.getElementById('hud-label');

        function resizeHUD() {
            hudCanvas.width = container.clientWidth;
            hudCanvas.height = container.clientHeight;
        }
        window.addEventListener('resize', resizeHUD);
        resizeHUD();

        function checkHover() {
            raycaster.setFromCamera(mouse, camera);
            const intersects = raycaster.intersectObjects(state.objects, true);
            
            if (intersects.length > 0) {
                let target = getRootObject(intersects[0].object);
                if(target) {
                    state.hovered = target;
                    document.body.style.cursor = 'pointer';
                    return;
                }
            }
            state.hovered = null;
            document.body.style.cursor = 'default';
        }

        function drawHUD() {
            hudCtx.clearRect(0,0, hudCanvas.width, hudCanvas.height);
            if (!state.hovered || state.dragging) {
                hudLabel.style.display = 'none';
                return;
            }

            const mesh = state.hovered;
            const pos = mesh.position.clone();
            pos.y += 1.2;
            pos.project(camera);

            const x = (pos.x * 0.5 + 0.5) * hudCanvas.width;
            const y = (pos.y * -0.5 + 0.5) * hudCanvas.height;
            const color = mesh.userData.isFolder ? '#00ffff' : '#ff00ff';

            const tx = x + 60;
            const ty = y - 60;

            hudCtx.beginPath();
            hudCtx.strokeStyle = color;
            hudCtx.lineWidth = 2;
            hudCtx.moveTo(x, y);
            hudCtx.lineTo(x + 20, y - 20);
            hudCtx.lineTo(tx, ty);
            hudCtx.stroke();
            
            hudCtx.beginPath(); hudCtx.arc(x,y, 4, 0, Math.PI*2); hudCtx.fillStyle = color; hudCtx.fill();

            hudLabel.style.display = 'block';
            hudLabel.style.left = tx + 'px';
            hudLabel.style.top = (ty - 15) + 'px';
            hudLabel.style.borderColor = color;
            hudLabel.style.color = color;
            hudLabel.innerText = mesh.userData.handle.name.toUpperCase();
        }

        // --- File Preview Logic (Text/Image/Binary) ---
        const pModal = document.getElementById('preview');
        const pBody = document.getElementById('p-body');
        
        async function openPreview(handle) {
            pModal.classList.add('active');
            document.getElementById('p-title').innerText = handle.name;
            pBody.innerHTML = '<div style="padding:20px; color:#fff;">Loading...</div>';
            
            try {
                const file = await handle.getFile();
                const name = file.name.toLowerCase();

                if(name.match(/\.(png|jpg|jpeg|gif|webp|bmp|svg)$/)) {
                    // Image
                    pBody.innerHTML = '';
                    const img = document.createElement('img');
                    img.src = URL.createObjectURL(file);
                    img.style.maxWidth = '100%'; img.style.maxHeight = '100%'; img.style.objectFit = 'contain';
                    pBody.appendChild(img);
                } else {
                    // Try Text
                    try {
                        const text = await file.text();
                        // Simple binary check: look for null bytes
                        if (text.includes('\0') && !name.endsWith('.txt')) {
                            throw new Error("Binary content detected");
                        }
                        const ta = document.createElement('textarea');
                        ta.className = 'hex-view';
                        ta.value = text;
                        pBody.innerHTML = '';
                        pBody.appendChild(ta);
                    } catch (err) {
                        // Fallback to Binary (Hex)
                        const buf = await file.arrayBuffer();
                        const view = new DataView(buf);
                        let hexStr = `BINARY VIEW (${file.size} bytes)\n\n`;
                        const limit = Math.min(view.byteLength, 2048); // Show first 2KB
                        
                        for(let i=0; i<limit; i+=16) {
                            hexStr += i.toString(16).padStart(6,'0').toUpperCase() + '  ';
                            let chars = '';
                            for(let j=0; j<16; j++) {
                                if(i+j < limit) {
                                    const b = view.getUint8(i+j);
                                    hexStr += b.toString(16).padStart(2,'0').toUpperCase() + ' ';
                                    chars += (b >= 32 && b <= 126) ? String.fromCharCode(b) : '.';
                                } else {
                                    hexStr += '   ';
                                }
                            }
                            hexStr += ' ' + chars + '\n';
                        }
                        if(file.size > limit) hexStr += '\n... (Truncated)';

                        const ta = document.createElement('textarea');
                        ta.className = 'hex-view';
                        ta.value = hexStr;
                        pBody.innerHTML = '';
                        pBody.appendChild(ta);
                    }
                }
            } catch(e) {
                pBody.innerHTML = '<div style="padding:20px; color:red;">Error reading file: ' + e.message + '</div>';
            }
        }
        document.getElementById('p-close').onclick = () => pModal.classList.remove('active');

        // --- Shelf Logic ---
        const shelf = document.getElementById('shelf');
        function moveToShelf(mesh) {
            filesGroup.remove(mesh);
            state.objects = state.objects.filter(o => o !== mesh);
            state.shelfData.push(mesh.userData.handle);
            renderShelf();
        }
        function renderShelf() {
            shelf.innerHTML = '';
            if (state.shelfData.length === 0) shelf.innerHTML = '<div style="color:var(--magenta); font-size:12px; opacity:0.6;">[ DRAG FILES & FOLDERS HERE ]</div>';
            
            state.shelfData.forEach((h, i) => {
                const el = document.createElement('div');
                el.className = 'shelf-item';
                el.draggable = true;
                el.innerHTML = `<div class="shelf-img">${h.kind==='directory'?'📂':'📄'}</div><div class="shelf-text">${h.name}</div>`;
                el.addEventListener('dragstart', (e) => e.dataTransfer.setData('idx', i));
                shelf.appendChild(el);
            });
        }
        container.addEventListener('dragover', e => e.preventDefault());
        container.addEventListener('drop', e => {
            e.preventDefault();
            const idx = e.dataTransfer.getData('idx');
            if(idx !== '') restoreFromShelf(parseInt(idx), e.clientX, e.clientY);
        });
        function restoreFromShelf(idx, cx, cy) {
            const h = state.shelfData[idx];
            state.shelfData.splice(idx, 1);
            renderShelf();
            
            const vec = new THREE.Vector3((cx/container.clientWidth)*2-1, -(cy/container.clientHeight)*2+1, 0.5);
            vec.unproject(camera);
            vec.sub(camera.position).normalize();
            const dist = (0 - camera.position.z) / vec.z;
            const pos = camera.position.clone().add(vec.multiplyScalar(dist));
            createMonolith(h, pos.x, pos.z);
        }
        const tab = document.getElementById('shelf-tab');
        tab.onclick = () => {
            shelf.classList.toggle('closed');
            tab.innerText = shelf.classList.contains('closed') ? '▲ DOCK' : '▼ DOCK';
        };

        // --- Tree View ---
        const treeCont = document.getElementById('tree-container');
        async function updateTree(root) {
            treeCont.innerHTML = '';
            treeCont.appendChild(createNode(root, 0));
            for await (const e of root.values()) {
                if(e.kind === 'directory') treeCont.appendChild(createNode(e, 1));
            }
        }
        function createNode(h, depth) {
            const div = document.createElement('div');
            div.className = 'tree-node';
            div.onclick = () => { if(h.kind==='directory') enterFolder(h); };
            for(let i=0; i<depth; i++) div.appendChild(document.createElement('div')).className='indent';
            div.innerHTML += `<div class="icon-3d icon-${h.kind}"></div><div>${h.name}</div>`;
            return div;
        }

        // --- Loop ---
        function animate() {
            requestAnimationFrame(animate);
            orbitControls.update();
            drawHUD();
            composer.render();
        }
        animate();

        window.onresize = () => {
            camera.aspect = container.clientWidth / container.clientHeight;
            camera.updateProjectionMatrix();
            renderer.setSize(container.clientWidth, container.clientHeight);
            composer.setSize(container.clientWidth, container.clientHeight);
            resizeHUD();
        };

    </script>
</body>
</html>

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

コメント

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