Merge branch 'demo4' into lpd

This commit is contained in:
polpol 2024-05-08 21:52:42 -07:00
commit 95bc2ff393
26 changed files with 675 additions and 194 deletions

View File

@ -10,3 +10,4 @@ and update these areas when we add or remove something.
memory system, make sure we can store memory of how things have interacted. something like when a button was last pressed,
why it was pressed, in response to what, etc. every single thing should connect and be traceable
widgets should have either 1 or any number of there own existance
widgets that can be more than one should have some parameters like "noUpdate" or "updateWhenFull" so we can control when more widgets can be placed and when they can be updated

9
package-lock.json generated
View File

@ -17,6 +17,7 @@
"react-icons": "^5.0.1",
"react-redux": "^9.1.0",
"react-router-dom": "^6.23.0",
"react-timer-hook": "^3.0.7",
"redux-state-sync": "^3.1.4",
"typia": "^5.5.10",
"uuid": "^9.0.1",
@ -8847,6 +8848,14 @@
"react-dom": ">=16.8"
}
},
"node_modules/react-timer-hook": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/react-timer-hook/-/react-timer-hook-3.0.7.tgz",
"integrity": "sha512-ATpNcU+PQRxxfNBPVqce2+REtjGAlwmfoNQfcEBMZFxPj0r3GYdKhyPHdStvqrejejEi0QvqaJZjy2lBlFvAsA==",
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/read-cache": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",

View File

@ -25,6 +25,7 @@
"react-icons": "^5.0.1",
"react-redux": "^9.1.0",
"react-router-dom": "^6.23.0",
"react-timer-hook": "^3.0.7",
"redux-state-sync": "^3.1.4",
"typia": "^5.5.10",
"uuid": "^9.0.1",

View File

@ -3,18 +3,17 @@ import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Minimap from 'src/pages/Minimap';
import LeftScreen from 'src/pages/LeftScreen';
import RightScreen from 'src/pages/RightScreen';
import Root from 'src/pages/Root';
import useMoveShips from 'src/hooks/useMoveShips';
import Layout from 'src/pages/Layout';
import Prototype from 'src/pages/Prototype';
const App = () => {
useMoveShips();
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Root />}>
<Route path="/" element={<Layout />}>
<Route path="/prototype" element={<Prototype />} />
<Route path="minimap" element={<Minimap />} />
<Route path="left-screen" element={<LeftScreen />} />
<Route path="pearce-screen" element={<LeftScreen />} />
<Route path="right-screen" element={<RightScreen />} />
</Route>
</Routes>

70
src/components/Home.tsx Normal file
View File

@ -0,0 +1,70 @@
import { NavLink } from 'react-router-dom';
import { useStopWatch } from 'src/hooks/useStopWatch';
import Spinner from 'src/ui/Spinner';
const Home = () => {
const { hours, minutes, seconds } = useStopWatch();
return (
<div
className="h-screen flex items-center
justify-center flex-col gap-10"
>
<p className="text-5xl text-blue-700">Conversation Manager</p>
<div>
<div className="flex items-center justify-center gap-4 mb-2">
<span className="text-2xl">Running</span>
<Spinner />
</div>
<span className="text-md">
Time elpased: {hours}:{minutes}:{seconds}
</span>
</div>
<div className="mt-10">
<p className="text-3xl text-center">Pages:</p>
<div className="flex items-center justify-center gap-2 mt-4">
<div>
<NavLink
to="/left-screen"
target="_blank"
className="w-24 bg-transparent hover:bg-blue-500
text-blue-700 font-semibold hover:text-white border border-blue-500 hover:border-transparent
rounded text-sm px-2 py-1 text-center"
>
Left Screen
</NavLink>
</div>
<div>
<NavLink
to="/minimap"
target="_blank"
className="w-24 bg-transparent hover:bg-blue-500
text-blue-700 font-semibold hover:text-white border border-blue-500 hover:border-transparent
rounded text-sm px-2 py-1 text-center"
>
Minimap
</NavLink>
</div>
<div>
<NavLink
to="/right-screen"
target="_blank"
className="w-24 bg-transparent hover:bg-blue-500
text-blue-700 font-semibold hover:text-white border border-blue-500 hover:border-transparent
rounded text-sm px-2 py-1 text-center"
>
Right Screen
</NavLink>
</div>
</div>
</div>
</div>
);
};
export default Home;

View File

