ImageBoosterのコースエディタ




画像

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

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

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>High-Speed Course Editor Pro</title>
    <style>
        body { margin: 0; overflow: hidden; font-family: 'Segoe UI', sans-serif; background: #1e1e1e; color: #eee; user-select: none; }
        
        /* Menu Bar */
        #menubar { height: 30px; background: #2d2d2d; display: flex; align-items: center; padding: 0 10px; border-bottom: 1px solid #3e3e3e; }
        .menu-item { padding: 0 15px; height: 100%; display: flex; align-items: center; cursor: pointer; position: relative; font-size: 13px; color: #ccc; }
        .menu-item:hover { background: #3e3e3e; color: #fff; }
        .dropdown { display: none; position: absolute; top: 30px; left: 0; background: #252526; border: 1px solid #3e3e3e; min-width: 180px; z-index: 1000; box-shadow: 0 4px 6px rgba(0,0,0,0.3); }
        .menu-item:hover .dropdown { display: block; }
        .dropdown-item { padding: 8px 15px; cursor: pointer; display: block; color: #ccc; text-decoration: none; font-size: 13px; }
        .dropdown-item:hover { background: #094771; color: #fff; }
        .separator { border-top: 1px solid #3e3e3e; margin: 4px 0; }

        /* Layout */
        #container { display: flex; height: calc(100vh - 30px); }
        #canvas-container { flex-grow: 1; position: relative; overflow: hidden; outline: none; background: #1e1e1e; }
        
        /* Right Panel */
        #properties-panel { width: 260px; background: #252526; border-left: 1px solid #3e3e3e; padding: 15px; box-sizing: border-box; overflow-y: auto; }
        .prop-group { margin-bottom: 20px; }
        .prop-label { display: block; font-size: 11px; color: #aaa; margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.5px; }
        input[type="text"], input[type="number"], input[type="color"], select {
            width: 100%; box-sizing: border-box; background: #3c3c3c; border: 1px solid #3e3e3e; color: #fff; padding: 6px; margin-bottom: 8px; font-size: 12px; border-radius: 2px; text-align: right; 
        }
        select { text-align: left; cursor: pointer; }
        input:focus, select:focus { border-color: #007fd4; outline: none; }
        
        /* Status / UI Overlays */
        #status-bar { position: absolute; bottom: 10px; left: 10px; color: #4fc1ff; font-family: monospace; font-size: 12px; pointer-events: none; text-shadow: 1px 1px 2px rgba(0,0,0,0.8); }
        #info-overlay { position: absolute; top: 10px; left: 10px; pointer-events: none; }
        #mode-display { background: rgba(0,0,0,0.7); color: #fff; padding: 6px 10px; border-radius: 4px; font-weight: 600; font-size: 14px; border-left: 3px solid #007fd4; margin-bottom: 5px; display: inline-block; }
        #height-display { background: rgba(0,0,0,0.5); color: #4fc1ff; padding: 4px 8px; border-radius: 4px; font-family: monospace; font-size: 14px; font-weight: bold; display: table; }
        
        /* Palette UI */
        #palette-box { display: flex; flex-wrap: wrap; gap: 5px; margin-bottom: 8px; }
        .palette-swatch { width: 20px; height: 20px; border: 1px solid #555; cursor: pointer; box-sizing: border-box; }
        .palette-swatch:hover { border-color: #fff; transform: scale(1.1); }
    </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="menubar">
        <div class="menu-item">File
            <div class="dropdown">
                <div class="dropdown-item" id="menu-new">New Course</div>
                <div class="dropdown-item" id="menu-open">Open JSON</div>
                <div class="dropdown-item" id="menu-save">Save JSON</div>
            </div>
        </div>
        <div class="menu-item">Edit
            <div class="dropdown">
                <div class="dropdown-item" id="tool-select">Select</div>
                <div class="dropdown-item" id="tool-add">Add</div>
                <div class="dropdown-item" id="tool-erase">Erase (Del)</div>
                <div class="separator"></div>
                <div class="dropdown-item" id="tool-move">Move</div>
                <div class="dropdown-item" id="tool-rotate">Rotate (Scenery only)</div>
                <div class="dropdown-item" id="tool-scale">Scale (Scenery only)</div>
            </div>
        </div>
        <div class="menu-item">View
            <div class="dropdown">
                <div class="dropdown-item" onclick="viewCam('default')">Default (F1)</div>
                <div class="dropdown-item" onclick="viewCam('top')">Top (F2)</div>
                <div class="dropdown-item" onclick="viewCam('front')">Front (F3)</div>
            </div>
        </div>
    </div>

    <div id="container">
        <div id="canvas-container" tabindex="0">
            <div id="info-overlay">
                <div id="mode-display">Select</div>
                <div id="height-display">Height: 0</div>
            </div>
            <div id="status-bar">Ready | Snap Grid: 10 | Move: Hold Click + Drag | Lock Axis: Hold X, Y, or Z</div>
        </div>
        
        <div id="properties-panel">
            <h3 id="prop-header" style="color:#4fc1ff; margin-top:0; border-bottom:1px solid #444; padding-bottom:5px;">Properties</h3>
            
            <div id="prop-add-settings" style="display:none;">
                <div class="prop-group">
                    <span class="prop-label">Item to Add</span>
                    <select id="add-item-type">
                        <option value="node">Track Node (Spline Point)</option>
                        <option value="scenery">Scenery Block</option>
                    </select>
                </div>
                <div id="add-scenery-options" style="display:none;">
                    <div class="prop-group">
                        <span class="prop-label">Color</span>
                        <div id="palette-box"></div>
                        <input type="color" id="brush-color" value="#888888">
                    </div>
                </div>
            </div>

            <div id="prop-content" style="display:none;">
                <div class="prop-group">
                    <span class="prop-label">Type</span>
                    <input type="text" id="prop-type" disabled style="color:#888;">
                </div>
                <div class="prop-group">
                    <span class="prop-label">Position (X, Y, Z)</span>
                    <div style="display:flex; gap:5px;">
                        <input type="number" id="prop-x" step="10">
                        <input type="number" id="prop-y" step="10">
                        <input type="number" id="prop-z" step="10">
                    </div>
                </div>
                
                <div id="prop-node-group" class="prop-group">
                    <span class="prop-label">Tunnel Bank / Roll (Deg)</span>
                    <input type="number" id="prop-roll" step="15">
                </div>

                <div id="prop-scenery-group">
                    <div class="prop-group">
                        <span class="prop-label">Rotation Y (Deg)</span>
                        <input type="number" id="prop-ry" step="15">
                    </div>
                    <div class="prop-group">
                        <span class="prop-label">Scale (X, Y, Z)</span>
                        <div style="display:flex; gap:5px;">
                            <input type="number" id="prop-sx" step="1">
                            <input type="number" id="prop-sy" step="1">
                            <input type="number" id="prop-sz" step="1">
                        </div>
                    </div>
                    <div class="prop-group">
                        <span class="prop-label">Color</span>
                        <input type="color" id="prop-color">
                    </div>
                </div>
            </div>
            <div id="prop-empty" style="color: #666; font-style: italic; text-align: center; margin-top: 20px;">
                No item selected.
            </div>
        </div>
    </div>

    <input type="file" id="file-input" style="display: none;" accept=".json">

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

        // --- State Variables ---
        let scene, camera, renderer, controls;
        let raycaster, mouse;
        
        let currentTool = 'select'; 
        let gridSize = 10;
        let baseHeight = 0;
        
        // Data Arrays
        let trackNodes = []; // mesh, roll
        let sceneryBlocks = []; // mesh
        let trackSplineMesh = null;

        let selectedItem = null;
        let selectedItemType = null; // 'node' or 'scenery'
        let addItemType = 'node';

        // Palette
        let palette = ['#888888', '#ff3333', '#33ff33', '#3333ff', '#ffff33', '#00ffff', '#ff00ff', '#111111'];

        // Visual Helpers
        let gridGroup = new THREE.Group(); 
        let planeGround; 
        let cursorTarget; // Red (Add)
        let cursorPoint;  // White (Hover)
        let cursorSelect; // Yellow (Select)

        // Interaction State
        let isDragging = false;
        let dragStartMouse = new THREE.Vector2();
        let dragStartVal = new THREE.Vector3(); 
        let startCursorPos = new THREE.Vector3(); 
        
        // Axis Lock State
        let axisLock = { x: false, y: false, z: false };

        // --- Init ---
        function init() {
            const container = document.getElementById('canvas-container');

            scene = new THREE.Scene();
            scene.background = new THREE.Color(0x1e1e1e);
            scene.fog = new THREE.Fog(0x1e1e1e, 200, 1500);

            camera = new THREE.PerspectiveCamera(60, container.clientWidth / container.clientHeight, 1, 5000);
            camera.position.set(100, 100, 100);
            camera.lookAt(0, 0, 0);

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

            const ambient = new THREE.AmbientLight(0xffffff, 0.6);
            scene.add(ambient);
            const sun = new THREE.DirectionalLight(0xffffff, 0.8);
            sun.position.set(100, 300, 200);
            scene.add(sun);

            scene.add(gridGroup);

            initCursors();
            updateEnvironment(); 
            renderPalette();

            const pgGeo = new THREE.PlaneGeometry(10000, 10000);
            const pgMat = new THREE.MeshBasicMaterial({ visible: false });
            planeGround = new THREE.Mesh(pgGeo, pgMat);
            planeGround.rotation.x = -Math.PI / 2;
            scene.add(planeGround);

            controls = new OrbitControls(camera, renderer.domElement);
            controls.enableDamping = true;
            controls.dampingFactor = 0.1;

            raycaster = new THREE.Raycaster();
            mouse = new THREE.Vector2();

            window.addEventListener('resize', onResize);
            window.addEventListener('keydown', onKeyDown);
            window.addEventListener('keyup', onKeyUp);
            
            const cvs = renderer.domElement;
            cvs.addEventListener('pointermove', onPointerMove);
            cvs.addEventListener('pointerdown', onPointerDown);
            cvs.addEventListener('pointerup', onPointerUp);
            cvs.addEventListener('contextmenu', (e) => { e.preventDefault(); });

            setupUI();
            setTool('select'); 
            
            // Initial Nodes
            createNode(new THREE.Vector3(0, 50, 0), 0);
            createNode(new THREE.Vector3(0, 50, -200), 0);
            createNode(new THREE.Vector3(200, 50, -100), 45);
            rebuildTrackCurve();
            
            animate();
        }

        function initCursors() {
            const tGeo = new THREE.PlaneGeometry(10, 10);
            const tMat = new THREE.MeshBasicMaterial({ color: 0xff3333, side: THREE.DoubleSide, transparent: true, opacity: 0.5 });
            cursorTarget = new THREE.Mesh(tGeo, tMat);
            cursorTarget.rotation.x = -Math.PI / 2;
            scene.add(cursorTarget);

            const pGeo = new THREE.BoxGeometry(10, 10, 10);
            const pEdges = new THREE.EdgesGeometry(pGeo);
            cursorPoint = new THREE.LineSegments(pEdges, new THREE.LineBasicMaterial({ color: 0xffffff, opacity: 0.8, transparent: true }));
            scene.add(cursorPoint);

            const sGeo = new THREE.BoxGeometry(10, 10, 10);
            const sEdges = new THREE.EdgesGeometry(sGeo);
            cursorSelect = new THREE.LineSegments(sEdges, new THREE.LineBasicMaterial({ color: 0xffff00, depthTest: false }));
            cursorSelect.visible = false;
            scene.add(cursorSelect);
        }

        function updateEnvironment() {
            while(gridGroup.children.length > 0) gridGroup.remove(gridGroup.children[0]);
            const floorGrid = new THREE.GridHelper(1000, 100, 0x444444, 0x2a2a2a);
            gridGroup.add(floorGrid);
        }

        function renderPalette() {
            const box = document.getElementById('palette-box');
            box.innerHTML = '';
            palette.forEach(color => {
                const el = document.createElement('div');
                el.className = 'palette-swatch';
                el.style.backgroundColor = color;
                el.onclick = () => { document.getElementById('brush-color').value = color; };
                box.appendChild(el);
            });
        }

        function animate() {
            requestAnimationFrame(animate);
            controls.update();

            if(cursorTarget) {
                cursorTarget.material.opacity = 0.5 + 0.2 * Math.sin(Date.now() * 0.005 * 8);
            }
            renderer.render(scene, camera);
        }

        function snapToGrid(val) {
            return Math.round(val / gridSize) * gridSize;
        }

        // --- Core Builders ---
        function createNode(pos, roll) {
            const geo = new THREE.SphereGeometry(6, 16, 16);
            const mat = new THREE.MeshPhongMaterial({ color: 0x00ff00, emissive: 0x004400 });
            const mesh = new THREE.Mesh(geo, mat);
            mesh.position.copy(pos);
            mesh.userData = { type: 'node', roll: roll };
            scene.add(mesh);
            trackNodes.push(mesh);
            rebuildTrackCurve();
            return mesh;
        }

        function createScenery(pos, scaleVec, colorHex, rotY) {
            const geo = new THREE.BoxGeometry(gridSize, gridSize, gridSize);
            const mat = new THREE.MeshStandardMaterial({ color: colorHex });
            const mesh = new THREE.Mesh(geo, mat);
            mesh.position.copy(pos);
            mesh.scale.copy(scaleVec);
            mesh.rotation.y = rotY;
            mesh.userData = { type: 'scenery' };
            scene.add(mesh);
            sceneryBlocks.push(mesh);
            return mesh;
        }

        function rebuildTrackCurve() {
            if (trackSplineMesh) scene.remove(trackSplineMesh);
            if (trackNodes.length < 3) return;

            const points = trackNodes.map(n => n.position);
            const curve = new THREE.CatmullRomCurve3(points, true); // true = loop

            const tubeGeo = new THREE.TubeGeometry(curve, trackNodes.length * 20, 12, 8, true);
            const tubeMat = new THREE.MeshBasicMaterial({ color: 0x00ffff, wireframe: true, transparent: true, opacity: 0.2 });
            trackSplineMesh = new THREE.Mesh(tubeGeo, tubeMat);
            scene.add(trackSplineMesh);

            const lineGeo = new THREE.BufferGeometry().setFromPoints(curve.getPoints(trackNodes.length * 20));
            const lineMat = new THREE.LineBasicMaterial({ color: 0xffff00 });
            trackSplineMesh.add(new THREE.Line(lineGeo, lineMat));
        }

        // --- Interaction Logic ---
        function onPointerMove(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;

            const isLocked = axisLock.x || axisLock.y || axisLock.z;

            // Handle Dragging selected object
            if (isDragging && selectedItem && ['move', 'rotate', 'scale'].includes(currentTool)) {
                const deltaX = e.clientX - dragStartMouse.x;
                const deltaY = e.clientY - dragStartMouse.y;
                
                if (currentTool === 'move') {
                    if (isLocked) {
                        const mag = (deltaX - deltaY) * 0.5; 
                        if(axisLock.x) selectedItem.position.x = snapToGrid(dragStartVal.x + mag);
                        if(axisLock.y) selectedItem.position.y = snapToGrid(dragStartVal.y + mag);
                        if(axisLock.z) selectedItem.position.z = snapToGrid(dragStartVal.z + mag);
                    } else {
                        raycaster.setFromCamera(mouse, camera);
                        const intersects = raycaster.intersectObject(planeGround);
                        if(intersects.length > 0) {
                            selectedItem.position.x = snapToGrid(intersects[0].point.x);
                            selectedItem.position.z = snapToGrid(intersects[0].point.z);
                        }
                    }
                    if(selectedItem.userData.type === 'node') rebuildTrackCurve();
                }
                else if (currentTool === 'rotate' && selectedItem.userData.type === 'scenery') {
                    const snapRad = Math.PI / 4;
                    selectedItem.rotation.y = Math.round((dragStartVal.y + deltaX * 0.02) / snapRad) * snapRad;
                }
                else if (currentTool === 'scale' && selectedItem.userData.type === 'scenery') {
                    const mag = -deltaY * 0.05; 
                    let sVec = dragStartVal.clone();
                    if(isLocked) {
                        if(axisLock.x) sVec.x = Math.max(1, Math.round(dragStartVal.x + mag));
                        if(axisLock.y) sVec.y = Math.max(1, Math.round(dragStartVal.y + mag));
                        if(axisLock.z) sVec.z = Math.max(1, Math.round(dragStartVal.z + mag));
                    } else {
                        const s = Math.max(1, Math.round(dragStartVal.x + mag));
                        sVec.set(s,s,s);
                    }
                    selectedItem.scale.copy(sVec);
                    // Adjust position so it scales from the bottom if needed (optional, keeping it simple center-scaled for now)
                }
                
                updateSelectionBox();
                updateUIFromSelection();
                return;
            }

            // Dragging empty cursor (for Add tool)
            if (isDragging && !selectedItem && currentTool === 'add' && isLocked) {
                const deltaX = e.clientX - dragStartMouse.x;
                const deltaY = e.clientY - dragStartMouse.y;
                const mag = (deltaX - deltaY) * 0.5;
                if(axisLock.x) cursorTarget.position.x = snapToGrid(startCursorPos.x + mag);
                if(axisLock.y) cursorTarget.position.y = snapToGrid(startCursorPos.y + mag);
                if(axisLock.z) cursorTarget.position.z = snapToGrid(startCursorPos.z + mag);
                document.getElementById('height-display').textContent = `Height: ${cursorTarget.position.y}`;
                return;
            }

            // Hover Logic
            if (!isDragging) {
                raycaster.setFromCamera(mouse, camera);
                
                if (currentTool === 'add') {
                    const intersects = raycaster.intersectObject(planeGround);
                    if (intersects.length > 0) {
                        const p = intersects[0].point;
                        cursorTarget.position.set(snapToGrid(p.x), baseHeight, snapToGrid(p.z));
                    }
                } else {
                    const allMeshes = [...trackNodes, ...sceneryBlocks];
                    const intersects = raycaster.intersectObjects(allMeshes);
                    if (intersects.length > 0) {
                        const hit = intersects[0].object;
                        cursorPoint.position.copy(hit.position);
                        if(hit.userData.type === 'node') cursorPoint.scale.set(1.5,1.5,1.5);
                        else cursorPoint.scale.copy(hit.scale);
                        cursorPoint.visible = true;
                    } else {
                        const pIntersects = raycaster.intersectObject(planeGround);
                        if (pIntersects.length > 0) {
                            cursorPoint.scale.set(1,1,1);
                            cursorPoint.position.set(snapToGrid(pIntersects[0].point.x), baseHeight, snapToGrid(pIntersects[0].point.z));
                            cursorPoint.visible = true;
                        }
                    }
                }
            }
        }

        function onPointerDown(e) {
            if (e.button !== 0) return;
            const isLocked = axisLock.x || axisLock.y || axisLock.z;

            if (isLocked) {
                isDragging = true;
                dragStartMouse.set(e.clientX, e.clientY);
                controls.enabled = false; 
                
                if(selectedItem && ['move', 'rotate', 'scale'].includes(currentTool)) {
                    if (currentTool === 'move') dragStartVal.copy(selectedItem.position);
                    if (currentTool === 'rotate') dragStartVal.set(0, selectedItem.rotation.y, 0);
                    if (currentTool === 'scale') dragStartVal.copy(selectedItem.scale);
                } else {
                    startCursorPos.copy(cursorTarget.position);
                }
                return; 
            }

            raycaster.setFromCamera(mouse, camera);
            const allMeshes = [...trackNodes, ...sceneryBlocks];
            const intersects = raycaster.intersectObjects(allMeshes);
            const hitItem = intersects.length > 0 ? intersects[0].object : null;

            if (currentTool === 'select') {
                selectItem(hitItem);
            }
            else if (currentTool === 'add') {
                if(addItemType === 'node') {
                    createNode(cursorTarget.position.clone(), 0);
                } else {
                    const col = document.getElementById('brush-color').value;
                    createScenery(cursorTarget.position.clone(), new THREE.Vector3(1,1,1), col, 0);
                }
            }
            else if (currentTool === 'erase') {
                if(hitItem) deleteItem(hitItem);
            }
            else if (['move', 'rotate', 'scale'].includes(currentTool)) {
                if (hitItem) {
                    selectItem(hitItem);
                    isDragging = true;
                    controls.enabled = false; 
                    dragStartMouse.set(e.clientX, e.clientY);
                    
                    if (currentTool === 'move') dragStartVal.copy(hitItem.position);
                    if (currentTool === 'rotate') dragStartVal.set(0, hitItem.rotation.y, 0);
                    if (currentTool === 'scale') dragStartVal.copy(hitItem.scale);
                } else {
                    selectItem(null);
                }
            }
        }

        function onPointerUp() {
            isDragging = false;
            controls.enabled = true;
        }

        function onKeyDown(e) {
            if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;

            if(e.key.toLowerCase() === 'x') axisLock.x = true;
            if(e.key.toLowerCase() === 'y') axisLock.y = true;
            if(e.key.toLowerCase() === 'z') axisLock.z = true;

            if (e.key === 'F1') { e.preventDefault(); viewCam('default'); }
            if (e.key === 'F2') { e.preventDefault(); viewCam('top'); }
            if (e.key === 'F3') { e.preventDefault(); viewCam('front'); }

            if (e.key === 'ArrowUp') { 
                e.preventDefault(); 
                if (['move','select'].includes(currentTool) && selectedItem) {
                    selectedItem.position.y += gridSize;
                    if(selectedItem.userData.type === 'node') rebuildTrackCurve();
                    updateUIFromSelection();
                    updateSelectionBox();
                } else {
                    baseHeight += gridSize;
                    document.getElementById('height-display').textContent = `Height: ${baseHeight}`;
                }
            }
            if (e.key === 'ArrowDown') { 
                e.preventDefault(); 
                if (['move','select'].includes(currentTool) && selectedItem) {
                    selectedItem.position.y -= gridSize;
                    if(selectedItem.userData.type === 'node') rebuildTrackCurve();
                    updateUIFromSelection();
                    updateSelectionBox();
                } else {
                    baseHeight -= gridSize;
                    document.getElementById('height-display').textContent = `Height: ${baseHeight}`;
                }
            }

            if (e.key === 'Delete') { if (selectedItem) deleteItem(selectedItem); }
        }

        function onKeyUp(e) {
            if(e.key.toLowerCase() === 'x') axisLock.x = false;
            if(e.key.toLowerCase() === 'y') axisLock.y = false;
            if(e.key.toLowerCase() === 'z') axisLock.z = false;
        }

        // --- Selection & Editing ---
        function deleteItem(mesh) {
            scene.remove(mesh);
            if(mesh.userData.type === 'node') {
                trackNodes = trackNodes.filter(v => v !== mesh);
                rebuildTrackCurve();
            } else {
                sceneryBlocks = sceneryBlocks.filter(v => v !== mesh);
            }
            if (selectedItem === mesh) selectItem(null);
        }

        function selectItem(mesh) {
            selectedItem = mesh;
            if (mesh) {
                selectedItemType = mesh.userData.type;
                if(currentTool !== 'add') cursorSelect.visible = true;
                updateSelectionBox();
                updateUIFromSelection();
            } else {
                selectedItemType = null;
                cursorSelect.visible = false;
                if (currentTool !== 'add') {
                    document.getElementById('prop-content').style.display = 'none';
                    document.getElementById('prop-empty').style.display = 'block';
                }
            }
        }

        function updateSelectionBox() {
            if(!selectedItem) return;
            cursorSelect.position.copy(selectedItem.position);
            cursorSelect.rotation.copy(selectedItem.rotation);
            if(selectedItem.userData.type === 'node') cursorSelect.scale.set(1.5,1.5,1.5);
            else cursorSelect.scale.copy(selectedItem.scale).multiplyScalar(1.05);
        }

        // --- UI Binding ---
        window.setTool = (tool) => {
            currentTool = tool;
            const names = { 'select': 'Select', 'add': 'Add', 'erase': 'Erase', 'move': 'Move', 'rotate': 'Rotate', 'scale': 'Scale' };
            document.getElementById('mode-display').innerText = names[tool];

            if(tool === 'add') {
                cursorTarget.visible = true; cursorPoint.visible = false; cursorSelect.visible = false;
                document.getElementById('prop-header').innerText = "Add Settings";
                document.getElementById('prop-add-settings').style.display = 'block';
                document.getElementById('prop-content').style.display = 'none';
                document.getElementById('prop-empty').style.display = 'none';
            } else {
                cursorTarget.visible = false; cursorPoint.visible = true;
                if(selectedItem) cursorSelect.visible = true;
                document.getElementById('prop-header').innerText = "Properties";
                document.getElementById('prop-add-settings').style.display = 'none';
                if(selectedItem) updateUIFromSelection();
                else {
                    document.getElementById('prop-content').style.display = 'none';
                    document.getElementById('prop-empty').style.display = 'block';
                }
            }
        };

        window.viewCam = (dir) => {
            const dist = 300;
            camera.up.set(0, 1, 0);
            switch(dir) {
                case 'default': camera.position.set(dist, dist, dist); break;
                case 'front': camera.position.set(0, 0, dist); break;
                case 'top': camera.position.set(0, dist, 0); break;
            }
            camera.lookAt(0,0,0); controls.target.set(0,0,0); controls.update();
        };

        function setupUI() {
            // Tools
            ['select', 'add', 'erase', 'move', 'rotate', 'scale'].forEach(t => {
                document.getElementById(`tool-${t}`).onclick = () => setTool(t);
            });

            // Add Options
            document.getElementById('add-item-type').addEventListener('change', (e) => {
                addItemType = e.target.value;
                document.getElementById('add-scenery-options').style.display = addItemType === 'scenery' ? 'block' : 'none';
                if(addItemType === 'node') cursorTarget.scale.set(1,1,1);
                else cursorTarget.scale.set(1,1,1);
            });

            // Property Bindings
            const bindProp = (id, objKey, axis, isDeg) => {
                document.getElementById(id).addEventListener('input', (e) => {
                    if(!selectedItem) return;
                    let val = parseFloat(e.target.value);
                    if(isDeg) val = THREE.MathUtils.degToRad(val);

                    if(objKey === 'roll') {
                        selectedItem.userData.roll = val;
                        rebuildTrackCurve();
                    } else if(axis) {
                        selectedItem[objKey][axis] = val;
                        if(objKey === 'position' && selectedItem.userData.type === 'node') rebuildTrackCurve();
                    } else if(objKey === 'color') {
                        selectedItem.material.color.set(e.target.value);
                    }
                    updateSelectionBox();
                });
            };

            bindProp('prop-x', 'position', 'x'); bindProp('prop-y', 'position', 'y'); bindProp('prop-z', 'position', 'z');
            bindProp('prop-sx', 'scale', 'x'); bindProp('prop-sy', 'scale', 'y'); bindProp('prop-sz', 'scale', 'z');
            bindProp('prop-ry', 'rotation', 'y', true);
            bindProp('prop-roll', 'roll', null, false); // Keep roll in degrees in UserData
            bindProp('prop-color', 'color');

            // Save/Load
            document.getElementById('menu-new').onclick = () => {
                if(confirm("Clear all course data?")) {
                    trackNodes.forEach(n => scene.remove(n));
                    sceneryBlocks.forEach(s => scene.remove(s));
                    trackNodes = []; sceneryBlocks = [];
                    rebuildTrackCurve(); selectItem(null);
                }
            };
            document.getElementById('menu-save').onclick = () => {
                const data = {
                    track: trackNodes.map(n => ({ x: n.position.x, y: n.position.y, z: n.position.z, roll: n.userData.roll })),
                    scenery: sceneryBlocks.map(s => ({
                        x: s.position.x, y: s.position.y, z: s.position.z,
                        sx: s.scale.x, sy: s.scale.y, sz: s.scale.z,
                        ry: s.rotation.y, color: s.material.color.getHex()
                    }))
                };
                const blob = new Blob([JSON.stringify(data)], {type:'application/json'});
                const link = document.createElement('a');
                link.href = URL.createObjectURL(blob); link.download = 'course.json'; link.click();
            };
            document.getElementById('menu-open').onclick = () => document.getElementById('file-input').click();
            document.getElementById('file-input').onchange = (e) => {
                const f = e.target.files[0]; if(!f) return;
                const r = new FileReader();
                r.onload = (ev) => {
                    const data = JSON.parse(ev.target.result);
                    trackNodes.forEach(n => scene.remove(n)); sceneryBlocks.forEach(s => scene.remove(s));
                    trackNodes = []; sceneryBlocks = [];
                    data.track.forEach(n => createNode(new THREE.Vector3(n.x, n.y, n.z), n.roll));
                    data.scenery.forEach(s => createScenery(new THREE.Vector3(s.x, s.y, s.z), new THREE.Vector3(s.sx, s.sy, s.sz), s.color, s.ry));
                    selectItem(null);
                };
                r.readAsText(f);
            };
        }

        function updateUIFromSelection() {
            if(!selectedItem) return;
            if(currentTool === 'add') return;

            document.getElementById('prop-content').style.display = 'block';
            document.getElementById('prop-empty').style.display = 'none';
            
            const isNode = selectedItem.userData.type === 'node';
            document.getElementById('prop-type').value = isNode ? 'Track Node' : 'Scenery Block';
            
            document.getElementById('prop-x').value = selectedItem.position.x;
            document.getElementById('prop-y').value = selectedItem.position.y;
            document.getElementById('prop-z').value = selectedItem.position.z;

            document.getElementById('prop-node-group').style.display = isNode ? 'block' : 'none';
            document.getElementById('prop-scenery-group').style.display = isNode ? 'none' : 'block';

            if(isNode) {
                document.getElementById('prop-roll').value = selectedItem.userData.roll;
            } else {
                document.getElementById('prop-sx').value = selectedItem.scale.x;
                document.getElementById('prop-sy').value = selectedItem.scale.y;
                document.getElementById('prop-sz').value = selectedItem.scale.z;
                document.getElementById('prop-ry').value = Math.round(THREE.MathUtils.radToDeg(selectedItem.rotation.y));
                document.getElementById('prop-color').value = '#' + selectedItem.material.color.getHexString();
            }
        }

        function onResize() {
            const c = document.getElementById('canvas-container');
            camera.aspect = c.clientWidth / c.clientHeight; camera.updateProjectionMatrix();
            renderer.setSize(c.clientWidth, c.clientHeight);
        }

        init();
    </script>
</body>
</html>

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

コメント

コメントするには、 ログイン または 会員登録 をお願いします。
ImageBoosterのコースエディタ|古井和雄
word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word 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