/** * TerrainPanel — левая боковая панель редактора ландшафта. * * Дизайн повторяет Roblox Studio → Terrain Editor: сетка 4×N кнопок-инструментов * (иконка + подпись), снизу — секция «Параметры инструмента» (пока пустая, * заглушка под brush size / strength / material). * * Полный список инструментов (как в RS, перевод на русский): * 1. Выделить (Select) — выбор региона * 2. Преобразовать (Transform) — move/scale/rotate выбранного региона * 3. Заполнить (Fill) — залить регион материалом * 4. Уровень моря (Sea Level) — поставить плоскость воды на высоту Y * 5. Рисовать (Draw) — кистью добавлять voxels * 6. Скульпт (Sculpt) — поднимать/опускать поверхность * 7. Сгладить (Smooth) — сглаживать неровности * 8. Раскрасить (Paint) — менять материал voxel'ов без изменения формы * 9. Выровнять (Flatten) — выровнять по плоскости * * Все обработчики — заглушки (console.log). Подключение к движку — следующий этап. * * Иконки — inline SVG (никаких новых зависимостей, цвет наследуется через * currentColor). Стиль монохромный, под тёмную тему Кубикона. */ import React, { useState, useEffect } from 'react'; import Icon from './Icon'; import cl from './TerrainPanel.module.css'; // ============================================================================ // Inline-SVG иконки. Все 24×24, stroke 1.6, currentColor. Стилизованы под // Roblox Studio Terrain Editor (минималистичные пиктограммы). // ============================================================================ const Ic = { Select: () => ( ), Transform: () => ( ), Fill: () => ( ), SeaLevel: () => ( ), Draw: () => ( ), Sculpt: () => ( ), Smooth: () => ( ), Paint: () => ( ), Flatten: () => ( ), }; // ============================================================================ // Описание всех инструментов — единое место правды, чтобы добавлять/убирать // инструменты было удобно. // ============================================================================ // Полный набор инструментов для VOXEL-режима. const TOOLS_VOXEL = [ { id: 'draw', label: 'Рисовать', icon: Ic.Draw, desc: 'Кисть: добавлять voxel\'ы материала' }, { id: 'sculpt', label: 'Скульпт', icon: Ic.Sculpt, desc: 'Поднимать или опускать поверхность кистью (Shift — опускать)' }, { id: 'smooth', label: 'Сгладить', icon: Ic.Smooth, desc: 'Сгладить неровности поверхности (использует существующие материалы)' }, { id: 'paint', label: 'Раскрасить', icon: Ic.Paint, desc: 'Изменить материал voxel\'ов без изменения формы' }, { id: 'flatten', label: 'Выровнять', icon: Ic.Flatten, desc: 'Выровнять воxel\'ы по горизонтальной плоскости' }, { id: 'erase', label: 'Стереть', icon: Ic.Draw, desc: 'Удалить voxel\'ы в зоне кисти' }, // === Plant-кисти: расстановка декораций === { id: 'plantGrass', label: 'Трава', icon: Ic.Draw, desc: 'Посадить пучки травы (Shift — удалить декорации)' }, { id: 'plantFlower', label: 'Цветы', icon: Ic.Paint, desc: 'Посадить цветы (Shift — удалить декорации)' }, { id: 'plantMushroom', label: 'Грибы', icon: Ic.Smooth, desc: 'Посадить грибы (Shift — удалить)' }, { id: 'plantTree', label: 'Деревья', icon: Ic.Sculpt, desc: 'Посадить деревья (Shift — удалить)' }, ]; // Инструменты для SMOOTH-режима (как в Roblox Studio Terrain Editor). const TOOLS_SMOOTH = [ { id: 'sculpt', label: 'Скульпт', icon: Ic.Sculpt, desc: 'Поднимать поверхность (Shift — опускать)' }, { id: 'smooth', label: 'Сгладить', icon: Ic.Smooth, desc: 'Сгладить неровности' }, { id: 'paint', label: 'Раскрасить', icon: Ic.Paint, desc: 'Перекрасить поверхность' }, { id: 'flatten', label: 'Выровнять', icon: Ic.Flatten, desc: 'Выровнять по плоскости первой точки клика' }, { id: 'fill', label: 'Заполнить', icon: Ic.Fill, desc: 'Залить сферу density=255' }, { id: 'erase', label: 'Стереть', icon: Ic.Draw, desc: 'Удалить материал в сфере' }, // === Plant-кисти: расстановка декораций === { id: 'plantGrass', label: 'Трава', icon: Ic.Draw, desc: 'Посадить пучки травы (Shift — удалить декорации)' }, { id: 'plantFlower', label: 'Цветы', icon: Ic.Paint, desc: 'Посадить цветы (Shift — удалить декорации)' }, { id: 'plantMushroom', label: 'Грибы', icon: Ic.Smooth, desc: 'Посадить грибы (Shift — удалить)' }, { id: 'plantTree', label: 'Деревья', icon: Ic.Sculpt, desc: 'Посадить деревья (Shift — удалить)' }, // === Поштучный выбор/удаление декораций === { id: 'pickDeco', label: 'Выбрать деко', icon: Ic.Draw, desc: 'Кликнуть по дереву/кусту/цветку — выделить, Del — удалить выбранное' }, ]; // Список материалов террейна. У каждого: // id — соответствует ключу в TerrainManager.TERRAIN_MATERIALS // label — отображаемое имя // color — fallback-цвет для случая если PNG-текстура не загрузилась // preview — путь к текстуре для иконки в палитре (32×32 px Kenney pack). // Для grass — берём grass_top.png, для остальных — основную. const TEX = '/kubikon-assets/textures'; // Материалы террейна. Деко-материалы (листва/ствол/цветы/гриб/трава/мох) // убраны — они ставятся отдельными plant-инструментами (Трава/Цветы/Грибы/Деревья). const TERRAIN_MATERIALS = [ { id: 'grass', label: 'Трава', color: '#52b15a', preview: `${TEX}/grass_top.png` }, { id: 'rock', label: 'Камень', color: '#7e7e7e', preview: `${TEX}/greystone.png` }, { id: 'sand', label: 'Песок', color: '#e6d27a', preview: `${TEX}/sand.png` }, { id: 'snow', label: 'Снег', color: '#f5f7fb', preview: `${TEX}/snow.png` }, { id: 'dirt', label: 'Земля', color: '#7c5430', preview: `${TEX}/dirt.png` }, { id: 'water', label: 'Вода', color: '#3a8fd6', preview: `${TEX}/water.png` }, { id: 'asphalt', label: 'Асфальт', color: '#3b3b3b', preview: `${TEX}/stone.png` }, { id: 'concrete', label: 'Бетон', color: '#b8b8b8', preview: `${TEX}/greystone.png` }, { id: 'wood', label: 'Дерево', color: '#a06a3a', preview: `${TEX}/wood.png` }, { id: 'glacier', label: 'Ледник', color: '#c8e6f5', preview: `${TEX}/ice.png` }, { id: 'salt', label: 'Соль', color: '#ecedef', preview: `${TEX}/snow.png` }, { id: 'mud', label: 'Грязь', color: '#553a25', preview: `${TEX}/gravel_dirt.png` }, ]; export default function TerrainPanel({ onBrushChange, onAction }) { // terrainMode: null = экран выбора режима, 'voxel' | 'smooth' = редактор. const [terrainMode, setTerrainMode] = useState(null); const [tool, setTool] = useState('sculpt'); const [material, setMaterial] = useState('grass'); const [brushSize, setBrushSize] = useState(4); const [strength, setStrength] = useState(50); const [shape, setShape] = useState('sphere'); // Каждый раз когда любое поле кисти меняется — пушим в движок. // terrainMode тоже передаём — движок переключает логику. useEffect(() => { if (!onBrushChange) return; // Не пушим пока пользователь не выбрал режим — иначе скоулптится // мимо ожидаемого таргета. if (terrainMode === null) return; onBrushChange({ tool, material, brushSize, strength, shape, terrainMode }); }, [onBrushChange, tool, material, brushSize, strength, shape, terrainMode]); // При переключении в smooth-режим — если текущий material не поддерживается, // переключаем на grass. useEffect(() => { if (terrainMode === 'smooth') { const supported = ['grass', 'rock', 'sand', 'snow', 'dirt', 'water', 'wood', 'glacier']; if (!supported.includes(material)) { setMaterial('grass'); } } }, [terrainMode, material]); // Hotkeys: только пока панель открыта. // 1..9 0 (=10) — выбор материала из палитры (первые 10) // Q/W/E/R/T/Y/U/I/O — выбор инструмента в порядке TOOLS // Ctrl+Z / Ctrl+Y — undo / redo // [ / ] — уменьшить / увеличить размер кисти useEffect(() => { const onKey = (e) => { // Игнорируем когда печатают в input/textarea const tag = (e.target?.tagName || '').toLowerCase(); if (tag === 'input' || tag === 'textarea' || tag === 'select') return; // Ctrl+Z / Ctrl+Y if ((e.ctrlKey || e.metaKey) && e.code === 'KeyZ' && !e.shiftKey) { e.preventDefault(); onAction?.('undo'); return; } if ((e.ctrlKey || e.metaKey) && (e.code === 'KeyY' || (e.code === 'KeyZ' && e.shiftKey))) { e.preventDefault(); onAction?.('redo'); return; } // Escape снимает выделение региона if (e.code === 'Escape') { onAction?.('clear-region'); return; } // [ ] — размер кисти if (e.code === 'BracketLeft') { e.preventDefault(); setBrushSize((s) => Math.max(1, s - 1)); return; } if (e.code === 'BracketRight') { e.preventDefault(); setBrushSize((s) => Math.min(32, s + 1)); return; } // Цифры — выбор материала (1..9, 0 → 10-й) if (e.code && e.code.startsWith('Digit')) { const n = parseInt(e.code.slice(5), 10); const idx = n === 0 ? 9 : n - 1; if (idx >= 0 && idx < TERRAIN_MATERIALS.length) { e.preventDefault(); setMaterial(TERRAIN_MATERIALS[idx].id); } return; } // T/Y/U/I/O/P — основные инструменты (draw/sculpt/smooth/paint/flatten/erase) // F/C/M/V — plant-кисти (Flora/Cvety/Mushrooms/Vegetation-trees) const toolKeyMap = { KeyT: 'draw', KeyY: 'sculpt', KeyU: 'smooth', KeyI: 'paint', KeyO: 'flatten', KeyP: 'erase', KeyF: 'plantGrass', KeyC: 'plantFlower', KeyM: 'plantMushroom', KeyV: 'plantTree', }; if (toolKeyMap[e.code] && !e.ctrlKey && !e.metaKey) { e.preventDefault(); setTool(toolKeyMap[e.code]); } }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); }, [onAction]); const handleTool = (id) => setTool(id); const handleMaterial = (id) => setMaterial(id); const handleShape = (id) => setShape(id); // === Экран выбора режима === if (terrainMode === null) { const modeCard = { display: 'flex', alignItems: 'flex-start', gap: 12, padding: '14px 16px', background: '#2e2e2e', border: '1px solid #3a3a3a', borderRadius: 10, cursor: 'pointer', color: '#e8e8ea', textAlign: 'left', fontFamily: 'inherit', boxShadow: '0 1px 3px rgba(0,0,0,0.3)', width: '100%', }; const modeTitle = { fontSize: 14, fontWeight: 800, color: '#e8e8ea', marginBottom: 6 }; const modeDesc = { fontSize: 12, lineHeight: 1.4, color: '#9a9a9e' }; return (