feat(frontend): implement via mode for setting routes with waypoints
This commit is contained in:
@@ -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();
|
||||||
|
@@ -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)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
Reference in New Issue
Block a user