/** * Tilemap Details Panel - Right panel with grouped properties * Tilemap 详情面板 - 右侧分组属性面板 */ import React, { useState, useCallback, useRef, useEffect } from 'react'; import { ChevronDown, ChevronRight, Plus, ArrowUp, ArrowDown, Copy, X, Search, Settings, Eye, EyeOff } from 'lucide-react'; import { useTilemapEditorStore, type LayerState } from '../../stores/TilemapEditorStore'; import type { TilemapComponent } from '../../../TilemapComponent'; import '../../styles/TilemapDetailsPanel.css'; interface TilemapDetailsPanelProps { tilemap: TilemapComponent | null; onAddLayer: () => void; onRemoveLayer: (index: number) => void; onMoveLayer: (from: number, to: number) => void; onTilemapChange: () => void; onOpenAssetPicker: () => void; } // Collapsible section component interface SectionProps { title: string; defaultOpen?: boolean; children: React.ReactNode; } const Section: React.FC = ({ title, defaultOpen = true, children }) => { const [isOpen, setIsOpen] = useState(defaultOpen); return (
setIsOpen(!isOpen)} > {isOpen ? : } {title}
{isOpen && (
{children}
)}
); }; // Property row component interface PropertyRowProps { label: string; children: React.ReactNode; indent?: boolean; } const PropertyRow: React.FC = ({ label, children, indent }) => (
{children}
); // Toggle property - unified style matching PropertyInspector interface TogglePropertyProps { label: string; checked: boolean; onChange: (checked: boolean) => void; indent?: boolean; } const ToggleProperty: React.FC = ({ label, checked, onChange, indent }) => (
); // Number input property interface NumberPropertyProps { label: string; value: number; onChange: (value: number) => void; min?: number; max?: number; step?: number; } const NumberProperty: React.FC = ({ label, value, onChange, min, max, step = 1 }) => ( onChange(parseFloat(e.target.value) || 0)} min={min} max={max} step={step} /> ); // Color property - unified style matching PropertyInspector interface ColorPropertyProps { label: string; value: string; onChange: (value: string) => void; showReset?: boolean; } const ColorProperty: React.FC = ({ label, value, onChange }) => { const inputRef = useRef(null); const handlePreviewClick = () => { inputRef.current?.click(); }; return (
onChange(e.target.value)} /> { const val = e.target.value; if (/^#[0-9A-Fa-f]{0,6}$/.test(val)) { onChange(val); } }} onBlur={(e) => { const val = e.target.value; if (!/^#[0-9A-Fa-f]{6}$/.test(val)) { onChange(value); } }} />
); }; export const TilemapDetailsPanel: React.FC = ({ tilemap, onAddLayer, onRemoveLayer, onMoveLayer, onTilemapChange, onOpenAssetPicker }) => { const { layers, currentLayer, setCurrentLayer, toggleLayerVisibility, setLayerOpacity, showCollision, setShowCollision } = useTilemapEditorStore(); // Layer properties state - synced with store's visibility const selectedLayer = layers[currentLayer]; const [hiddenInGame, setHiddenInGame] = useState(false); const [layerCollides, setLayerCollides] = useState(true); const [overrideCollisionThickness, setOverrideCollisionThickness] = useState(false); const [overrideCollisionOffset, setOverrideCollisionOffset] = useState(false); const [collisionThickness, setCollisionThickness] = useState(50.0); const [collisionOffset, setCollisionOffset] = useState(0.0); const [layerColor, setLayerColor] = useState('#ffffff'); // hiddenInEditor is derived from layer visibility (inverse relationship) const hiddenInEditor = selectedLayer ? !selectedLayer.visible : false; const handleHiddenInEditorChange = useCallback((hidden: boolean) => { if (currentLayer >= 0 && currentLayer < layers.length) { toggleLayerVisibility(currentLayer); // Also update tilemap component if (tilemap && tilemap.layers[currentLayer]) { tilemap.layers[currentLayer].visible = !hidden; tilemap.renderDirty = true; onTilemapChange(); } } }, [currentLayer, layers.length, toggleLayerVisibility, tilemap, onTilemapChange]); // Handle eye icon click in layer list const handleLayerVisibilityToggle = useCallback((index: number, e: React.MouseEvent) => { e.stopPropagation(); toggleLayerVisibility(index); // Also update tilemap component if (tilemap && tilemap.layers[index]) { tilemap.layers[index].visible = !tilemap.layers[index].visible; tilemap.renderDirty = true; onTilemapChange(); } }, [toggleLayerVisibility, tilemap, onTilemapChange]); // Colors const [tileGridColor, setTileGridColor] = useState('#333333'); const [multiTileGridColor, setMultiTileGridColor] = useState('#ff0000'); const [layerGridColor, setLayerGridColor] = useState('#00ff00'); const handleLayerSelect = useCallback((index: number) => { setCurrentLayer(index); }, [setCurrentLayer]); const handleMapWidthChange = useCallback((value: number) => { if (tilemap && value > 0) { tilemap.resize(value, tilemap.height, 'bottom-left'); onTilemapChange(); } }, [tilemap, onTilemapChange]); const handleMapHeightChange = useCallback((value: number) => { if (tilemap && value > 0) { tilemap.resize(tilemap.width, value, 'bottom-left'); onTilemapChange(); } }, [tilemap, onTilemapChange]); const handleTileWidthChange = useCallback((value: number) => { if (tilemap && value > 0) { tilemap.tileWidth = value; onTilemapChange(); } }, [tilemap, onTilemapChange]); const handleTileHeightChange = useCallback((value: number) => { if (tilemap && value > 0) { tilemap.tileHeight = value; onTilemapChange(); } }, [tilemap, onTilemapChange]); if (!tilemap) { return (
未选择瓦片地图
); } return (
细节
{/* Tile Map Section */}
图层 {currentLayer + 1}
{/* Tile Layers List */}
{layers.map((layer, index) => (
handleLayerSelect(index)} > {layer.name}
))}
{/* Layer action buttons */}
{/* Selected Layer Section */}
{selectedLayer?.name || '图层 1'} {overrideCollisionThickness && ( )} {overrideCollisionOffset && ( )}
{/* Setup Section */}
{}} step={0.1} /> {}} step={0.1} />
{/* Material Section */}
{/* Advanced Section */}
{}} /> {}} />
{/* Collision Section */}
); }; export default TilemapDetailsPanel;