Initial commit

This commit is contained in:
Dmitry Popov
2024-09-18 01:55:30 +04:00
parent 6a96a5f56e
commit 4136aaad76
1675 changed files with 124664 additions and 1 deletions

View File

@@ -0,0 +1,9 @@
export * from './useMapInit';
export * from './useMapAddSystems';
export * from './useMapUpdateSystems';
export * from './useMapRemoveSystems';
export * from './useCommandsCharacters';
export * from './useCommandsConnections';
export * from './useCommandsConnections';
export * from './useSelectSystem';
export * from './useMapCommands';

View File

@@ -0,0 +1,44 @@
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { useCallback, useRef } from 'react';
import {
CommandCharacterAdded,
CommandCharacterRemoved,
CommandCharactersUpdated,
CommandCharacterUpdated,
CommandPresentCharacters,
} from '@/hooks/Mapper/types';
export const useCommandsCharacters = () => {
const { update } = useMapState();
const ref = useRef({ update });
ref.current = { update };
const charactersUpdated = useCallback((characters: CommandCharactersUpdated) => {
ref.current.update(() => ({ characters: characters.slice() }));
}, []);
const characterAdded = useCallback((value: CommandCharacterAdded) => {
ref.current.update(state => {
return { characters: [...state.characters.filter(x => x.eve_id !== value.eve_id), value] };
});
}, []);
const characterRemoved = useCallback((value: CommandCharacterRemoved) => {
ref.current.update(state => {
return { characters: [...state.characters.filter(x => x.eve_id !== value.eve_id)] };
});
}, []);
const characterUpdated = useCallback((value: CommandCharacterUpdated) => {
ref.current.update(state => {
return { characters: [...state.characters.filter(x => x.eve_id !== value.eve_id), value] };
});
}, []);
const presentCharacters = useCallback((value: CommandPresentCharacters) => {
ref.current.update(() => ({ presentCharacters: value }));
}, []);
return { charactersUpdated, presentCharacters, characterAdded, characterRemoved, characterUpdated };
};

View File

@@ -0,0 +1,25 @@
import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandAddConnections, CommandRemoveConnections, CommandUpdateConnection } from '@/hooks/Mapper/types';
import { convertConnection2Edge } from '@/hooks/Mapper/components/map/helpers';
export const useCommandsConnections = () => {
const rf = useReactFlow();
const ref = useRef({ rf });
ref.current = { rf };
const addConnections = useCallback((systems: CommandAddConnections) => {
ref.current.rf.addEdges(systems.map(convertConnection2Edge));
}, []);
const removeConnections = useCallback((connections: CommandRemoveConnections) => {
ref.current.rf.deleteElements({ edges: connections.map(x => ({ id: x })) });
}, []);
const updateConnection = useCallback((value: CommandUpdateConnection) => {
ref.current.rf.deleteElements({ edges: [value] });
ref.current.rf.addEdges([convertConnection2Edge(value)]);
}, []);
return { addConnections, removeConnections, updateConnection };
};

View File

@@ -0,0 +1,29 @@
import { Node, useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandAddSystems } from '@/hooks/Mapper/types/mapHandlers.ts';
import { convertSystem2Node } from '../../helpers';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
export const useMapAddSystems = () => {
const rf = useReactFlow();
const {
data: { systems },
update,
} = useMapState();
const ref = useRef({ rf, systems, update });
ref.current = { update, systems, rf };
return useCallback(
(systems: CommandAddSystems) => {
const nodes = rf.getNodes();
const prepared: Node[] = systems.filter(x => !nodes.some(y => x.id === y.id)).map(convertSystem2Node);
rf.addNodes(prepared);
ref.current.update({
systems: [...ref.current.systems.filter(sys => systems.some(x => sys.id !== x.id)), ...systems],
});
},
[rf],
);
};

View File

@@ -0,0 +1,32 @@
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { useCallback, useRef } from 'react';
import { CommandKillsUpdated, CommandMapUpdated } from '@/hooks/Mapper/types';
export const useMapCommands = () => {
const { update } = useMapState();
const ref = useRef({ update });
ref.current = { update };
const mapUpdated = useCallback(({ hubs }: CommandMapUpdated) => {
const out: Partial<MapData> = {};
if (hubs) {
out.hubs = hubs;
}
ref.current.update(out);
}, []);
const killsUpdated = useCallback((updated_kills: CommandKillsUpdated) => {
ref.current.update(({ kills }) => {
updated_kills.forEach(kill => {
kills[kill.solar_system_id] = kill.kills;
});
return { kills: { ...kills } };
});
}, []);
return { mapUpdated, killsUpdated };
};

View File

