207 lines
6.8 KiB
Plaintext
207 lines
6.8 KiB
Plaintext
import React, { useState } from 'react';
|
|
import Image from 'next/image';
|
|
import useNavigationStore from '@/app/store/navigationStore';
|
|
import type { Zone } from '@/app/types';
|
|
|
|
interface ToolbarButton {
|
|
icon: string;
|
|
label: string;
|
|
onClick: () => void;
|
|
onMouseDown?: () => void;
|
|
onMouseUp?: () => void;
|
|
active?: boolean;
|
|
children?: ToolbarButton[];
|
|
}
|
|
|
|
interface SceneToolbarProps {
|
|
onZoomIn?: () => void;
|
|
onZoomOut?: () => void;
|
|
onTopView?: () => void;
|
|
onPan?: () => void;
|
|
onSelectModel?: (modelPath: string) => void;
|
|
panActive?: boolean;
|
|
navMenuActive?: boolean;
|
|
}
|
|
|
|
const SceneToolbar: React.FC<SceneToolbarProps> = ({
|
|
onZoomIn,
|
|
onZoomOut,
|
|
onTopView,
|
|
onPan,
|
|
onSelectModel,
|
|
panActive = false,
|
|
navMenuActive = false,
|
|
}) => {
|
|
const [isZoomOpen, setIsZoomOpen] = useState(false);
|
|
const { showMonitoring, openMonitoring, closeMonitoring, currentZones, loadZones, currentObject } = useNavigationStore();
|
|
|
|
const handleToggleNavMenu = () => {
|
|
if (showMonitoring) {
|
|
closeMonitoring();
|
|
} else {
|
|
openMonitoring();
|
|
}
|
|
};
|
|
|
|
const handleHomeClick = async () => {
|
|
if (!onSelectModel) return;
|
|
|
|
try {
|
|
let zones: Zone[] = Array.isArray(currentZones) ? currentZones : [];
|
|
|
|
// Если зоны ещё не загружены, откройте Monitoring и загрузите зоны для текущего объекта
|
|
if ((!zones || zones.length === 0) && currentObject?.id) {
|
|
if (!showMonitoring) {
|
|
openMonitoring();
|
|
}
|
|
await loadZones(currentObject.id);
|
|
zones = useNavigationStore.getState().currentZones || [];
|
|
}
|
|
|
|
if (!Array.isArray(zones) || zones.length === 0) {
|
|
console.warn('No zones available to select a model from.');
|
|
return;
|
|
}
|
|
|
|
const sorted = zones.slice().sort((a: Zone, b: Zone) => {
|
|
const oa = typeof a.order === 'number' ? a.order : 0;
|
|
const ob = typeof b.order === 'number' ? b.order : 0;
|
|
if (oa !== ob) return oa - ob;
|
|
return (a.name || '').localeCompare(b.name || '');
|
|
});
|
|
|
|
const top = sorted[0];
|
|
let chosenPath: string | null = top?.model_path && String(top.model_path).trim() ? top.model_path! : null;
|
|
if (!chosenPath) {
|
|
const nextWithModel = sorted.find((z) => z.model_path && String(z.model_path).trim());
|
|
chosenPath = nextWithModel?.model_path ?? null;
|
|
}
|
|
|
|
if (chosenPath) {
|
|
onSelectModel(chosenPath);
|
|
} else {
|
|
console.warn('No zone has a valid model_path to open.');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error selecting top zone model:', error);
|
|
}
|
|
};
|
|
|
|
const defaultButtons: ToolbarButton[] = [
|
|
{
|
|
icon: '/icons/Zoom.png',
|
|
label: 'Zoom',
|
|
onClick: () => setIsZoomOpen(!isZoomOpen),
|
|
active: isZoomOpen,
|
|
children: [
|
|
{
|
|
icon: '/icons/plus.svg',
|
|
label: 'Zoom In',
|
|
onClick: onZoomIn || (() => {}),
|
|
},
|
|
{
|
|
icon: '/icons/minus.svg',
|
|
label: 'Zoom Out',
|
|
onClick: onZoomOut || (() => {}),
|
|
},
|
|
]
|
|
},
|
|
{
|
|
icon: '/icons/Video.png',
|
|
label: "Top View",
|
|
onClick: onTopView || (() => console.log('Top View')),
|
|
},
|
|
{
|
|
icon: '/icons/Pointer.png',
|
|
label: 'Pan',
|
|
onClick: onPan || (() => console.log('Pan')),
|
|
active: panActive,
|
|
},
|
|
{
|
|
icon: '/icons/Warehouse.png',
|
|
label: 'Home',
|
|
onClick: handleHomeClick,
|
|
},
|
|
{
|
|
icon: '/icons/Layers.png',
|
|
label: 'Levels',
|
|
onClick: handleToggleNavMenu,
|
|
active: navMenuActive,
|
|
},
|
|
];
|
|
|
|
|
|
return (
|
|
<div className="fixed right-5 top-1/2 transform -translate-y-1/2 z-50">
|
|
<div className="flex flex-col gap-0">
|
|
<div
|
|
className="flex flex-col items-center gap-2 py-4 bg-[#161824] rounded-[15px] border border-white/10 shadow-[0_8px_32px_rgba(0,0,0,0.3)]"
|
|
style={{ minHeight: '320px' }}
|
|
>
|
|
{defaultButtons.map((button, index) => (
|
|
<div key={index} className="flex flex-col items-center gap-2">
|
|
<button
|
|
onClick={button.onClick}
|
|
className={`
|
|
relative group flex items-center justify-center w-16 h-12 rounded-lg transition-all duration-200
|
|
hover:bg-blue-600/20 hover:scale-110 hover:shadow-lg
|
|
focus:outline-none focus:ring-2 focus:ring-blue-500/50
|
|
${button.active
|
|
? 'bg-blue-600/30 text-blue-400 shadow-md'
|
|
: 'bg-transparent text-gray-300 hover:text-blue-400'
|
|
}
|
|
`}
|
|
title={button.label}
|
|
>
|
|
<Image
|
|
src={button.icon}
|
|
alt={button.label}
|
|
width={20}
|
|
height={20}
|
|
className="w-5 h-5 transition-transform duration-200 group-hover:scale-110"
|
|
/>
|
|
<div className="absolute right-full mr-3 top-1/2 transform -translate-y-1/2
|
|
opacity-0 group-hover:opacity-100 transition-opacity duration-200
|
|
pointer-events-none z-60">
|
|
<div className="bg-gray-900 text-white text-xs px-2 py-1 rounded
|
|
whitespace-nowrap shadow-lg border border-gray-700">
|
|
{button.label}
|
|
</div>
|
|
<div className="absolute left-full top-1/2 transform -translate-y-1/2
|
|
w-0 h-0 border-t-4 border-t-transparent
|
|
border-b-4 border-b-transparent
|
|
border-l-4 border-l-gray-900">
|
|
</div>
|
|
</div>
|
|
</button>
|
|
{button.active && button.children && (
|
|
<div className="flex flex-col gap-2 mt-2">
|
|
{button.children.map((childButton, childIndex) => (
|
|
<button
|
|
key={childIndex}
|
|
onClick={childButton.onClick}
|
|
onMouseDown={childButton.onMouseDown}
|
|
onMouseUp={childButton.onMouseUp}
|
|
className="relative group flex items-center justify-center w-12 h-10 bg-gray-800/50 rounded-md transition-all duration-200 hover:bg-blue-600/30"
|
|
title={childButton.label}
|
|
>
|
|
<Image
|
|
src={childButton.icon}
|
|
alt={childButton.label}
|
|
width={16}
|
|
height={16}
|
|
className="w-4 h-4"
|
|
/>
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default SceneToolbar; |