feat(frontend): implement via mode for setting routes with waypoints

This commit is contained in:
2025-08-09 19:57:04 +02:00
parent cd1cc6dc5f
commit ef57bf4cde
2 changed files with 51 additions and 66 deletions

View File

@@ -67,14 +67,11 @@ export const MapNode: React.FC<MapNodeProps> = ({
onDragEnd?.(e); onDragEnd?.(e);
}; };
const nodeColor = security !== undefined const nodeColor = security !== undefined ? getSecurityColor(security) : '#a855f7';
? getSecurityColor(security)
: '#a855f7'; // fallback purple color
if (type === 'region') { if (type === 'region') {
// Further reduce region size to prevent overlap - made even smaller const pillWidth = Math.max(name.length * 5, 40);
const pillWidth = Math.max(name.length * 5, 40); // Reduced from 8 to 5, min from 60 to 40 const pillHeight = 18;
const pillHeight = 18; // Reduced from 24 to 18
return ( return (
<g <g
@@ -131,9 +128,8 @@ export const MapNode: React.FC<MapNodeProps> = ({
</g> </g>
); );
} else { } else {
// Render system as a dot with external label
const nodeSize = 6; const nodeSize = 6;
const textOffset = 20; // Position text below the dot - moved down more const textOffset = 20;
return ( return (
<g <g
@@ -144,7 +140,7 @@ export const MapNode: React.FC<MapNodeProps> = ({
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
if (!disableNavigate) onClick(); onClick();
}} }}
onDoubleClick={(e) => { onDoubleClick={(e) => {
e.stopPropagation(); e.stopPropagation();

View File

@@ -70,14 +70,34 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
const svgRef = useRef<SVGSVGElement>(null); const svgRef = useRef<SVGSVGElement>(null);
const [viaMode, setViaMode] = useState(false); const [viaMode, setViaMode] = useState(false);
const [viaDest, setViaDest] = useState<string | null>(null);
const [viaQueue, setViaQueue] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => { const onKeyDown = async (e: KeyboardEvent) => {
if (e.key === 'Escape') setViaMode(false); if (e.key === 'Escape' && viaMode) {
// Commit the route: post destination then queued waypoints
try {
if (!(await ensureAnyLoggedIn())) return;
if (viaDest) {
await SetDestinationForAll(viaDest, true, false);
for (const name of viaQueue) {
await AddWaypointForAllByName(name, false);
}
toast({ title: 'Route set', description: `${viaDest}${viaQueue.length ? ' via ' + viaQueue.join(', ') : ''}` });
}
} catch (err: any) {
toast({ title: 'Failed to set route', description: String(err), variant: 'destructive' });
} finally {
setViaMode(false);
setViaDest(null);
setViaQueue([]);
}
}
}; };
window.addEventListener('keydown', onKeyDown); window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown); return () => window.removeEventListener('keydown', onKeyDown);
}, []); }, [viaMode, viaDest, viaQueue]);
const { data: rsystems, isLoading, error } = useRegionData(regionName); const { data: rsystems, isLoading, error } = useRegionData(regionName);
useEffect(() => { useEffect(() => {
@@ -85,18 +105,14 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
setSystems(rsystems); setSystems(rsystems);
}, [rsystems, isLoading, error]); }, [rsystems, isLoading, error]);
// Process connections when systems or nodePositions change
useEffect(() => { useEffect(() => {
if (!systems || systems.size === 0) return; if (!systems || systems.size === 0) return;
console.log("Computing node positions");
const positions = computeNodePositions(systems); const positions = computeNodePositions(systems);
setPositions(positions); setPositions(positions);
console.log("Computing node connections");
const connections = computeNodeConnections(systems); const connections = computeNodeConnections(systems);
setConnections(connections); setConnections(connections);
}, [systems]); }, [systems]);
// Load wormhole systems on mount if in wormhole region
useEffect(() => { useEffect(() => {
if (isWormholeRegion) { if (isWormholeRegion) {
loadWormholeSystems().then(wormholeSystems => { loadWormholeSystems().then(wormholeSystems => {
@@ -121,14 +137,13 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
const handleSystemClick = async (systemName: string) => { const handleSystemClick = async (systemName: string) => {
if (viaMode) { if (viaMode) {
try { setViaQueue(prev => {
if (!(await ensureAnyLoggedIn())) return; if (prev.includes(systemName)) return prev;
await AddWaypointForAllByName(systemName, false); const next = [...prev, systemName];
toast({ title: 'Waypoint added', description: systemName }); return next;
} catch (e: any) { });
console.error('Append waypoint failed:', e); console.log('Queued waypoint:', systemName);
toast({ title: 'Failed to add waypoint', description: String(e), variant: 'destructive' }); toast({ title: 'Waypoint queued', description: systemName });
}
return; return;
} }
if (focusSystem === systemName) return; if (focusSystem === systemName) return;
@@ -268,36 +283,20 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
if (!svgRef.current) return; if (!svgRef.current) return;
setIsPanning(true); setIsPanning(true);
const rect = svgRef.current.getBoundingClientRect(); const rect = svgRef.current.getBoundingClientRect();
setLastPanPoint({ setLastPanPoint({ x: e.clientX - rect.left, y: e.clientY - rect.top });
x: e.clientX - rect.left,
y: e.clientY - rect.top
});
}, []); }, []);
const handleMouseMove = useCallback((e: React.MouseEvent) => { const handleMouseMove = useCallback((e: React.MouseEvent) => {
if (!isPanning || !svgRef.current) return; if (!isPanning || !svgRef.current) return;
const rect = svgRef.current.getBoundingClientRect(); const rect = svgRef.current.getBoundingClientRect();
const currentPoint = { const currentPoint = { x: e.clientX - rect.left, y: e.clientY - rect.top };
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
const deltaX = (lastPanPoint.x - currentPoint.x) * (viewBox.width / rect.width); const deltaX = (lastPanPoint.x - currentPoint.x) * (viewBox.width / rect.width);
const deltaY = (lastPanPoint.y - currentPoint.y) * (viewBox.height / rect.height); const deltaY = (lastPanPoint.y - currentPoint.y) * (viewBox.height / rect.height);
setViewBox(prev => ({ ...prev, x: prev.x + deltaX, y: prev.y + deltaY }));
setViewBox(prev => ({
...prev,
x: prev.x + deltaX,
y: prev.y + deltaY
}));
setLastPanPoint(currentPoint); setLastPanPoint(currentPoint);
}, [isPanning, lastPanPoint, viewBox.width, viewBox.height]); }, [isPanning, lastPanPoint, viewBox.width, viewBox.height]);
const handleMouseUp = useCallback(() => { const handleMouseUp = useCallback(() => { setIsPanning(false); }, []);
setIsPanning(false);
}, []);
const handleWheel = useCallback((e: React.WheelEvent) => { const handleWheel = useCallback((e: React.WheelEvent) => {
e.preventDefault(); e.preventDefault();
@@ -403,16 +402,18 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
const onSetDestination = async (systemName: string, wantVia: boolean) => { const onSetDestination = async (systemName: string, wantVia: boolean) => {
try { try {
if (!(await ensureAnyLoggedIn())) return; if (!(await ensureAnyLoggedIn())) return;
if (!viaMode) { if (wantVia) {
setViaDest(systemName);
setViaQueue([]);
setViaMode(true);
console.log('Via mode start, dest:', systemName);
toast({ title: 'Via mode', description: `Destination ${systemName}. Click systems to add waypoints. Esc to commit.` });
} else {
await SetDestinationForAll(systemName, true, false); await SetDestinationForAll(systemName, true, false);
toast({ title: 'Destination set', description: systemName }); toast({ title: 'Destination set', description: systemName });
if (wantVia) setViaMode(true);
} else {
await AddWaypointForAllByName(systemName, false);
toast({ title: 'Waypoint added', description: systemName });
} }
} catch (e: any) { } catch (e: any) {
console.error('Set destination failed:', e); console.error('Failed to set destination:', e);
toast({ title: 'Failed to set destination', description: String(e), variant: 'destructive' }); toast({ title: 'Failed to set destination', description: String(e), variant: 'destructive' });
} }
}; };
@@ -459,20 +460,8 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
viewBox={`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`} viewBox={`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`}
className="cursor-grab active:cursor-grabbing" className="cursor-grab active:cursor-grabbing"
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onMouseMove={(e) => { onMouseMove={(e) => { if (isPanning) { handleMouseMove(e); } else if (draggingNode) { handleSvgMouseMove(e); } }}
if (isPanning) { onMouseUp={(e) => { if (isPanning) { handleMouseUp(); } else if (draggingNode) { handleSvgMouseUp(e); } }}
handleMouseMove(e);
} else {
handleSvgMouseMove(e);
}
}}
onMouseUp={(e) => {
if (isPanning) {
handleMouseUp();
} else {
handleSvgMouseUp(e);
}
}}
onMouseLeave={handleMouseUp} onMouseLeave={handleMouseUp}
onWheel={handleWheel} onWheel={handleWheel}
onDoubleClick={handleMapDoubleClick} onDoubleClick={handleMapDoubleClick}
@@ -554,7 +543,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
{viaMode && ( {viaMode && (
<div className="absolute top-2 right-2 px-2 py-1 rounded bg-emerald-600 text-white text-xs shadow"> <div className="absolute top-2 right-2 px-2 py-1 rounded bg-emerald-600 text-white text-xs shadow">
VIA mode (Esc to exit) VIA mode: Dest {viaDest} ({viaQueue.length} waypoints). Esc to commit
</div> </div>
)} )}
@@ -567,7 +556,7 @@ export const RegionMap = ({ regionName, focusSystem, isCompact = false, isWormho
onRename={(newName) => handleRenameSystem(contextMenu.system.solarSystemName, newName)} onRename={(newName) => handleRenameSystem(contextMenu.system.solarSystemName, newName)}
onDelete={handleDeleteSystem} onDelete={handleDeleteSystem}
onClearConnections={handleClearConnections} onClearConnections={handleClearConnections}
onSetDestination={onSetDestination} onSetDestination={(systemName) => onSetDestination(systemName, true)}
onClose={() => setContextMenu(null)} onClose={() => setContextMenu(null)}
/> />
)} )}