@ -22,6 +22,7 @@ export function findElementsInGaze(
radius: number,
inCirclePercentageThresh: number,
elementPercentageThesh: number,
screen: string,
) {
const elemsInGaze: ElementInGaze[] = [];
@ -34,22 +35,25 @@ export function findElementsInGaze(
Object.keys(widgets).forEach((widgetId) => {
const widget = widgets[widgetId];
//find the widgets that are within our circle
let isIn = false;
for (let x = widget.x; x < widget.x + widget.w; x++) {
//find the number of pixels within the element that are in the gaze circle
if (!isIn) {
for (let y = widget.y; y < widget.y + widget.h; y++) {
if (distance(x, y, mousePosition.x, mousePosition.y) < radius) {
widgetsInGaze.push(widget);
isIn = true;
console.log('isin!');
break;
if (widget.screen === screen){ //make sure the widget we are on is in the screen we are interacting with and not a different screen
//find the widgets that are within our circle
let isIn = false;
for (let x = widget.x; x < widget.x + widget.w; x++) {
//find the number of pixels within the element that are in the gaze circle
if (!isIn) {
for (let y = widget.y; y < widget.y + widget.h; y++) {
if (distance(x, y, mousePosition.x, mousePosition.y) < radius) {
widgetsInGaze.push(widget);
isIn = true;
//console.log('isin!');
break;
}
}
}
}
}
// const topLeft: Position = {x:widget.x, y:widget.y};
// const topLeft: Position = {x:widget.x, y:widget.y}; //old bad way without distance formula
// const topRight: Position = {x:widget.x+widget.w, y:widget.y};
// const bottomLeft: Position = {x:widget.x, y:widget.y+widget.h};
// const bottomRight: Position = {x:widget.x+widget.w, y:widget.y+widget.h};
@ -64,9 +68,9 @@ export function findElementsInGaze(
// }
});
if (widgetsInGaze.length > 0) {
//if (widgetsInGaze.length > 0) {
//console.log("widgets in gaze:", widgetsInGaze)
}
//}
widgetsInGaze.forEach(function (widget, widgetIndex) {
widget.elements.forEach(function (element, elementIndex) {

View File

@ -12,7 +12,7 @@ export function useKeyDown() {
useEffect(() => {
function handleKeyDown(ev: KeyboardEvent) {
setKeyDown(ev.code);
console.log('Key pressed: ' + ev.code);
//console.log('Key pressed: ' + ev.code);
}
document.addEventListener('keydown', handleKeyDown);

View File

@ -12,7 +12,7 @@ export function useKeyUp() {
useEffect(() => {
function handleKeyUp(ev: KeyboardEvent) {
setKeyUp(ev.code);
console.log('Key pressed: ' + ev.code);
//console.log('Key pressed: ' + ev.code);
}
document.addEventListener('keyup', handleKeyUp);

View File

@ -15,7 +15,7 @@ export function useMouseButtonDown() {
useEffect(() => {
function handleMouseButtonDown(ev: MouseEvent) {
console.log('Mouse button pressed: ' + ev.button);
//console.log('Mouse button pressed: ' + ev.button);
if (ev.button === 0 || ev.button === 1 || ev.button === 2 || ev.button === 3 ) {
setMouseButtDown(ev.button.toString());
//console.log('Mouse button pressed: ' + ev.button);

View File

@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'src/redux/hooks';
import {
getDrones,
getOwnship,
updateWidget,
updateShipPosition,
} from 'src/redux/slices/minimapSlice';
import { OWNSHIP_TRAJECTORY } from 'src/utils/constants';
@ -12,6 +12,10 @@ const useMoveShips = () => {
const ownship = useAppSelector(getOwnship);
const drones = useAppSelector(getDrones);
useEffect(() => {
console.log('drones', drones);
}, [drones.length]);
useEffect(() => {
if (!ownship) return;
@ -23,11 +27,11 @@ const useMoveShips = () => {
) {
// only update ownship position if within bounds
dispatch(
updateWidget({
...ownship,
x: ownship.x + OWNSHIP_TRAJECTORY.xSpeed,
y: ownship.y - OWNSHIP_TRAJECTORY.ySpeed,
}),
updateShipPosition(
ownship.id,
ownship.x + OWNSHIP_TRAJECTORY.xSpeed,
ownship.y - OWNSHIP_TRAJECTORY.ySpeed,
),
);
}
}, 500);
@ -72,11 +76,11 @@ const useMoveShips = () => {
}
dispatch(
updateWidget({
...drone,
x: drone.x + droneMove.x,
y: drone.y + droneMove.y,
}),
updateShipPosition(
drone.id,
drone.x + droneMove.x,
drone.y + droneMove.y,
),
);
});
}, 1500);
@ -86,7 +90,7 @@ const useMoveShips = () => {
// dependencies omitted because drones array is changing too frequently
// some warning/issue of selector returning different values despite same parameters
/* eslint-disable-next-line react-hooks/exhaustive-deps */
}, []);
}, [drones.length]);
};
export default useMoveShips;

20
src/hooks/useStopWatch.ts Normal file
View File

@ -0,0 +1,20 @@
import { useStopwatch } from 'react-timer-hook';
export type StopWatch = {
hours: string;
minutes: string;
seconds: string;
};
export const useStopWatch = (): StopWatch => {
const { hours, minutes, seconds } = useStopwatch({ autoStart: true });
let hoursStr = hours.toString();
let minutesStr = minutes.toString();
let secondsStr = seconds.toString();
if (hours / 10 < 1) hoursStr = `0${hours}`;
if (minutes / 10 < 1) minutesStr = `0${minutes}`;
if (seconds / 10 < 1) secondsStr = `0${seconds}`;
return { hours: hoursStr, minutes: minutesStr, seconds: secondsStr };
};

134
src/pages/Layout.tsx Normal file
View File

@ -0,0 +1,134 @@
import { useEffect } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import Gaze from 'src/ui/Gaze';
// ~~~~~~~ Redux ~~~~~~~
import { useAppDispatch, useAppSelector } from 'src/redux/hooks';
import { getSections, getWidgets } from 'src/redux/slices/minimapSlice';
import {
addKeyDown,
getElementsInGaze,
getGazesAndKeys,
removeKeyDown,
setElementsInGaze,
type ElementInGaze,
} from 'src/redux/slices/gazeSlice';
// ~~~~~~~ Cusdom Hooks ~~~~~~~
import { useKeyDown } from 'src/hooks/useKeyDown';
import { useMousePosition } from 'src/hooks/useMousePosition';
import { useKeyUp } from 'src/hooks/useKeyUp';
import { useMouseButtonDown } from 'src/hooks/useMouseButtonDown';
import { useMouseButtonUp } from 'src/hooks/useMouseButtonUp';
import { findElementsInGaze } from 'src/hooks/findElementsInGaze';
// ~~~~~~~ Constants ~~~~~~~
import { GAZE_RADIUS } from 'src/utils/constants';
const CIRCLE_PERCENTAGE_THRESH = 0.1;
const ELEMENT_PERCENTAGE_THRESH = 0.1;
const Layout = () => {
// ~~~~~ React Router ~~~~~~
const { pathname } = useLocation();
const navigate = useNavigate();
// ~~~~~ Custom Hooks ~~~~~~
const mousePosition = useMousePosition();
const keyDown = useKeyDown();
const keyUp = useKeyUp();
const mouseButtonDown = useMouseButtonDown();
const mouseButtonUp = useMouseButtonUp();
// ~~~~~ Selectors ~~~~~~
const sections = useAppSelector(getSections);
const widgets = useAppSelector(getWidgets);
const gazesAndKeys = useAppSelector(getGazesAndKeys);
const elemsInGaze: ElementInGaze[] = useAppSelector(getElementsInGaze);
const dispatch = useAppDispatch();
// on mouse position move, check for elements in gaze
useEffect(() => {
const elementsInGaze = findElementsInGaze(
mousePosition,
dispatch,
widgets,
GAZE_RADIUS,
CIRCLE_PERCENTAGE_THRESH,
ELEMENT_PERCENTAGE_THRESH,
pathname,
);
dispatch(setElementsInGaze(elementsInGaze));
if (elementsInGaze.length > 0) {
// console.log('elements in gaze: ', elemsInGaze);
}
}, [mousePosition]);
// print out the gazes and keys
useEffect(() => {
// console.log('gazesAndKeys', gazesAndKeys);
}, [gazesAndKeys]);
// on key or mouse press, log the press and what elements are in the gaze to state
useEffect(() => {
if (keyDown !== '') {
const time = new Date().toISOString();
dispatch(
addKeyDown({
elemsInGaze: elemsInGaze,
keyPress: keyDown.toString(),
timeEnteredGaze: time,
}),
);
}
}, [keyDown]);
useEffect(() => {
if (mouseButtonDown.toString() !== '3') {
const time = new Date().toISOString();
dispatch(
addKeyDown({
elemsInGaze: elemsInGaze,
keyPress: mouseButtonDown.toString(),
timeEnteredGaze: time,
}),
);
}
}, [mouseButtonDown]);
// on key or mouse release, delete the press that was logged to state and ensure the key/mouse is reset so we can accept the same key/mouse again
useEffect(() => {
console.log(keyUp);
if (keyUp !== '') {
dispatch(removeKeyDown(keyUp.toString()));
document.dispatchEvent(new KeyboardEvent('keyup', { key: '_' }));
document.dispatchEvent(new KeyboardEvent('keydown', { key: '_' }));
}
}, [keyUp]);
useEffect(() => {
if (mouseButtonUp !== '3') {
dispatch(removeKeyDown(mouseButtonUp.toString()));
document.dispatchEvent(new MouseEvent('mouseup', { button: 3 }));
document.dispatchEvent(new MouseEvent('mousedown', { button: 3 }));
}
}, [mouseButtonUp]);
// Redirect to /prototype if the user is on the root path
useEffect(() => {
if (pathname === '/') {
navigate('/prototype');
}
}, [pathname, navigate]);
return (
<div>
{/* {pathname !== '/prototype' && <Navigation />} */}
{pathname !== '/prototype' && <Gaze mousePosition={mousePosition} />}
<main>
<Outlet />
</main>
</div>
);
};
export default Layout;

View File

@ -1,11 +1,17 @@
import Widget from 'src/components/Widget/Widget';
import { useAppSelector } from 'src/redux/hooks';
import { getMinimapWidgets } from 'src/redux/slices/minimapSlice';
import useMoveShips from 'src/hooks/useMoveShips';
import { useEffect } from 'react';
const Minimap = () => {
const widgets = useAppSelector(getMinimapWidgets);
/* If this is here, then ships only move if this page is being rendered */
useMoveShips();
// console.log('minimap widgets:', widgets);
useEffect(() => {
console.log('Minimap widgets:', widgets);
}, [widgets.length]);
return (
<div className="absolute top-0 left-0 bg-stone-200 w-[1920px] h-[1080px] hover:cursor-pointer">

View File

@ -1,47 +1,34 @@
import { useEffect } from 'react';
// ~~~~~~~ Components ~~~~~~~
import Gaze from 'src/ui/Gaze';
// ~~~~~~~ Redux ~~~~~~~
import { useAppDispatch, useAppSelector } from 'src/redux/hooks';
import {
type InitialMinimapState,
addElementToWidget,
addWidget,
addWidgetToSection,
getSections,
getWidgets,
initializeState,
} from 'src/redux/slices/minimapSlice';
import {
addKeyDown,
getElementsInGaze,
getGazesAndKeys,
removeKeyDown,
setElementsInGaze,
type ElementInGaze,
} from 'src/redux/slices/gazeSlice';
// ~~~~~~~ Cusdom Hooks ~~~~~~~
import { useKeyDown } from 'src/hooks/useKeyDown';
import { useMousePosition } from 'src/hooks/useMousePosition';
import { useKeyUp } from 'src/hooks/useKeyUp';
import { useMouseButtonDown } from 'src/hooks/useMouseButtonDown';
import { useMouseButtonUp } from 'src/hooks/useMouseButtonUp';
import useWorldSim from 'src/hooks/useWorldSim';
import { findElementsInGaze } from 'src/hooks/findElementsInGaze';
// ~~~~~~~ Prototype ~~~~~~~
import assimilator from 'src/prototype/assimilator';
import selector from 'src/prototype/selector';
// ~~~~~~~ Constants ~~~~~~~
import { GAZE_RADIUS } from 'src/utils/constants';
const CIRCLE_PERCENTAGE_THRESH = 0.1;
const ELEMENT_PERCENTAGE_THRESH = 0.1;
import { ownship, drones, initialShips } from 'src/utils/initialShips';
import { initialSections } from 'src/utils/initialSections';
import Home from 'src/components/Home';
import monitor from 'src/prototype/monitor';
const Prototype = () => {
// ~~~~~ Custom Hooks ~~~~~~
const { messages, stressLevel } = useWorldSim();
const mousePosition = useMousePosition();
const keyDown = useKeyDown();
const keyUp = useKeyUp();
const mouseButtonDown = useMouseButtonDown();
const mouseButtonUp = useMouseButtonUp();
// ~~~~~ Selectors ~~~~~~
const sections = useAppSelector(getSections);
@ -51,64 +38,29 @@ const Prototype = () => {
const dispatch = useAppDispatch();
// on mouse position move, check for elements in gaze
useEffect(() => {
const elementsInGaze = findElementsInGaze(
mousePosition,
dispatch,
widgets,
GAZE_RADIUS,
CIRCLE_PERCENTAGE_THRESH,
ELEMENT_PERCENTAGE_THRESH,
);
dispatch(setElementsInGaze(elementsInGaze));
if (elementsInGaze.length > 0) {
console.log('elements in gaze: ', elemsInGaze);
}
}, [mousePosition]);
// Intiailize minimap state
const initialState: InitialMinimapState = {
visualComplexity: 0,
audioComplexity: 0,
ownship,
drones,
widgets: { ...initialShips },
messages: [],
sections: [...initialSections],
};
// print out the gazes and keys
useEffect(() => {
console.log('gazesAndKeys', gazesAndKeys);
}, [gazesAndKeys]);
dispatch(initializeState(initialState));
}, [dispatch]);
// on key or mouse press, log the press and what elements are in the gaze to state
//call the monitor
useEffect(() => {
if (keyDown !== '') {
dispatch(
addKeyDown({ elemsInGaze: elemsInGaze, keyPress: keyDown.toString() }),
);
}
}, [keyDown]);
const intervalID = setInterval(() => {
monitor({ dispatch });
}, 100);
useEffect(() => {
if (mouseButtonDown.toString() !== '') {
dispatch(
addKeyDown({
elemsInGaze: elemsInGaze,
keyPress: mouseButtonDown.toString(),
}),
);
}
}, [mouseButtonDown]);
// on key or mouse release, delete the press that was logged to state and ensure the key/mouse is reset so we can accept the same key/mouse again
useEffect(() => {
console.log(keyUp);
if (keyUp !== '') {
dispatch(removeKeyDown(keyUp.toString()));
document.dispatchEvent(new KeyboardEvent('keyup', { key: '_' }));
document.dispatchEvent(new KeyboardEvent('keydown', { key: '_' }));
}
}, [keyUp]);
useEffect(() => {
if (mouseButtonUp !== '') {
dispatch(removeKeyDown(mouseButtonUp.toString()));
document.dispatchEvent(new KeyboardEvent('mouseup', { key: '_' }));
document.dispatchEvent(new KeyboardEvent('mousedown', { key: '_' }));
}
}, [mouseButtonUp]);
return () => clearInterval(intervalID);
}, []);
// run whenever messages array changes
useEffect(() => {
@ -175,7 +127,7 @@ const Prototype = () => {
}
}, [messages]);
return <Gaze mousePosition={mousePosition} />;
return <Home />;
};
export default Prototype;

View File

@ -1,30 +0,0 @@
import { useEffect } from 'react';
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
import Prototype from 'src/components/Prototype';
import Navigation from 'src/ui/Navigation';
const Root = () => {
// ~~~~~ React Router ~~~~~~
const { pathname } = useLocation();
const navigate = useNavigate();
// Redirect to /minimap if the user is on the root path
useEffect(() => {
if (pathname === '/') {
navigate('/minimap');
}
}, [pathname, navigate]);
return (
<div>
<Navigation />
<Prototype />
<main>
<Outlet />
</main>
</div>
);
};
export default Root;

View File

@ -2,8 +2,11 @@ import type { AppDispatch } from 'src/redux/store';
import {
removeWidget,
deleteElementFromWidget,
updateElementExpiration,
} from 'src/redux/slices/minimapSlice';
import store from 'src/redux/store';
import type { ElementInGaze, GazeAndKey } from 'src/redux/slices/gazeSlice';
import type { BaseElement } from 'src/types/element';
type MonitorProps = {
// define expected input here and it's type (number, string, etc.)
@ -16,26 +19,60 @@ type MonitorProps = {
* @returns ???
*/
const monitor = ({ dispatch }: MonitorProps) => {
//human visual recognition is about 100 ms: https://www.cell.com/neuron/fulltext/S0896-6273(09)00171-8?_returnURL=https%3A%2F%2Flinkinghub.elsevier.com%2Fretrieve%2Fpii%2FS0896627309001718%3Fshowall%3Dtrue#secd20967130e327
const widgets = store.getState().minimap.widgets;
const elementsInGaze = store.getState().gaze.elementsInGaze;
const gazesAndKeys = store.getState().gaze.gazesAndKeys;
Object.keys(widgets).forEach((widgetId) => {
/*
*
*detect and handle interactions with elements
*
*/
//detect interactions via gaze
const timeSomeMsAgo = new Date();
timeSomeMsAgo.setMilliseconds(
timeSomeMsAgo.getMilliseconds()-100, //<- 100 should be in constants file, but just testing now
);//set timeSomeMsAgo to the time it was 100 ms ago
elementsInGaze.forEach(function(elementInGaze: ElementInGaze, elementInGazeIndex:number){
if(timeSomeMsAgo.toISOString() >= elementInGaze.timeEnteredGaze){ //has been in gaze for at least 100 ms
console.log('interacted with element '+elementInGaze.id+' using gaze');
dispatch(updateElementExpiration(elementInGaze.widgetId, elementInGaze.id)); //update the time until expiration
}
});
//detect interactions via key press
gazesAndKeys.forEach(function(gazeAndKey:GazeAndKey, gazeAndKeyIndex:number){
gazeAndKey.elemsInGaze.forEach(function(elementInGaze, elementInGazeIndex){
dispatch(updateElementExpiration(elementInGaze.widgetId, elementInGaze.id)); //update the time until expiration
console.log('interacted with element '+elementInGaze.id+' using '+gazeAndKey.keyPress);
});
});
Object.keys(widgets).forEach((widgetId) => { //update widgets and elements that haven't been interacted with
const widget = widgets[widgetId];
widget.elements.forEach((element, elementIndex) => {
widget.elements.forEach((element:BaseElement, elementIndex:number) => {
//go through each element
if (element.expiration && !element.interacted) {
if (element.expiration && !element.interacted) {//if it has an expiration and has not been interacted with
const time = new Date().toISOString();
if (element.expiration <= time) {
console.log('element ' + element.id + ' expired! deleting...');
if (element.onExpiration === 'delete') {
if (widget.elements.length === 1) {
console.log('widget length 1');
dispatch(removeWidget(widget.id));
} else {
dispatch(deleteElementFromWidget(widgetId, element.id));
}
switch(element.onExpiration){
case 'delete':
console.log('element ' + element.id + ' expired! deleting...');
if (widget.elements.length === 1) { //if this is the last element, delete the whole widget
dispatch(removeWidget(widget.id));
} else {
dispatch(deleteElementFromWidget(widgetId, element.id)); //delete the widget
}
break;
}
}
}

View File

@ -38,7 +38,159 @@ const selector = ({ message, stressLevel }: SelectorProps = {}) => {
} else {
// If no message is provided, return the initial LPD
return initialLPD;
}
// const selector = ({ message }: SelectorProps) => {
// const possibleWidgets: Widget[] = [];
// const expirationTime = new Date();
// expirationTime.setSeconds(
// expirationTime.getSeconds() + (Math.floor(Math.random() * 10) + 5),
// ); //set the time to expire to a time between 5 and 15 seconds
// const expiration = expirationTime.toISOString();
// const onExpiration = 'delete';
// // Only doing a single widget for Demo3
// const widget: Widget = {
// // static ID for Demo3
// id: 'list',
// sectionType: 'tinder',
// type: 'list',
// screen: '/pearce-screen',
// elements: [],
// x: 50,
// y: 40,
// w: 300,
// h: 800,
// canOverlap: false,
// useElementLocation: false,
// maxAmount: 1,
// };
// let elements: Element[] = [];
// switch (message.kind) {
// case 'RequestApprovalToAttack':
// elements.push({
// id: uuid(),
// type: 'request-approval',
// modality: 'visual',
// h: 100,
// w: 200,
// xWidget: 0,
// yWidget: 0,
// message,
// collapsed: true,
// priority: message.priority,
// icon: {
// id: uuid(),
// modality: 'visual',
// type: 'icon',
// h: 30,
// w: 30,
// xWidget: 0,
// yWidget: 0,
// src: DRONE_ICON,
// },
// leftButton: {
// id: uuid(),
// modality: 'visual',
// h: 30,
// w: 80,
// xWidget: 0,
// yWidget: 0,
// text: 'Deny',
// type: 'button',
// },
// rightButton: {
// id: uuid(),
// modality: 'visual',
// h: 30,
// w: 80,
// xWidget: 0,
// yWidget: 0,
// text: 'Approve',
// type: 'button',
// },
// } satisfies RequestApprovalElement);
// break;
// case 'MissileToOwnshipDetected':
// elements.push({
// id: uuid(),
// type: 'missile-incoming',
// modality: 'visual',
// xWidget: 0,
// yWidget: 0,
// h: 80,
// w: 80,
// message,
// priority: message.priority,
// icon: {
// id: uuid(),
// modality: 'visual',
// type: 'icon',
// src: DANGER_ICON,
// h: 30,
// w: 30,
// xWidget: 0,
// yWidget: 0,
// },
// } satisfies MissileIncomingElement);
// break;
// case 'AcaHeadingToBase':
// elements.push({
// id: uuid(),
// type: 'text',
// modality: 'visual',
// xWidget: 0,
// yWidget: 0,
// h: 30,
// w: 200,
// text: 'Aircraft heading to base',
// priority: message.priority,
// } satisfies TextElement);
// break;
// case 'AcaFuelLow':
// elements.push({
// id: uuid(),
// type: 'table',
// modality: 'visual',
// xWidget: 0,
// yWidget: 0,
// h: 50,
// w: 200,
// rows: 2,
// cols: 2,
// tableData: [
// ['Fuel', 'Low'],
// ['Altitude', 'Low'],
// ],
// priority: message.priority,
// } satisfies TableElement);
// break;
// case 'AcaDefect':
// elements.push({
// id: uuid(),
// type: 'table',
// modality: 'visual',
// xWidget: 0,
// yWidget: 0,
// h: 50,
// w: 200,
// rows: 2,
// cols: 2,
// tableData: [
// ['Defect', 'Engine'],
// ['Altitude', 'Low'],
// ],
// priority: message.priority,
// } satisfies TableElement);
// break;
// }
// const possibleWidgets: Widget[] = [];
// const expirationTime = new Date();

View File

@ -65,10 +65,10 @@ export const gazeSlice = createSlice({
// ]
// }
},
removeKeyDown: (state, action: PayloadAction<string>) => {
state.gazesAndKeys.map(function (gazeAndKey, gazeAndKeyIndex) {
// console.log('equality toAdd: '+action.payload+' inStorage: '+gazeAndKey.keyPress)
if (action.payload === gazeAndKey.keyPress) {
removeKeyDown: (state, action:PayloadAction<string>) => {
state.gazesAndKeys.map(function(gazeAndKey, gazeAndKeyIndex){
//console.log('equality toAdd: '+action.payload+' inStorage: '+gazeAndKey.keyPress)
if(action.payload === gazeAndKey.keyPress){
//we found the key that was released
state.gazesAndKeys = [
...state.gazesAndKeys.slice(0, gazeAndKeyIndex),

View File

@ -1,21 +1,26 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
import type { Widget, WidgetMap } from 'src/types/widget';
import type { Widget, VehicleWidget, WidgetMap } from 'src/types/widget';
import type { Message } from 'src/types/schema-types';
import type { Element } from 'src/types/element';
import type { LinkedSectionWidget, Section } from 'src/types/support-types';
import selector from 'src/prototype/selector';
import { ownship, drones } from 'src/prototype/lpd/initialLPD';
type InitialState = {
export type InitialMinimapState = {
visualComplexity: number;
audioComplexity: number;
// read-only
ownship: VehicleWidget | null;
drones: VehicleWidget[];
widgets: WidgetMap;
messages: Message[];
sections: Section[];
};
const initialState: InitialState = {
const initialState: InitialMinimapState = {
visualComplexity: 0,
audioComplexity: 0,
messages: [],
@ -27,6 +32,22 @@ export const minimapSlice = createSlice({
name: 'minimap',
initialState,
reducers: {
// This is needed for compatibility with redux-state-sync
initializeState: (state, action: PayloadAction<InitialMinimapState>) => {
// don't initialize if we already have data (one-time initialization)
if (Object.keys(state.widgets).length > 0 || state.sections.length > 0) {
return;
}
state.visualComplexity = action.payload.visualComplexity;
state.audioComplexity = action.payload.audioComplexity;
state.ownship = action.payload.ownship;
state.drones = action.payload.drones;
state.widgets = action.payload.widgets;
state.messages = action.payload.messages;
state.sections = action.payload.sections;
},
addMapSection: (state, action) => {
state.sections.push(action.payload); //add it to our sections as well
},
@ -39,6 +60,40 @@ export const minimapSlice = createSlice({
state.widgets[action.payload.id] = action.payload;
},
updateShipPosition: {
prepare(shipId: string, x: number, y: number) {
return {
payload: { shipId, x, y },
};
},
reducer: (
state,
action: PayloadAction<{ shipId: string; x: number; y: number }>,
) => {
const { shipId, x, y } = action.payload;
const ship = state.widgets[shipId];
// check if ship exists
if (!ship) {
console.error(`Ship with id ${shipId} not found`);
return;
}
// check if the ship is a vehicle
if (ship.type !== 'vehicle') {
console.error(`Widget with id ${shipId} is not a vehicle`);
return;
}
state.widgets[shipId] = {
...ship,
x,
y,
};
},
},
removeWidget: (state, action: PayloadAction<string>) => {
delete state.widgets[action.payload];
},
@ -69,6 +124,43 @@ export const minimapSlice = createSlice({
});
},
updateElementExpiration: {
//update the time until window of interaction expires
prepare(widgetId: string, elementId: string) {
return {
payload: { widgetId, elementId },
};
},
reducer: (
state,
action: PayloadAction<{ widgetId: string; elementId: string }>,
) => {
const { widgetId, elementId } = action.payload;
const widget = state.widgets[widgetId];
// if widget exists
if (widget) {
const tempElements = state.widgets[widgetId].elements;
tempElements.forEach(function (element, elementIndex) {
if (element.id === elementId && element.expirationInterval) {
const newExpiration = new Date();
newExpiration.setSeconds(
newExpiration.getSeconds() + element.expirationInterval,
);
tempElements[elementIndex].expiration =
newExpiration.toISOString();
}
});
state.widgets[widgetId] = {
...widget,
elements: tempElements,
};
} else {
console.error(`Widget with id ${widgetId} not found`);
}
},
},
deleteElementFromWidget: {
prepare(widgetId: string, elementId: string) {
return {
@ -154,23 +246,21 @@ export const minimapSlice = createSlice({
getWidgets: (state) => state.widgets,
getLeftScreenWidgets: (state) => {
const minimapWidgets = Object.keys(state.widgets).filter(
(id) => state.widgets[id].screen === 'left',
(id) => state.widgets[id].screen === '/pearce-screen',
);
console.log('minimapWidgets:', minimapWidgets);
return minimapWidgets.map((id) => state.widgets[id]);
},
getMinimapWidgets: (state) => {
const minimapWidgets = Object.keys(state.widgets).filter(
(id) => state.widgets[id].screen === 'minimap',
(id) => state.widgets[id].screen === '/minimap',
);
return minimapWidgets.map((id) => state.widgets[id]);
},
getRightScreenWidgets: (state) => {
const minimapWidgets = Object.keys(state.widgets).filter(
(id) => state.widgets[id].screen === 'right',
(id) => state.widgets[id].screen === '/right-screen',
);
return minimapWidgets.map((id) => state.widgets[id]);
@ -183,27 +273,40 @@ export const minimapSlice = createSlice({
getMessages: (state) => state.messages,
// ~~~~~ selectors for ships ~~~~~
getOwnship: (state) =>
state.widgets[ownship.id] ? state.widgets[ownship.id] : null,
getDrones: (state) =>
drones.map((drone) =>
state.widgets[drone.id] ? state.widgets[drone.id] : null,
),
getOwnship: (state) => {
if (state.ownship) {
return state.widgets[state.ownship.id];
}
return null;
},
getDrones: (state) => {
if (state.drones.length > 0) {
return state.drones.map((drone) => state.widgets[drone.id]);
}
return [];
},
},
});
// action creators (automatically generated by createSlice for each reducer)
export const {
initializeState,
addMapSection,
addMessage,
addWidget,
updateWidget,
removeWidget,
addElementToWidget,
addWidgetToSection,
deleteElementFromWidget,
updateElementExpiration,
updateWidget,
updateShipPosition,
updateVisualComplexity,
updateAudioComplexity,
removeWidget,
deleteElementFromWidget,
toggleElementInteraction,
} = minimapSlice.actions;

View File

@ -1,7 +1,8 @@
import { combineSlices, configureStore } from '@reduxjs/toolkit';
import {
createStateSyncMiddleware,
initMessageListener,
initStateWithPrevTab,
withReduxStateSync,
type Config,
} from 'redux-state-sync';
import { minimapSlice } from './slices/minimapSlice';
@ -16,18 +17,20 @@ const rootReducer = combineSlices(minimapSlice, gazeSlice);
type RootState = ReturnType<typeof rootReducer>;
const reduxStateSyncConfig: Config = {
prepareState: (state: RootState) => state,
// blacklist actions that should not be synced
blacklist: [minimapSlice.actions.initializeState.type],
};
const stateSyncMiddleware = createStateSyncMiddleware(reduxStateSyncConfig);
const store = configureStore({
reducer: rootReducer,
reducer: withReduxStateSync(rootReducer),
// @ts-ignore (TODO: fix this type error)
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(stateSyncMiddleware),
});
initMessageListener(store);
// Initialize the state with the previous tab's state
initStateWithPrevTab(store);
// Infer the type of `store`
type AppStore = typeof store;

View File

@ -5,7 +5,7 @@ export type Cell = {
color?: string;
};
export type ScreenType = 'pearce' | 'minimap' | 'boring-right';
export type ScreenType = '/pearce-screen' | '/minimap' | '/right-screen';
export type SectionType =
| 'free'

View File

@ -36,6 +36,8 @@ export type GridWidget = BaseWidget & {
export type VehicleWidget = BaseWidget & {
type: 'vehicle';
// this corresponds to the id in the schema-types defined by the world-sim team
vehicleId: number;
// additonal properties...
};

View File

@ -10,11 +10,11 @@ const Navigation = () => {
dark:text-gray-300"
>
<NavLink
to="/left-screen"
to="/pearce-screen"
className={`text-gray-800 dark:text-gray-200 border-b-2
${pathname === '/left-screen' ? 'border-blue-500' : ''} mx-1.5 sm:mx-6 text-3xl`}
${pathname === '/pearce-screen' ? 'border-blue-500' : ''} mx-1.5 sm:mx-6 text-3xl`}
>
Left Screen
Pearce Screen
</NavLink>
<NavLink
to="/minimap"

10
src/ui/Spinner.tsx Normal file
View File

@ -0,0 +1,10 @@
const Spinner = () => {
return (
<div
className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-solid border-current border-e-transparent align-[-0.125em] text-primary motion-reduce:animate-[spin_1.5s_linear_infinite] text-red-600"
role="status"
/>
);
};
export default Spinner;

View File

@ -2,19 +2,21 @@ import { v4 as uuid } from 'uuid';
import type { IconElement } from 'src/types/element';
import OWNSHIP_LOGO from 'src/icons/currentPosition.svg';
import DRONE_LOGO from 'src/icons/drone.svg';
import type { Widget, VehicleWidget, WidgetMap } from 'src/types/widget';
import type { VehicleWidget, WidgetMap } from 'src/types/widget';
const createDroneWidget = (
x: number,
y: number,
w: number,
h: number,
vehicleId: number,
): VehicleWidget => ({
elements: [droneElement],
id: uuid(),
sectionType: 'free',
type: 'vehicle',
screen: 'minimap',
screen: '/minimap',
vehicleId,
x,
y,
@ -42,13 +44,14 @@ const ownshipElement: IconElement = {
export const ownship: VehicleWidget = {
id: uuid(),
vehicleId: 0,
x: 400,
y: 950,
w: 50,
h: 50,
screen: 'minimap',
screen: '/minimap',
sectionType: 'free',
type: 'vehicle',
elements: [ownshipElement],
@ -72,18 +75,19 @@ const droneElement: IconElement = {
yWidget: 0,
};
export const drones = [
createDroneWidget(500, 200, 50, 50),
createDroneWidget(1500, 550, 50, 50),
createDroneWidget(1500, 350, 50, 50),
createDroneWidget(200, 900, 50, 50),
createDroneWidget(1150, 750, 50, 50),
];
const drone1 = createDroneWidget(500, 200, 50, 50, 1);
const drone2 = createDroneWidget(1500, 550, 50, 50, 2);
const drone3 = createDroneWidget(1500, 350, 50, 50, 3);
const drone4 = createDroneWidget(200, 900, 50, 50, 4);
const drone5 = createDroneWidget(1150, 750, 50, 50, 5);
export const drones = [drone1, drone2, drone3, drone4, drone5];
export const initialShips: WidgetMap = {
[ownship.id]: ownship,
...drones.reduce((acc, drone) => {
acc[drone.id] = drone;
return acc;
}, {} as WidgetMap),
[drone1.id]: drone1,
[drone2.id]: drone2,
[drone3.id]: drone3,
[drone4.id]: drone4,
[drone5.id]: drone5,
};