@@ -0,0 +1,70 @@
import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandInit } from '@/hooks/Mapper/types/mapHandlers.ts';
import { convertConnection2Edge, convertSystem2Node } from '../../helpers';
import { MapData, useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
export const useMapInit = () => {
const rf = useReactFlow();
const { data, update } = useMapState();
const ref = useRef({ rf, data, update });
ref.current = { update, data, rf };
return useCallback(
({
systems,
kills,
connections,
wormholes,
characters,
user_characters,
present_characters,
hubs,
}: CommandInit) => {
const { update } = ref.current;
const { rf } = ref.current;
const updateData: Partial<MapData> = {};
if (wormholes) {
updateData.wormholesData = wormholes.reduce((acc, x) => ({ ...acc, [x.name]: x }), {});
}
if (characters) {
updateData.characters = characters.slice();
}
if (user_characters) {
updateData.userCharacters = user_characters;
}
if (present_characters) {
updateData.presentCharacters = present_characters;
}
if (hubs) {
updateData.hubs = hubs;
}
if (systems) {
updateData.systems = systems;
}
if (kills) {
updateData.kills = kills.reduce((acc, x) => ({ ...acc, [x.solar_system_id]: x.kills }), {});
}
update(updateData);
if (systems) {
rf.setNodes(systems.map(convertSystem2Node));
}
if (connections) {
rf.setEdges(connections.map(convertConnection2Edge));
}
},
[],
);
};

View File

@@ -0,0 +1,28 @@
import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandRemoveSystems } from '@/hooks/Mapper/types/mapHandlers.ts';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
export const useMapRemoveSystems = (onSelectionChange: OnMapSelectionChange) => {
const rf = useReactFlow();
const ref = useRef({ onSelectionChange });
ref.current = { onSelectionChange };
return useCallback(
(systems: CommandRemoveSystems) => {
rf.deleteElements({ nodes: systems.map(x => ({ id: `${x}` })) });
const newSelection = rf
.getNodes()
.filter(x => !systems.includes(parseInt(x.id)))
.filter(x => x.selected)
.map(x => x.id);
ref.current.onSelectionChange({
systems: newSelection,
connections: [],
});
},
[rf],
);
};

View File

@@ -0,0 +1,49 @@
import { Node, useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandUpdateSystems } from '@/hooks/Mapper/types/mapHandlers.ts';
import { convertSystem2Node } from '../../helpers/index.ts';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
export const useMapUpdateSystems = () => {
const rf = useReactFlow();
const {
update,
data: { systems },
} = useMapState();
const ref = useRef({ systems, update });
ref.current = { systems, update };
return useCallback(
(systems: CommandUpdateSystems) => {
const nodes = rf.getNodes();
const prepared: Node[] = nodes.map(node => {
const system = systems.find(s => s.id === node.id);
if (system) {
return {
...node,
...convertSystem2Node(system),
};
} else {
return node;
}
});
rf.setNodes(prepared);
const out = ref.current.systems.map(current => {
const newSystem = systems.find(x => current.id === x.id);
if (!newSystem) {
return current;
}
return newSystem;
});
update({ systems: out });
},
[rf, update],
);
};

View File

@@ -0,0 +1,21 @@
import { useReactFlow } from 'reactflow';
import { useCallback, useRef } from 'react';
import { CommandSelectSystem } from '@/hooks/Mapper/types';
export const useSelectSystem = () => {
const rf = useReactFlow();
const ref = useRef({ rf });
ref.current = { rf };
return useCallback((systemId: CommandSelectSystem) => {
if (!ref.current?.rf) {
return;
}
const systemNode = ref.current.rf.getNodes().find(x => x.data.id === systemId);
if (!systemNode) {
return;
}
ref.current.rf.setCenter(systemNode.position.x, systemNode.position.y, { duration: 1000 });
}, []);
};

View File

@@ -0,0 +1,2 @@
export * from './useMapHandlers';
export * from './useUpdateNodes';

View File

