test17.html

<!DOCTYPE html>

<html lang="ko">

<head>

    <meta charset="UTF-8">

    <meta name="viewport" content="width=device-width, initial-scale=1.0">

    <title>Puzzle Layout Tester v16 (Wide & Scroll)</title>

    <style>

        /* --- Core Layout --- */

        body {

            font-family: 'Segoe UI', sans-serif;

            background-color: #f0f2f5;

            margin: 0; padding: 0;

            overflow: hidden;

            height: 100vh;

            display: flex;

            flex-direction: column;

            user-select: none;

        }

        /* --- Top Control Bar (Expanded) --- */

        .top-bar {

            height: 280px; /* 높이 넉넉하게 확보 */

            background: rgba(255, 255, 255, 0.98);

            border-bottom: 1px solid #ccc;

            display: flex;

            justify-content: center;

            align-items: flex-start;

            gap: 20px; /* 패널 간격 증가 */

            padding-top: 15px;

            z-index: 2000;

            box-shadow: 0 5px 15px rgba(0,0,0,0.05);

            flex-shrink: 0;

        }

        /* --- Control Panel (Wide & Scrollable) --- */

        .control-panel {

            background: #f8f9fa;

            border: 1px solid #ccc;

            border-radius: 8px;

            padding: 12px;

            display: flex;

            flex-direction: column;

            align-items: center;

            

            /* [요청 2] PC 전용 넓은 폭 */

            width: 340px; 

            height: 95%; /* 상단바 꽉 채우기 */

            box-sizing: border-box;

            box-shadow: 0 2px 5px rgba(0,0,0,0.05);

            /* [요청 1] 내용 넘치면 스크롤 발생 */

            overflow-y: auto;

            overflow-x: hidden;

        }

        /* 스크롤바 스타일링 (Chrome/Safari) */

        .control-panel::-webkit-scrollbar { width: 6px; }

        .control-panel::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; }

        .control-panel::-webkit-scrollbar-thumb { background: #ccc; border-radius: 4px; }

        .control-panel::-webkit-scrollbar-thumb:hover { background: #bbb; }

        .panel-title { font-weight: bold; font-size: 14px; margin-bottom: 10px; width: 100%; text-align: center; border-bottom: 2px solid #ddd; padding-bottom: 5px; color:#333; }

        .sub-title { font-size: 11px; color:#555; font-weight:bold; margin-top:10px; margin-bottom:6px; width:100%; text-align:left; padding-left:8px; border-left:3px solid #aaa; background: rgba(0,0,0,0.03); padding-top:2px; padding-bottom:2px;}

        

        .btn-row { display: flex; gap: 6px; justify-content: center; width: 100%; margin-bottom: 6px; flex-wrap: wrap; }

        .sm-btn { padding: 5px 12px; font-size: 11px; cursor: pointer; background: #fff; border: 1px solid #999; border-radius: 4px; min-width: 30px; transition: 0.1s; }

        .sm-btn:hover { background: #eee; }

        .file-btn { background: #e3f2fd; border-color: #2196f3; color: #1565c0; width: 45%; }

        

        .toggle-row {

            display: flex; align-items: center; justify-content: center; gap: 10px; margin-bottom: 10px;

            background: #fff; padding: 5px 15px; border-radius: 20px; border: 1px solid #ccc; width: fit-content;

        }

        .toggle-label { font-size: 12px; font-weight: bold; color: #555; cursor: pointer; }

        .toggle-switch {

            position: relative; width: 40px; height: 20px; background: #ccc; border-radius: 20px; cursor: pointer; transition: 0.3s;

        }

        .toggle-switch::after {

            content: ''; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px;

            background: white; border-radius: 50%; transition: 0.3s; box-shadow: 0 1px 3px rgba(0,0,0,0.3);

        }

        .toggle-active { background: #2196f3; }

        .toggle-active::after { left: 22px; }

        .slider-row { width: 100%; display: flex; justify-content: space-between; align-items: center; font-size: 11px; margin-top: 5px; }

        input[type=range] { flex: 1; margin: 0 10px; cursor: pointer; }

        /* --- [요청 2] New Rotator Layout (Side by Side) --- */

        .rotator-wrapper-wide {

            display: flex;

            flex-direction: row;

            align-items: center;

            justify-content: space-between;

            width: 100%;

            background: white;

            padding: 8px;

            border: 1px dashed #ddd;

            border-radius: 6px;

            box-sizing: border-box;

        }

        .ball-area {

            display: flex; flex-direction: column; align-items: center; gap: 4px;

            border-right: 1px solid #eee; padding-right: 10px; margin-right: 10px;

        }

        .slider-area {

            display: flex; flex-direction: column; gap: 8px; flex: 1;

        }

        .slider-item {

            display: flex; align-items: center; gap: 5px; font-size: 11px;

        }

        .slider-label-mini { width: 40px; font-weight: bold; color: #555; }

        .trackball-container { width: 50px; height: 50px; perspective: 200px; }

        .trackball {

            width: 100%; height: 100%; border-radius: 50%;

            background: radial-gradient(circle at 30% 30%, #fff, #bbb, #444);

            box-shadow: 0 3px 6px rgba(0,0,0,0.3); cursor: grab;

        }

        .trackball:active { cursor: grabbing; transform: scale(0.95); }

        .trackball::after {

            content: ''; position: absolute; top:0; left:0; width:100%; height:100%; border-radius: 50%;

            background: linear-gradient(45deg, transparent 45%, rgba(0,0,0,0.3) 45%, rgba(0,0,0,0.3) 55%, transparent 55%),

                        linear-gradient(-45deg, transparent 45%, rgba(0,0,0,0.3) 45%, rgba(0,0,0,0.3) 55%, transparent 55%);

            pointer-events: none;

        }

        /* D-Pad Horizontal Layout */

        .d-pad-wide {

            display: flex; align-items: center; justify-content: center; gap: 5px;

            background: #fff; padding: 5px; border-radius: 4px; border: 1px solid #eee; width: 100%; box-sizing: border-box;

        }

        .d-btn { padding: 2px 8px; font-size: 10px; cursor: pointer; background: #fff; border: 1px solid #999; border-radius: 3px; min-width: 24px;}

        .d-btn:hover { background: #ddd; }

        /* --- 3D Scene --- */

        .scene-split {

            flex: 1; display: flex; width: 100%; height: 100%; position: relative;

            background: radial-gradient(circle at center, #f0f0f0, #dcdcdc);

            overflow: hidden; 

        }

        .viewport {

            flex: 1; position: relative; overflow: hidden;

            perspective: 1000px; display: flex; justify-content: center; align-items: center;

        }

        .divider { width: 2px; background: rgba(0,0,0,0.1); z-index: 100; }

        .world-root { position: relative; width: 0; height: 0; transform-style: preserve-3d; transition: transform 0.1s linear; }

        .board-plane { position: absolute; top: 0; left: 0; width: 0; height: 0; transform-style: preserve-3d; }

        /* --- Cells --- */

        .cell {

            position: absolute; transform-style: preserve-3d; cursor: pointer;

            transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.3s;

        }

        .face-top {

            position: absolute; top: 0; left: 0; width: 100%; height: 100%;

            background-color: #f0f0f0; display: flex; justify-content: center; align-items: center;

            font-size: 10px; font-weight: bold; color: #555;

            backface-visibility: hidden; border: 1px solid rgba(0,0,0,0.15); box-sizing: border-box;

        }

        .hex .face-top {

            clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);

            border: none; filter: drop-shadow(0 0 1px #888);

        }

        .wall {

            position: absolute; background-color: var(--pillar-color, #a5d6a7);

            backface-visibility: visible; border: 1px solid rgba(0,0,0,0.05); pointer-events: none;

        }

        

        :root { --lift-h: 40px; --face-color: #2e7d32; --pillar-color: #81c784; }

        /* --- States --- */

        .cell.clicked { z-index: 1000 !important; }

        .cell.clicked .face-top {

            background-color: var(--face-color) !important; color: white !important;

            filter: none; border: 1px solid rgba(255,255,255,0.4);

        }

        .cell:hover { transform: translateZ(10px); z-index: 200; }

        .cell:hover .face-top { background-color: #fff9c4; color: black; }

        .cell.highlight .face-top { background-color: #fff9c4 !important; box-shadow: inset 0 0 10px gold; }

        .zone-0 .face-top { background-color: #ffffff; }

        .zone-1 .face-top { background-color: #e8ebf0; }

        .hex.zone-null { display: none !important; }

        .square.zone-null { pointer-events: none; }

        .square.zone-null .face-top {

            background-color: #e0e0e0;

            background-image: repeating-linear-gradient(45deg, transparent, transparent 5px, rgba(0,0,0,0.05) 5px, rgba(0,0,0,0.05) 10px);

            color: #ccc; border-color: #ddd;

        }

        .square.zone-null:hover { transform: none !important; }

        .square.zone-null .face-top { box-shadow: none; }

        #file-input { display: none; }

    </style>

</head>

<body>

    <div class="top-bar">

        

        <div class="control-panel">

            <div class="panel-title">Global Settings</div>

            

            <div class="toggle-row" onclick="toggleViewMode()">

                <span class="toggle-label" id="lbl-default">Default</span>

                <div id="view-toggle" class="toggle-switch"></div>

                <span class="toggle-label" id="lbl-hex" style="color:#aaa;">Hexagon</span>

            </div>

            <div class="btn-row">

                <button class="sm-btn" onclick="changeMode(-1)">&lt;</button>

                <span id="indicator" style="font-weight:bold; margin:0 5px;">■ □ □ □</span>

                <button class="sm-btn" onclick="changeMode(1)">&gt;</button>

            </div>

            <div id="mode-desc" style="font-size:10px; margin-bottom:5px; text-align:center;">기본 모드</div>

            

            <div class="slider-row" style="background:#e8f5e9; padding:8px; border-radius:4px; margin-top: auto;">

                <span style="color:#2e7d32; font-weight:bold;">Pop-Up Height</span>

                <input type="range" min="0" max="100" value="40" oninput="updateLift(this.value)">

                <span id="val-lift" style="width:25px; text-align:right;">40</span>

            </div>

            <div class="sub-title" style="margin-top:10px;">Save/Load</div>

            <div class="btn-row">

                <button class="sm-btn file-btn" onclick="saveToJson()">Save JSON</button>

                <button class="sm-btn file-btn" onclick="document.getElementById('file-input').click()">Load JSON</button>

                <input type="file" id="file-input" accept=".json" onchange="loadFromJson(this)">

            </div>

            <div style="font-size:9px; color:#888; text-align:center; margin-top:2px;">*Auto-saved</div>

        </div>

        <div class="control-panel">

            <div class="panel-title">Hexagon</div>

            

            <div class="sub-title">Rotation & View</div>

            <div class="rotator-wrapper-wide">

                <div class="ball-area">

                    <div class="trackball-container"><div id="ball-hex" class="trackball"></div></div>

                    <div style="font-size:9px; color:#888;">Trackball</div>

                </div>

                <div class="slider-area">

                    <div class="slider-item">

                        <span class="slider-label-mini">Tilt (X)</span>

                        <input type="range" id="slider-hex-x" min="0" max="85" value="55" oninput="manualRotate('hex', 'x', this.value)">

                    </div>

                    <div class="slider-item">

                        <span class="slider-label-mini">Spin (Z)</span>

                        <input type="range" id="slider-hex-z" min="-180" max="180" value="0" oninput="manualRotate('hex', 'z', this.value)">

                    </div>

                </div>

            </div>

            <div class="sub-title">Position & Zoom</div>

            <div class="d-pad-wide">

                <button class="d-btn" onclick="moveCam('hex', 0, -20)">▲</button>

                <button class="d-btn" onclick="moveCam('hex', 0, 20)">▼</button>

                <button class="d-btn" onclick="moveCam('hex', -20, 0)">◀</button>

                <button class="d-btn" onclick="moveCam('hex', 20, 0)">▶</button>

                <button class="d-btn" onclick="resetCam('hex')" title="Reset" style="border-color:#f44336; color:#f44336;">R</button>

            </div>

            <div class="slider-row" style="margin-top:5px;">

                <span style="font-weight:bold;">Zoom</span>

                <input type="range" min="-300" max="300" value="0" step="10" oninput="zoomCam('hex', this.value)" id="zoom-hex">

            </div>

            <div class="sub-title">Layout Data</div>

            <div class="btn-row">

                <button class="sm-btn" onclick="updateScale('hex', 'w', 0.1)">W+</button><button class="sm-btn" onclick="updateScale('hex', 'w', -0.1)">W-</button>

                <button class="sm-btn" onclick="updateScale('hex', 'h', 0.1)">H+</button><button class="sm-btn" onclick="updateScale('hex', 'h', -0.1)">H-</button>

            </div>

            <div class="slider-row"><span>Cell Gap</span><input type="range" max="20" value="0" oninput="updateGap('hex','cell',this.value)" id="gap-hex-c"></div>

            <div class="slider-row"><span>Zone Gap</span><input type="range" max="40" value="0" oninput="updateGap('hex','zone',this.value)" id="gap-hex-z"></div>

        </div>

        <div class="control-panel">

            <div class="panel-title">Square</div>

            

            <div class="sub-title">Rotation & View</div>

            <div class="rotator-wrapper-wide">

                <div class="ball-area">

                    <div class="trackball-container"><div id="ball-sq" class="trackball"></div></div>

                    <div style="font-size:9px; color:#888;">Trackball</div>

                </div>

                <div class="slider-area">

                    <div class="slider-item">

                        <span class="slider-label-mini">Tilt (X)</span>

                        <input type="range" id="slider-sq-x" min="0" max="85" value="55" oninput="manualRotate('sq', 'x', this.value)">

                    </div>

                    <div class="slider-item">

                        <span class="slider-label-mini">Spin (Z)</span>

                        <input type="range" id="slider-sq-z" min="-180" max="180" value="0" oninput="manualRotate('sq', 'z', this.value)">

                    </div>

                </div>

            </div>

            <div class="sub-title">Position & Zoom</div>

            <div class="d-pad-wide">

                <button class="d-btn" onclick="moveCam('sq', 0, -20)">▲</button>

                <button class="d-btn" onclick="moveCam('sq', 0, 20)">▼</button>

                <button class="d-btn" onclick="moveCam('sq', -20, 0)">◀</button>

                <button class="d-btn" onclick="moveCam('sq', 20, 0)">▶</button>

                <button class="d-btn" onclick="resetCam('sq')" title="Reset" style="border-color:#f44336; color:#f44336;">R</button>

            </div>

            <div class="slider-row" style="margin-top:5px;">

                <span style="font-weight:bold;">Zoom</span>

                <input type="range" min="-300" max="300" value="0" step="10" oninput="zoomCam('sq', this.value)" id="zoom-sq">

            </div>

            <div class="sub-title">Layout Data</div>

            <div class="btn-row">

                <button class="sm-btn" onclick="updateScale('sq', 'w', 2)">W+</button><button class="sm-btn" onclick="updateScale('sq', 'w', -2)">W-</button>

                <button class="sm-btn" onclick="updateScale('sq', 'h', 2)">H+</button><button class="sm-btn" onclick="updateScale('sq', 'h', -2)">H-</button>

            </div>

            <div class="slider-row"><span>Cell Gap</span><input type="range" max="20" value="0" oninput="updateGap('sq','cell',this.value)" id="gap-sq-c"></div>

            <div class="slider-row"><span>Zone Gap</span><input type="range" max="40" value="0" oninput="updateGap('sq','zone',this.value)" id="gap-sq-z"></div>

        </div>

    </div>

    <div class="scene-split">

        <div class="viewport"><div id="hex-root" class="world-root"><div id="hex-board" class="board-plane"></div></div></div>

        <div class="divider"></div>

        <div class="viewport"><div id="sq-root" class="world-root"><div id="sq-board" class="board-plane"></div></div></div>

    </div>

    <script>

        const GRID_SIZE = 9;

        const BASE_HEX_SIDE = 24;

        const BASE_SQ_SIDE = 35;

        // Default State

        const defaultState = {

            clicked: [], 

            modeIdx: 0,

            hexView: false,

            scales: { hex: { w: 1.0, h: 1.0 }, sq: { w: BASE_SQ_SIDE, h: BASE_SQ_SIDE } },

            gaps: { hex: { cell: 0, zone: 0 }, sq: { cell: 0, zone: 0 } },

            lift: 40,

            camera: {

                hex: { panX: 0, panY: 0, zoom: 0, rotX: 55, rotZ: 0 },

                sq:  { panX: 0, panY: 0, zoom: 0, rotX: 55, rotZ: 0 }

            }

        };

        let state = JSON.parse(JSON.stringify(defaultState));

        state.clicked = new Set();

        const MODES = [

            { label: "■ □ □ □", desc: "1. 기본 (Pointy)", rotate: false, vec_q: [1, 0], vec_r: [0.5, 0.75] },

            { label: "□ ■ □ □", desc: "2. Y반전 (Pointy)", rotate: false, vec_q: [1, 0], vec_r: [-0.5, 0.75] },

            { label: "□ □ ■ □", desc: "3. 세로 (Flat) - 우상향", rotate: true, vec_r: [0, 1], vec_q: [0.75, -0.5] },

            { label: "□ □ □ ■", desc: "4. 세로 (Flat) - 우하향", rotate: true, vec_r: [0, 1], vec_q: [0.75, 0.5] }

        ];

        const hexBoard = document.getElementById('hex-board');

        const sqBoard = document.getElementById('sq-board');

        const hexRoot = document.getElementById('hex-root');

        const sqRoot = document.getElementById('sq-root');

        function init() {

            loadFromLocalStorage(); 

            createGrids();

            setupTrackball('ball-hex', 'hex');

            setupTrackball('ball-sq', 'sq');

            syncUI(); 

            updateView();

            updateCamera('hex'); updateCamera('sq');

        }

        // --- Persistence ---

        function saveToLocalStorage() {

            const saveObj = { ...state, clicked: Array.from(state.clicked) };

            localStorage.setItem('puzzleConfig_v16', JSON.stringify(saveObj));

        }

        function loadFromLocalStorage() {

            try {

                const raw = localStorage.getItem('puzzleConfig_v16');

                if (raw) {

                    const loaded = JSON.parse(raw);

                    if (loaded.camera && loaded.scales) {

                        state = loaded;

                        state.clicked = new Set(loaded.clicked);

                        return;

                    }

                }

            } catch (e) {

                console.error("Load failed", e);

                localStorage.removeItem('puzzleConfig_v16');

            }

            state = JSON.parse(JSON.stringify(defaultState));

            state.clicked = new Set();

        }

        function saveToJson() {

            const saveObj = { ...state, clicked: Array.from(state.clicked) };

            const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(saveObj, null, 2));

            const downloadAnchorNode = document.createElement('a');

            downloadAnchorNode.setAttribute("href", dataStr);

            downloadAnchorNode.setAttribute("download", "puzzle_layout_save.json");

            document.body.appendChild(downloadAnchorNode);

            downloadAnchorNode.click();

            downloadAnchorNode.remove();

        }

        function loadFromJson(input) {

            const file = input.files[0];

            if (!file) return;

            const reader = new FileReader();

            reader.onload = function(e) {

                try {

                    const loaded = JSON.parse(e.target.result);

                    state = loaded;

                    state.clicked = new Set(loaded.clicked);

                    syncUI();

                    createGrids();

                    updateView();

                    updateCamera('hex'); updateCamera('sq');

                    saveToLocalStorage();

                } catch(err) {

                    alert("Invalid JSON file");

                }

            };

            reader.readAsText(file);

            input.value = '';

        }

        function syncUI() {

            document.getElementById('val-lift').innerText = state.lift;

            document.querySelector('input[oninput*="updateLift"]').value = state.lift;

            

            const toggle = document.getElementById('view-toggle');

            const lblDef = document.getElementById('lbl-default');

            const lblHex = document.getElementById('lbl-hex');

            if(state.hexView) {

                toggle.classList.add('toggle-active');

                lblDef.style.color = '#aaa'; lblHex.style.color = '#1565c0';

            } else {

                toggle.classList.remove('toggle-active');

                lblDef.style.color = '#1565c0'; lblHex.style.color = '#aaa';

            }

            ['hex', 'sq'].forEach(t => {

                document.getElementById(`slider-${t}-x`).value = state.camera[t].rotX;

                document.getElementById(`slider-${t}-z`).value = state.camera[t].rotZ;

                document.getElementById(`zoom-${t}`).value = state.camera[t].zoom;

                document.getElementById(`gap-${t}-c`).value = state.gaps[t].cell;

                document.getElementById(`gap-${t}-z`).value = state.gaps[t].zone;

            });

        }

        // --- Rotation Logic ---

        function updateRotators(target) {

            const c = state.camera[target];

            const ball = document.getElementById('ball-'+target);

            ball.style.transform = `rotateX(${c.rotX}deg) rotateZ(${c.rotZ}deg)`;

            document.getElementById(`slider-${target}-x`).value = c.rotX;

            document.getElementById(`slider-${target}-z`).value = c.rotZ;

        }

        function manualRotate(target, axis, val) {

            state.camera[target][axis === 'x' ? 'rotX' : 'rotZ'] = parseFloat(val);

            updateCamera(target); 

            saveToLocalStorage();

        }

        function setupTrackball(id, t) {

            const ball = document.getElementById(id);

            let drag = false, sx, sy, srx, srz;

            ball.onmousedown = e => { 

                drag=true; sx=e.clientX; sy=e.clientY; 

                srx=state.camera[t].rotX; srz=state.camera[t].rotZ; 

                ball.style.cursor='grabbing'; 

                window.addEventListener('mousemove', move); 

                window.addEventListener('mouseup', up); 

            };

            function move(e) { 

                if(!drag)return; 

                const dx=e.clientX-sx, dy=e.clientY-sy; 

                state.camera[t].rotX = Math.max(0, Math.min(85, srx+dy)); 

                state.camera[t].rotZ = srz+(dx*0.5); 

                updateCamera(t);

                saveToLocalStorage();

            }

            function up() { 

                drag=false; ball.style.cursor='grab'; 

                window.removeEventListener('mousemove', move); 

                window.removeEventListener('mouseup', up); 

            }

        }

        function updateCamera(target) {

            const root = target === 'hex' ? hexRoot : sqRoot;

            const c = state.camera[target];

            root.style.transform = `translateX(${c.panX}px) translateY(${c.panY}px) translateZ(${c.zoom}px) rotateX(${c.rotX}deg) rotateZ(${c.rotZ}deg)`;

            updateRotators(target);

        }

        function moveCam(t, dx, dy) { state.camera[t].panX += dx; state.camera[t].panY += dy; updateCamera(t); saveToLocalStorage(); }

        function zoomCam(t, val) { state.camera[t].zoom = parseInt(val); updateCamera(t); saveToLocalStorage(); }

        function resetCam(t) { 

            state.camera[t] = { panX: 0, panY: 0, zoom: 0, rotX: 55, rotZ: 0 }; 

            syncUI(); updateCamera(t); saveToLocalStorage();

        }

        function toggleViewMode() {

            state.hexView = !state.hexView;

            syncUI();

            if(state.hexView) cleanupInvalidSelections();

            createGrids(); updateView(); renderClickState();

            saveToLocalStorage();

        }

        function cleanupInvalidSelections() {

            const toRemove = [];

            state.clicked.forEach(key => {

                const [q, r] = key.split(',').map(Number);

                if (getZoneType(q, r) === 'zone-null') toRemove.push(key);

            });

            toRemove.forEach(k => state.clicked.delete(k));

        }

        function getZoneType(q, r) {

            if (!state.hexView) return (Math.floor(q/3) + Math.floor(r/3)) % 2 === 0 ? 'zone-0' : 'zone-1';

            const dq = q - 4; const dr = r - 4; const ds = -dq - dr;

            const dist = Math.max(Math.abs(dq), Math.abs(dr), Math.abs(ds));

            if (dist > 4) return 'zone-null';

            return (dist % 2 === 0) ? 'zone-0' : 'zone-1';

        }

        function createGrids() {

            hexBoard.innerHTML = ''; sqBoard.innerHTML = '';

            for(let r=0; r<GRID_SIZE; r++){

                for(let q=0; q<GRID_SIZE; q++){

                    const id = `${q},${r}`;

                    const zoneType = getZoneType(q, r);

                    const hex = document.createElement('div');

                    hex.className = `cell hex ${zoneType}`;

                    hex.dataset.id = id; hex.dataset.q = q; hex.dataset.r = r;

                    hex.innerHTML = `<div class="face-top"><span>${id}</span></div>`;

                    addEvents(hex, q, r);

                    hexBoard.appendChild(hex);

                    const sq = document.createElement('div');

                    sq.className = `cell square ${zoneType}`;

                    sq.dataset.id = id; sq.dataset.q = q; sq.dataset.r = r;

                    sq.innerHTML = `<div class="face-top"><span>${id}</span></div>`;

                    addEvents(sq, q, r);

                    sqBoard.appendChild(sq);

                }

            }

        }

        function addEvents(el, q, r) {

            el.onmousedown = e => e.stopPropagation();

            el.onclick = e => { e.stopPropagation(); toggleClick(q, r); };

            el.onmouseenter = () => highlight(q, r);

            el.onmouseleave = () => unhighlight();

        }

        function toggleClick(q, r) {

            if (state.hexView && getZoneType(q,r) === 'zone-null') return;

            const key = `${q},${r}`;

            if (state.clicked.has(key)) state.clicked.delete(key);

            else state.clicked.add(key);

            renderClickState();

            saveToLocalStorage();

        }

        function renderClickState() {

            document.querySelectorAll('.cell').forEach(el => {

                if (el.classList.contains('zone-null')) return;

                const isClicked = state.clicked.has(el.dataset.id);

                if (isClicked) {

                    el.classList.add('clicked');

                    el.style.transform = `translate(-50%, -50%) translate3d(${el.dataset.tx}px, ${el.dataset.ty}px, ${state.lift}px) rotate(${el.dataset.rot}deg)`;

                    const w = parseFloat(el.style.width); const h = parseFloat(el.style.height);

                    if(el.classList.contains('hex')) createHexWalls(el, w, h, state.lift);

                    else createSquareWalls(el, w, h, state.lift);

                } else {

                    el.classList.remove('clicked');

                    el.style.transform = `translate(-50%, -50%) translate3d(${el.dataset.tx}px, ${el.dataset.ty}px, 0px) rotate(${el.dataset.rot}deg)`;

                    el.querySelectorAll('.wall').forEach(w => w.remove());

                }

            });

        }

        function highlight(q, r) { 

            if (state.hexView && getZoneType(q,r) === 'zone-null') return;

            document.querySelectorAll(`[data-id="${q},${r}"]`).forEach(el => el.classList.add('highlight')); 

        }

        function unhighlight() { document.querySelectorAll('.highlight').forEach(el => el.classList.remove('highlight')); }

        function createSquareWalls(cell, w, h, depth) {

            cell.querySelectorAll('.wall').forEach(el => el.remove());

            const edges = [

                { x: w/2, y: 0, ang: 0, len: w }, { x: w, y: h/2, ang: 90, len: h },

                { x: w/2, y: h, ang: 180, len: w }, { x: 0, y: h/2, ang: 270, len: h }

            ];

            edges.forEach(e => {

                const div = document.createElement('div');

                div.className = 'wall'; div.style.width = `${e.len + 1}px`; div.style.height = `${depth}px`;

                div.style.left = `${e.x}px`; div.style.top = `${e.y}px`;

                div.style.transformOrigin = 'top center';

                div.style.transform = `translate(-50%, 0) rotateZ(${e.ang}deg) rotateX(-90deg)`;

                cell.appendChild(div);

            });

        }

        function createHexWalls(cell, w, h, depth) {

            cell.querySelectorAll('.wall').forEach(el => el.remove());

            const points = [

                {x: 0.5 * w, y: 0}, {x: 1.0 * w, y: 0.25 * h}, {x: 1.0 * w, y: 0.75 * h},

                {x: 0.5 * w, y: 1.0 * h}, {x: 0, y: 0.75 * h}, {x: 0, y: 0.25 * h}

            ];

            for(let i=0; i<6; i++) {

                const p1 = points[i]; const p2 = points[(i+1)%6];

                const mx = (p1.x + p2.x) / 2; const my = (p1.y + p2.y) / 2;

                const dx = p2.x - p1.x; const dy = p2.y - p1.y;

                const len = Math.sqrt(dx*dx + dy*dy); const ang = Math.atan2(dy, dx) * (180 / Math.PI);

                const div = document.createElement('div');

                div.className = 'wall'; div.style.width = `${len + 1}px`; div.style.height = `${depth}px`;

                div.style.left = `${mx}px`; div.style.top = `${my}px`;

                div.style.transformOrigin = 'top center';

                div.style.transform = `translate(-50%, 0) rotateZ(${ang}deg) rotateX(-90deg)`;

                cell.appendChild(div);

            }

        }

        function changeMode(d) { state.modeIdx = (state.modeIdx + d + MODES.length) % MODES.length; updateView(); saveToLocalStorage(); }

        function updateScale(t, a, d) { 

            if(t==='hex') state.scales.hex[a] = Math.max(0.2, parseFloat((state.scales.hex[a]+d).toFixed(1)));

            else state.scales.sq[a] = Math.max(10, state.scales.sq[a]+d);

            updateView(); saveToLocalStorage();

        }

        function updateGap(t, k, v) { state.gaps[t][k] = parseInt(v); updateView(); saveToLocalStorage(); }

        function updateLift(v) { state.lift = parseInt(v); document.getElementById('val-lift').innerText=v; renderClickState(); saveToLocalStorage(); }

        function updateView() {

            const mode = MODES[state.modeIdx];

            document.getElementById('indicator').innerText = mode.label;

            document.getElementById('mode-desc').innerText = mode.desc;

            const hSc = state.scales.hex; const hGp = state.gaps.hex;

            let hW, hH;

            if(mode.rotate) { hH = (2*BASE_HEX_SIDE)*hSc.w; hW = (Math.sqrt(3)*BASE_HEX_SIDE)*hSc.h; }

            else { hW = (Math.sqrt(3)*BASE_HEX_SIDE)*hSc.w; hH = (2*BASE_HEX_SIDE)*hSc.h; }

            let vW = mode.rotate ? hH : hW; let vH = mode.rotate ? hW : hH;

            hexBoard.querySelectorAll('.cell').forEach(hex => {

                if(hex.classList.contains('zone-null')) return;

                const q = parseInt(hex.dataset.q); const r = parseInt(hex.dataset.r);

                const relQ = q - 4; const relR = r - 4;

                const gQ = Math.floor(q/3) - 1; const gR = Math.floor(r/3) - 1;

                const zX = gQ * hGp.zone; const zY = gR * hGp.zone;

                let x, y;

                if(!mode.rotate) {

                    const sW = vW + hGp.cell; const sH = (vH * 0.75) + (hGp.cell * 0.8);

                    x = relQ * sW + relR * (sW * 0.5);

                    if(mode.vec_r[0] < 0) x = relQ * sW + relR * (sW * -0.5);

                    y = relR * sH;

                } else {

                    const sW = (vW * 0.75) + (hGp.cell * 0.8); const sH = vH + hGp.cell;

                    const sign = mode.vec_q[1] > 0 ? 1 : -1;

                    x = relQ * sW; y = relQ * (sH * 0.5 * sign) + relR * sH;

                }

                hex.style.width = `${hW}px`; hex.style.height = `${hH}px`;

                hex.dataset.tx = x + zX; hex.dataset.ty = y + zY; hex.dataset.rot = mode.rotate ? 90 : 0;

                

                if(!hex.classList.contains('clicked')) {

                    hex.style.transform = `translate(-50%, -50%) translate3d(${x+zX}px, ${y+zY}px, 0) rotate(${hex.dataset.rot}deg)`;

                } else {

                    hex.style.transform = `translate(-50%, -50%) translate3d(${x+zX}px, ${y+zY}px, ${state.lift}px) rotate(${hex.dataset.rot}deg)`;

                    createHexWalls(hex, hW, hH, state.lift);

                }

                hex.querySelector('span').style.transform = `rotate(${mode.rotate?-90:0}deg)`;

            });

            const sSc = state.scales.sq; const sGp = state.gaps.sq;

            sqBoard.querySelectorAll('.cell').forEach(sq => {

                const q = parseInt(sq.dataset.q); const r = parseInt(sq.dataset.r);

                const relQ = q - 4; const relR = r - 4;

                const gQ = Math.floor(q/3) - 1; const gR = Math.floor(r/3) - 1;

                const x = relQ * (sSc.w + sGp.cell) + (gQ * sGp.zone);

                const y = relR * (sSc.h + sGp.cell) + (gR * sGp.zone);

                

                sq.style.width = `${sSc.w}px`; sq.style.height = `${sSc.h}px`;

                sq.dataset.tx = x; sq.dataset.ty = y; sq.dataset.rot = 0;

                

                if(!sq.classList.contains('clicked')) {

                    sq.style.transform = `translate(-50%, -50%) translate3d(${x}px, ${y}px, 0)`;

                } else {

                    sq.style.transform = `translate(-50%, -50%) translate3d(${x}px, ${y}px, ${state.lift}px)`;

                    createSquareWalls(sq, sSc.w, sSc.h, state.lift);

                }

            });

        }

        init();

    </script>

</body>

</html>