294 lines
8.5 KiB
TypeScript
294 lines
8.5 KiB
TypeScript
import React, { useState } from 'react';
|
||
import { getSecurityColor } from '../utils/securityColors';
|
||
|
||
interface MapNodeProps {
|
||
id: string;
|
||
name: string;
|
||
position: { x: number; y: number };
|
||
onClick: () => void;
|
||
onDoubleClick?: (e: React.MouseEvent) => void;
|
||
onDragStart?: (e: React.MouseEvent) => void;
|
||
onDrag?: (e: React.MouseEvent) => void;
|
||
onDragEnd?: (e: React.MouseEvent) => void;
|
||
onContextMenu?: (e: React.MouseEvent) => void;
|
||
type: 'region' | 'system';
|
||
security?: number;
|
||
signatures?: number;
|
||
isDraggable?: boolean;
|
||
disableNavigate?: boolean;
|
||
jumps?: number;
|
||
kills?: number;
|
||
showJumps?: boolean;
|
||
showKills?: boolean;
|
||
viewBoxWidth?: number; // Add viewBox width for scaling calculations
|
||
}
|
||
|
||
export const MapNode: React.FC<MapNodeProps> = ({
|
||
id,
|
||
name,
|
||
position,
|
||
onClick,
|
||
onDoubleClick,
|
||
onDragStart,
|
||
onDrag,
|
||
onDragEnd,
|
||
onContextMenu,
|
||
type,
|
||
security,
|
||
signatures,
|
||
isDraggable = false,
|
||
disableNavigate = false,
|
||
jumps,
|
||
kills,
|
||
showJumps = false,
|
||
showKills = false,
|
||
viewBoxWidth = 1200,
|
||
}) => {
|
||
const [isHovered, setIsHovered] = useState(false);
|
||
const [isDragging, setIsDragging] = useState(false);
|
||
|
||
const handleMouseDown = (e: React.MouseEvent) => {
|
||
console.log('MapNode handleMouseDown', { isDraggable, type, isDragging });
|
||
if (!isDraggable || type !== 'system') return;
|
||
e.stopPropagation();
|
||
onDragStart?.(e);
|
||
};
|
||
|
||
const handleMouseMove = (e: React.MouseEvent) => {
|
||
console.log('MapNode handleMouseMove', { isDragging, isDraggable, type });
|
||
if (!isDragging || !isDraggable || type !== 'system') return;
|
||
e.stopPropagation();
|
||
onDrag?.(e);
|
||
};
|
||
|
||
const handleMouseUp = (e: React.MouseEvent) => {
|
||
console.log('MapNode handleMouseUp', { isDragging, isDraggable, type });
|
||
if (!isDragging || !isDraggable || type !== 'system') return;
|
||
e.stopPropagation();
|
||
setIsDragging(false);
|
||
onDragEnd?.(e);
|
||
};
|
||
|
||
const handleMouseLeave = (e: React.MouseEvent) => {
|
||
console.log('MapNode handleMouseLeave', { isDragging, isDraggable, type });
|
||
if (!isDragging || !isDraggable || type !== 'system') return;
|
||
e.stopPropagation();
|
||
setIsDragging(false);
|
||
onDragEnd?.(e);
|
||
};
|
||
|
||
const nodeColor = security !== undefined ? getSecurityColor(security) : '#a855f7';
|
||
|
||
if (type === 'region') {
|
||
const pillWidth = Math.max(name.length * 5, 40);
|
||
const pillHeight = 18;
|
||
|
||
return (
|
||
<g
|
||
transform={`translate(${position.x}, ${position.y})`}
|
||
className="cursor-pointer select-none"
|
||
onMouseEnter={() => setIsHovered(true)}
|
||
onMouseLeave={() => setIsHovered(false)}
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
onClick();
|
||
}}
|
||
>
|
||
{/* Glow effect */}
|
||
<rect
|
||
x={-pillWidth / 2 - 3}
|
||
y={-pillHeight / 2 - 3}
|
||
width={pillWidth + 6}
|
||
height={pillHeight + 6}
|
||
rx={(pillHeight + 6) / 2}
|
||
fill={nodeColor}
|
||
opacity={isHovered ? 0.3 : 0.1}
|
||
filter="url(#glow)"
|
||
className="transition-all duration-300"
|
||
/>
|
||
|
||
{/* Main pill */}
|
||
<rect
|
||
x={-pillWidth / 2}
|
||
y={-pillHeight / 2}
|
||
width={pillWidth}
|
||
height={pillHeight}
|
||
rx={pillHeight / 2}
|
||
fill={nodeColor}
|
||
stroke="#ffffff"
|
||
strokeWidth="1.5"
|
||
filter="url(#glow)"
|
||
className={`transition-all duration-300 ${isHovered ? 'drop-shadow-lg' : ''
|
||
}`}
|
||
/>
|
||
|
||
{/* Text inside pill - made smaller */}
|
||
<text
|
||
x="0"
|
||
y="3"
|
||
textAnchor="middle"
|
||
fill="#ffffff"
|
||
fontSize="8"
|
||
fontWeight="bold"
|
||
className="transition-all duration-300 pointer-events-none select-none"
|
||
style={{ textShadow: '1px 1px 2px rgba(0,0,0,0.8)' }}
|
||
>
|
||
{name}
|
||
</text>
|
||
</g>
|
||
);
|
||
} else {
|
||
const nodeSize = 6;
|
||
const textOffset = 20;
|
||
|
||
return (
|
||
<g
|
||
transform={`translate(${position.x}, ${position.y})`}
|
||
className={`cursor-pointer select-none ${isDraggable ? 'cursor-move' : ''}`}
|
||
onMouseEnter={() => setIsHovered(true)}
|
||
onMouseLeave={() => setIsHovered(false)}
|
||
onMouseDown={handleMouseDown}
|
||
onClick={(e) => {
|
||
e.stopPropagation();
|
||
onClick();
|
||
}}
|
||
onDoubleClick={(e) => {
|
||
e.stopPropagation();
|
||
onDoubleClick?.(e);
|
||
}}
|
||
onContextMenu={(e) => {
|
||
e.stopPropagation();
|
||
onContextMenu?.(e);
|
||
}}
|
||
>
|
||
{/* Node glow effect */}
|
||
<circle
|
||
r={isHovered ? nodeSize + 4 : nodeSize}
|
||
fill={nodeColor}
|
||
opacity={isHovered ? 0.3 : 0.1}
|
||
filter="url(#glow)"
|
||
className="transition-all duration-300"
|
||
/>
|
||
|
||
{/* Main node - removed stroke to eliminate white border */}
|
||
<circle
|
||
r={nodeSize}
|
||
fill={nodeColor}
|
||
filter="url(#glow)"
|
||
className={`transition-all duration-300 ${isHovered ? 'drop-shadow-lg' : ''
|
||
}`}
|
||
/>
|
||
|
||
{/* Inner core */}
|
||
<circle
|
||
r={nodeSize - 3}
|
||
fill={isHovered ? '#ffffff' : nodeColor}
|
||
opacity={0.8}
|
||
className="transition-all duration-300"
|
||
/>
|
||
|
||
{/* Node label - fixed visual size regardless of zoom */}
|
||
<text
|
||
x="0"
|
||
y={textOffset}
|
||
textAnchor="middle"
|
||
fill="#ffffff"
|
||
fontSize="10"
|
||
fontWeight="bold"
|
||
className={`transition-all duration-300 ${isHovered ? 'fill-purple-200' : 'fill-white'
|
||
} pointer-events-none select-none`}
|
||
style={{
|
||
textShadow: '2px 2px 4px rgba(0,0,0,0.8)',
|
||
vectorEffect: 'non-scaling-stroke'
|
||
}}
|
||
transform={`scale(${1 / (1200 / viewBoxWidth)})`}
|
||
transformOrigin="0 0"
|
||
>
|
||
{name} {security !== undefined && (
|
||
<tspan fill={getSecurityColor(security)}>{security.toFixed(1)}</tspan>
|
||
)}
|
||
</text>
|
||
|
||
{/* Dynamic text positioning based on what's shown */}
|
||
{(() => {
|
||
let currentY = textOffset + 15;
|
||
const textElements = [];
|
||
|
||
// Add signatures if present
|
||
if (signatures !== undefined && signatures > 0) {
|
||
textElements.push(
|
||
<text
|
||
key="signatures"
|
||
x="0"
|
||
y={currentY}
|
||
textAnchor="middle"
|
||
fill="#a3a3a3"
|
||
fontSize="12"
|
||
className="pointer-events-none select-none"
|
||
style={{
|
||
textShadow: '1px 1px 2px rgba(0,0,0,0.8)',
|
||
vectorEffect: 'non-scaling-stroke'
|
||
}}
|
||
transform={`scale(${1 / (1200 / viewBoxWidth)})`}
|
||
transformOrigin="0 0"
|
||
>
|
||
📡 {signatures}
|
||
</text>
|
||
);
|
||
currentY += 15;
|
||
}
|
||
|
||
// Add jumps if enabled and present
|
||
if (showJumps && jumps !== undefined) {
|
||
textElements.push(
|
||
<text
|
||
key="jumps"
|
||
x="0"
|
||
y={currentY}
|
||
textAnchor="middle"
|
||
fill="#60a5fa"
|
||
fontSize="10"
|
||
className="pointer-events-none select-none"
|
||
style={{
|
||
textShadow: '1px 1px 2px rgba(0,0,0,0.8)',
|
||
vectorEffect: 'non-scaling-stroke'
|
||
}}
|
||
transform={`scale(${1 / (1200 / viewBoxWidth)})`}
|
||
transformOrigin="0 0"
|
||
>
|
||
🚀 {jumps}
|
||
</text>
|
||
);
|
||
currentY += 15;
|
||
}
|
||
|
||
// Add kills if enabled and present
|
||
if (showKills && kills !== undefined) {
|
||
textElements.push(
|
||
<text
|
||
key="kills"
|
||
x="0"
|
||
y={currentY}
|
||
textAnchor="middle"
|
||
fill="#f87171"
|
||
fontSize="10"
|
||
className="pointer-events-none select-none"
|
||
style={{
|
||
textShadow: '1px 1px 2px rgba(0,0,0,0.8)',
|
||
vectorEffect: 'non-scaling-stroke'
|
||
}}
|
||
transform={`scale(${1 / (1200 / viewBoxWidth)})`}
|
||
transformOrigin="0 0"
|
||
>
|
||
⚔️ {kills}
|
||
</text>
|
||
);
|
||
}
|
||
|
||
return textElements;
|
||
})()}
|
||
</g>
|
||
);
|
||
}
|
||
};
|