@@ -0,0 +1,111 @@
import { ForwardedRef, useImperativeHandle } from 'react';
import {
CommandAddConnections,
CommandAddSystems,
CommandCharacterAdded,
CommandCharacterRemoved,
CommandCharactersUpdated,
CommandCharacterUpdated,
CommandInit,
CommandKillsUpdated,
CommandMapUpdated,
CommandPresentCharacters,
CommandRemoveConnections,
CommandRemoveSystems,
Commands,
CommandSelectSystem,
CommandUpdateConnection,
CommandUpdateSystems,
MapHandlers,
} from '@/hooks/Mapper/types/mapHandlers.ts';
import {
useCommandsCharacters,
useCommandsConnections,
useMapAddSystems,
useMapCommands,
useMapInit,
useMapRemoveSystems,
useMapUpdateSystems,
useSelectSystem,
} from './api';
import { OnMapSelectionChange } from '@/hooks/Mapper/components/map/map.types.ts';
export const useMapHandlers = (ref: ForwardedRef<MapHandlers>, onSelectionChange: OnMapSelectionChange) => {
const mapInit = useMapInit();
const mapAddSystems = useMapAddSystems();
const mapUpdateSystems = useMapUpdateSystems();
const removeSystems = useMapRemoveSystems(onSelectionChange);
const selectSystem = useSelectSystem();
const { addConnections, removeConnections, updateConnection } = useCommandsConnections();
const { mapUpdated, killsUpdated } = useMapCommands();
const { charactersUpdated, presentCharacters, characterAdded, characterRemoved, characterUpdated } =
useCommandsCharacters();
useImperativeHandle(
ref,
() => {
return {
command(type, data) {
switch (type) {
case Commands.init:
mapInit(data as CommandInit);
break;
case Commands.addSystems:
mapAddSystems(data as CommandAddSystems);
break;
case Commands.updateSystems:
mapUpdateSystems(data as CommandUpdateSystems);
break;
case Commands.removeSystems:
removeSystems(data as CommandRemoveSystems);
break;
case Commands.addConnections:
addConnections(data as CommandAddConnections);
break;
case Commands.removeConnections:
removeConnections(data as CommandRemoveConnections);
break;
case Commands.charactersUpdated:
charactersUpdated(data as CommandCharactersUpdated);
break;
case Commands.characterAdded:
characterAdded(data as CommandCharacterAdded);
break;
case Commands.characterRemoved:
characterRemoved(data as CommandCharacterRemoved);
break;
case Commands.characterUpdated:
characterUpdated(data as CommandCharacterUpdated);
break;
case Commands.presentCharacters:
presentCharacters(data as CommandPresentCharacters);
break;
case Commands.updateConnection:
updateConnection(data as CommandUpdateConnection);
break;
case Commands.mapUpdated:
mapUpdated(data as CommandMapUpdated);
break;
case Commands.killsUpdated:
killsUpdated(data as CommandKillsUpdated);
break;
case Commands.selectSystem:
selectSystem(data as CommandSelectSystem);
break;
case Commands.routes:
// do nothing here
break;
default:
console.warn(`Map handlers: Unknown command: ${type}`, data);
break;
}
},
};
},
[],
);
};

View File

@@ -0,0 +1,88 @@
import { useCallback, useEffect, useRef } from 'react';
import { Node, useOnViewportChange, useReactFlow } from 'reactflow';
import { useMapState } from '@/hooks/Mapper/components/map/MapProvider.tsx';
import { SolarSystemRawType } from '@/hooks/Mapper/types';
const useThrottle = () => {
const throttleSeed = useRef<number | null>(null);
const throttleFunction = useRef((func: any, delay = 200) => {
if (!throttleSeed.current) {
// Call the callback immediately for the first time
func();
throttleSeed.current = setTimeout(() => {
throttleSeed.current = null;
}, delay);
}
});
return throttleFunction.current;
};
const X_OFFSET = 50;
const Y_OFFSET = 50;
const isNodeVisible = (node: Node, viewport: { x: number; y: number; width: number; height: number }) => {
const { x: nodeX, y: nodeY } = node.position;
const { width, height } = node;
return (
nodeX + (width ?? 0) + X_OFFSET > viewport.x &&
nodeX - X_OFFSET < viewport.x + viewport.width &&
nodeY + (height ?? 0) + Y_OFFSET > viewport.y &&
nodeY - Y_OFFSET < viewport.y + viewport.height
);
};
export const useUpdateNodes = (nodes: Node<SolarSystemRawType>[]) => {
const { screenToFlowPosition } = useReactFlow();
const throttle = useThrottle();
const { update } = useMapState();
const ref = useRef({ screenToFlowPosition });
ref.current = { screenToFlowPosition };
const getViewport = useCallback(() => {
const clientRect = document.querySelector('.react-flow__renderer')?.getBoundingClientRect();
if (!clientRect) {
return;
}
const { screenToFlowPosition } = ref.current;
const topLeft = screenToFlowPosition({ x: clientRect.left, y: clientRect.top });
const bottomRight = screenToFlowPosition({ x: clientRect.right, y: clientRect.bottom });
return {
x: topLeft.x,
y: topLeft.y,
width: bottomRight.x - topLeft.x,
height: bottomRight.y - topLeft.y,
};
}, []);
const updateNodesVisibility = useCallback(() => {
if (!nodes) {
return;
}
const viewport = getViewport();
if (!viewport) {
const visibleNodes = new Set(nodes.map(x => x.id));
update({ visibleNodes });
return;
}
const visibleNodes = new Set(nodes.filter(x => isNodeVisible(x, viewport)).map(x => x.id));
update({ visibleNodes });
}, [nodes]);
useOnViewportChange({
onChange: () => throttle(updateNodesVisibility.bind(this)),
onEnd: () => throttle(updateNodesVisibility.bind(this)),
});
useEffect(() => {
updateNodesVisibility();
}, [nodes]);
};