mirror of
https://github.com/markmanx/isoflow.git
synced 2025-01-31 23:22:31 +00:00
feat: replaces xstate with custom state machine implementation
This commit is contained in:
parent
08ff6865fc
commit
6b90ad9b7b
19 changed files with 9624 additions and 304 deletions
5
jest.config.js
Normal file
5
jest.config.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||
module.exports = {
|
||||
preset: "ts-jest",
|
||||
testEnvironment: "node",
|
||||
};
|
9417
package-lock.json
generated
9417
package-lock.json
generated
File diff suppressed because it is too large
Load diff
13
package.json
13
package.json
|
@ -9,11 +9,16 @@
|
|||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/jsdom": "^21.1.0",
|
||||
"@types/node": "^16.18.12",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"jsdom": "^21.1.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"ts-jest": "^29.0.5",
|
||||
"ts-loader": "^9.4.2",
|
||||
"typescript": "^4.9.5",
|
||||
"webpack": "^5.76.2",
|
||||
|
@ -24,13 +29,13 @@
|
|||
"@emotion/styled": "^11.10.6",
|
||||
"@mui/icons-material": "^5.11.9",
|
||||
"@mui/material": "^5.11.10",
|
||||
"@xstate/react": "^3.2.1",
|
||||
"auto-bind": "^5.0.1",
|
||||
"gsap": "^3.11.4",
|
||||
"mobx": "^6.8.0",
|
||||
"mobx-react": "^7.6.0",
|
||||
"paper": "^0.12.17",
|
||||
"react-hook-form": "^7.43.2",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"xstate": "^4.37.0",
|
||||
"zod": "^3.20.6",
|
||||
"zustand": "^4.3.3"
|
||||
},
|
||||
|
@ -39,7 +44,9 @@
|
|||
"react-dom": ">=17"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "webpack"
|
||||
"dev": "webpack --watch",
|
||||
"build": "webpack",
|
||||
"test": "jest"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
|
|
17
src/App.tsx
17
src/App.tsx
|
@ -1,12 +1,13 @@
|
|||
import React from "react";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import { theme } from "./theme";
|
||||
import Box from "@mui/material/Box";
|
||||
import { SideNav } from "./components/SideNav";
|
||||
import { Sidebar } from "./components/Sidebars";
|
||||
import { ToolMenu } from "./components/ToolMenu";
|
||||
import { RendererContainer } from "./components/RendererContainer";
|
||||
import { SceneI } from "./validation/SceneSchema";
|
||||
import { GlobalStateProvider } from "./contexts/GlobalStateContext";
|
||||
import { ModeManagerProvider } from "./contexts/ModeManagerContext";
|
||||
|
||||
interface Props {
|
||||
initialScene: SceneI;
|
||||
|
@ -15,12 +16,14 @@ interface Props {
|
|||
export const App = ({ initialScene }: Props) => {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<GlobalStateProvider>
|
||||
<RendererContainer key="renderer" />
|
||||
<Sidebar />
|
||||
<SideNav />
|
||||
<ToolMenu />
|
||||
</GlobalStateProvider>
|
||||
<ModeManagerProvider>
|
||||
<Box sx={{ width: "100%", height: "100%", position: "relative" }}>
|
||||
<RendererContainer key="renderer" />
|
||||
<Sidebar />
|
||||
<SideNav />
|
||||
<ToolMenu />
|
||||
</Box>
|
||||
</ModeManagerProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
import { Select } from "./Select";
|
||||
import { Pan } from "./Pan";
|
||||
import { createMachine, assign } from "xstate";
|
||||
import { Events, Context } from "./types";
|
||||
|
||||
export const EditorMachine = createMachine({
|
||||
id: "Editor",
|
||||
initial: "UNINITIALISED",
|
||||
on: {
|
||||
SWITCH_TO_SELECT: {
|
||||
target: "SELECT_MODE",
|
||||
},
|
||||
SWITCH_TO_PAN: {
|
||||
target: "PAN_MODE",
|
||||
},
|
||||
},
|
||||
states: {
|
||||
UNINITIALISED: {
|
||||
on: {
|
||||
INIT: {
|
||||
actions: assign({
|
||||
renderer: (ctx, { renderer }) => renderer,
|
||||
}),
|
||||
target: "SELECT_MODE",
|
||||
},
|
||||
},
|
||||
},
|
||||
SELECT_MODE: Select,
|
||||
PAN_MODE: Pan,
|
||||
},
|
||||
schema: {
|
||||
context: {} as Context,
|
||||
events: {} as Events,
|
||||
},
|
||||
});
|
|
@ -1,37 +0,0 @@
|
|||
import { MachineConfig } from "xstate";
|
||||
import type { Events, Context } from "./types";
|
||||
|
||||
const changeCursor = (cursorType: string) => (ctx: Context) => {
|
||||
if (ctx.renderer)
|
||||
ctx.renderer.domElements.container.style.cursor = cursorType;
|
||||
};
|
||||
|
||||
export const Pan: MachineConfig<Context, any, Events> = {
|
||||
id: "Pan",
|
||||
initial: "IDLE",
|
||||
entry: changeCursor("grab"),
|
||||
exit: changeCursor("default"),
|
||||
states: {
|
||||
IDLE: {
|
||||
on: {
|
||||
MOUSE_DOWN: {
|
||||
target: "PANNING",
|
||||
},
|
||||
},
|
||||
},
|
||||
PANNING: {
|
||||
entry: changeCursor("grabbing"),
|
||||
exit: changeCursor("grab"),
|
||||
on: {
|
||||
MOUSE_MOVE: {
|
||||
actions: (ctx, data) => {
|
||||
ctx.renderer?.scrollToDelta(data.delta.x, data.delta.y);
|
||||
},
|
||||
},
|
||||
MOUSE_UP: {
|
||||
target: "IDLE",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,31 +0,0 @@
|
|||
import { MachineConfig } from "xstate";
|
||||
import type { Events, Context } from "./types";
|
||||
|
||||
export const Select: MachineConfig<Context, any, Events> = {
|
||||
id: "#Select",
|
||||
initial: "HOVERING",
|
||||
states: {
|
||||
HOVERING: {
|
||||
entry: (ctx) => {
|
||||
ctx.renderer?.sceneElements.cursor.enable();
|
||||
},
|
||||
exit: (ctx) => {
|
||||
ctx.renderer?.sceneElements.cursor.disable();
|
||||
},
|
||||
on: {
|
||||
MOUSE_MOVE: {
|
||||
actions: (ctx, data) => {
|
||||
if (!ctx.renderer) return;
|
||||
|
||||
const tile = ctx.renderer.getTileFromMouse(
|
||||
data.position.x,
|
||||
data.position.y
|
||||
);
|
||||
|
||||
ctx.renderer.sceneElements.cursor.displayAt(tile.x, tile.y);
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1,59 +0,0 @@
|
|||
import { Renderer } from "../renderer/Renderer";
|
||||
|
||||
export interface Context {
|
||||
renderer?: Renderer;
|
||||
}
|
||||
|
||||
interface Coords {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export type MouseEvents =
|
||||
| {
|
||||
type: "MOUSE_DOWN";
|
||||
position: Coords;
|
||||
delta: Coords;
|
||||
renderer: Renderer;
|
||||
}
|
||||
| {
|
||||
type: "MOUSE_MOVE";
|
||||
position: Coords;
|
||||
delta: Coords;
|
||||
renderer: Renderer;
|
||||
}
|
||||
| {
|
||||
type: "MOUSE_UP";
|
||||
position: Coords;
|
||||
delta: Coords;
|
||||
renderer: Renderer;
|
||||
}
|
||||
| {
|
||||
type: "MOUSE_ENTER";
|
||||
position: Coords;
|
||||
delta: Coords;
|
||||
renderer: Renderer;
|
||||
}
|
||||
| {
|
||||
type: "MOUSE_LEAVE";
|
||||
position: Coords;
|
||||
delta: Coords;
|
||||
renderer: Renderer;
|
||||
};
|
||||
|
||||
export type Events =
|
||||
| {
|
||||
type: "INIT";
|
||||
renderer: Renderer;
|
||||
}
|
||||
| {
|
||||
type: "SWITCH_TO_SELECT";
|
||||
}
|
||||
| {
|
||||
type: "SWITCH_TO_PAN";
|
||||
}
|
||||
| {
|
||||
type: "SWITCH_TOOL";
|
||||
tool: "SELECT" | "PAN";
|
||||
}
|
||||
| MouseEvents;
|
|
@ -1,12 +1,14 @@
|
|||
import React from "react";
|
||||
import { useRef, useEffect, useContext } from "react";
|
||||
import React, { useContext } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { Renderer } from "../renderer/Renderer";
|
||||
import { useGlobalState } from "../hooks/useGlobalState";
|
||||
import { GlobalStateContext } from "../contexts/GlobalStateContext";
|
||||
import { useMouseInput } from "../hooks/useMouseInput";
|
||||
import { modeManagerContext } from "../contexts/ModeManagerContext";
|
||||
import { Select } from "../modes/Select";
|
||||
|
||||
export const RendererContainer = () => {
|
||||
const { editor } = useContext(GlobalStateContext);
|
||||
export const RendererContainer = observer(() => {
|
||||
const modeManager = useContext(modeManagerContext);
|
||||
const rendererEl = useRef<HTMLDivElement>(null);
|
||||
const { setDomEl, setCallbacks } = useMouseInput();
|
||||
const setRenderer = useGlobalState((state) => state.setRenderer);
|
||||
|
@ -17,32 +19,31 @@ export const RendererContainer = () => {
|
|||
const renderer = new Renderer(rendererEl.current);
|
||||
setRenderer(renderer);
|
||||
setDomEl(rendererEl.current);
|
||||
editor.send("INIT", {
|
||||
renderer,
|
||||
});
|
||||
}, [setRenderer, setDomEl, editor]);
|
||||
modeManager.setRenderer(renderer);
|
||||
modeManager.activateMode(Select);
|
||||
}, [setRenderer, setDomEl, modeManager]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rendererEl.current) return;
|
||||
if (!modeManager) return;
|
||||
|
||||
setCallbacks({
|
||||
onMouseMove: (event) => {
|
||||
editor.send("MOUSE_MOVE", { ...event });
|
||||
modeManager.onMouseEvent("MOUSE_MOVE", event);
|
||||
},
|
||||
onMouseDown: (event) => {
|
||||
editor.send("MOUSE_DOWN", { ...event });
|
||||
modeManager.onMouseEvent("MOUSE_DOWN", event);
|
||||
},
|
||||
onMouseUp: (event) => {
|
||||
editor.send("MOUSE_UP", { ...event });
|
||||
modeManager.onMouseEvent("MOUSE_UP", event);
|
||||
},
|
||||
onMouseEnter: (event) => {
|
||||
editor.send("MOUSE_ENTER", { ...event });
|
||||
modeManager.onMouseEvent("MOUSE_ENTER", event);
|
||||
},
|
||||
onMouseLeave: (event) => {
|
||||
editor.send("MOUSE_LEAVE", { ...event });
|
||||
modeManager.onMouseEvent("MOUSE_LEAVE", event);
|
||||
},
|
||||
});
|
||||
}, [setCallbacks, editor]);
|
||||
}, [setCallbacks, modeManager]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -51,9 +52,9 @@ export const RendererContainer = () => {
|
|||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: "100vw",
|
||||
height: "100vh",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,20 +1,21 @@
|
|||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useContext } from "react";
|
||||
import { useTheme } from "@mui/material";
|
||||
import { useActor } from "@xstate/react";
|
||||
import Card from "@mui/material/Card";
|
||||
import { MenuItem } from "../MenuItem";
|
||||
import PanToolIcon from "@mui/icons-material/PanTool";
|
||||
import ZoomInIcon from "@mui/icons-material/ZoomIn";
|
||||
import ZoomOutIcon from "@mui/icons-material/ZoomOut";
|
||||
import NearMeIcon from "@mui/icons-material/NearMe";
|
||||
import { GlobalStateContext } from "../../contexts/GlobalStateContext";
|
||||
import { useZoom } from "../../hooks/useZoom";
|
||||
import { modeManagerContext } from "../../contexts/ModeManagerContext";
|
||||
import { Select } from "../../modes/Select";
|
||||
import { Pan } from "../../modes/Pan";
|
||||
|
||||
export const ToolMenu = () => {
|
||||
export const ToolMenu = observer(() => {
|
||||
const modeManager = useContext(modeManagerContext);
|
||||
const theme = useTheme();
|
||||
const { editor } = useContext(GlobalStateContext);
|
||||
const [state] = useActor(editor);
|
||||
const { incrementZoom, decrementZoom } = useZoom();
|
||||
|
||||
return (
|
||||
|
@ -30,16 +31,16 @@ export const ToolMenu = () => {
|
|||
<MenuItem
|
||||
name="Select"
|
||||
Icon={NearMeIcon}
|
||||
onClick={() => editor.send("SWITCH_TO_SELECT")}
|
||||
onClick={() => modeManager.activateMode(Select)}
|
||||
size={theme.customVars.toolMenu.height}
|
||||
isActive={state.matches("SELECT_MODE")}
|
||||
isActive={modeManager.currentMode instanceof Select}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Pan"
|
||||
Icon={PanToolIcon}
|
||||
onClick={() => editor.send("SWITCH_TO_PAN")}
|
||||
onClick={() => modeManager.activateMode(Pan)}
|
||||
size={theme.customVars.toolMenu.height}
|
||||
isActive={state.matches("PAN_MODE")}
|
||||
isActive={modeManager.currentMode instanceof Pan}
|
||||
/>
|
||||
<MenuItem
|
||||
name="Zoom in"
|
||||
|
@ -55,4 +56,4 @@ export const ToolMenu = () => {
|
|||
/>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import React from "react";
|
||||
import { createContext } from "react";
|
||||
import { interpret, InterpreterFrom } from "xstate";
|
||||
import { useInterpret } from "@xstate/react";
|
||||
import { EditorMachine } from "../actions/Editor";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
type Service = InterpreterFrom<typeof EditorMachine>;
|
||||
|
||||
interface Context {
|
||||
editor: Service;
|
||||
}
|
||||
|
||||
export const GlobalStateContext = createContext<Context>({
|
||||
editor: interpret(EditorMachine),
|
||||
});
|
||||
|
||||
export const GlobalStateProvider = ({ children }: Props) => {
|
||||
const editor = useInterpret(EditorMachine);
|
||||
|
||||
return (
|
||||
<GlobalStateContext.Provider value={{ editor }}>
|
||||
{children}
|
||||
</GlobalStateContext.Provider>
|
||||
);
|
||||
};
|
16
src/contexts/ModeManagerContext.tsx
Normal file
16
src/contexts/ModeManagerContext.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React, { createContext, useMemo } from "react";
|
||||
import { ModeManager } from "../modes/ModeManager";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const modeManagerContext = createContext(new ModeManager());
|
||||
|
||||
export const ModeManagerProvider = ({ children }: Props) => {
|
||||
return (
|
||||
<modeManagerContext.Provider value={new ModeManager()}>
|
||||
{children}
|
||||
</modeManagerContext.Provider>
|
||||
);
|
||||
};
|
15
src/modes/ModeBase.ts
Normal file
15
src/modes/ModeBase.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { ModeContext, Mouse } from "./types";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
export class ModeBase {
|
||||
ctx;
|
||||
|
||||
constructor(ctx: ModeContext) {
|
||||
makeAutoObservable(this);
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
entry(mouse: Mouse) {}
|
||||
|
||||
exit() {}
|
||||
}
|
50
src/modes/ModeManager.ts
Normal file
50
src/modes/ModeManager.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { makeAutoObservable } from "mobx";
|
||||
import autoBind from "auto-bind";
|
||||
import { Renderer } from "../renderer/Renderer";
|
||||
import { ModeBase } from "./ModeBase";
|
||||
import { Mouse } from "./types";
|
||||
|
||||
export class ModeManager {
|
||||
renderer?: Renderer = undefined;
|
||||
currentMode?: ModeBase = undefined;
|
||||
mouse: Mouse = {
|
||||
position: { x: 0, y: 0 },
|
||||
delta: null,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
setRenderer(renderer: Renderer) {
|
||||
this.renderer = renderer;
|
||||
}
|
||||
|
||||
activateMode(Mode: typeof ModeBase) {
|
||||
if (!this.renderer) return;
|
||||
|
||||
const lastMode = this.currentMode;
|
||||
this.currentMode?.exit();
|
||||
|
||||
this.currentMode = new Mode({
|
||||
renderer: this.renderer,
|
||||
activateMode: this.activateMode.bind(this),
|
||||
deactivate: lastMode?.exit ?? (() => {}),
|
||||
});
|
||||
|
||||
this.currentMode.entry(this.mouse);
|
||||
}
|
||||
|
||||
onMouseEvent(eventName: string, mouse: Mouse) {
|
||||
this.mouse = mouse;
|
||||
|
||||
this.send(eventName, mouse);
|
||||
}
|
||||
|
||||
send(eventName: string, params?: any) {
|
||||
// TODO: Improve typings below
|
||||
// @ts-ignore
|
||||
this.currentMode?.[eventName]?.(params);
|
||||
}
|
||||
}
|
39
src/modes/Pan.ts
Normal file
39
src/modes/Pan.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { ModeBase } from "./ModeBase";
|
||||
import { Mouse } from "./types";
|
||||
import { Renderer } from "../renderer/Renderer";
|
||||
|
||||
const changeCursor = (cursorType: string, renderer: Renderer) => {
|
||||
renderer.domElements.container.style.cursor = cursorType;
|
||||
};
|
||||
|
||||
export class Pan extends ModeBase {
|
||||
isPanning = false;
|
||||
|
||||
entry() {
|
||||
changeCursor("grab", this.ctx.renderer);
|
||||
}
|
||||
|
||||
exit() {
|
||||
changeCursor("default", this.ctx.renderer);
|
||||
}
|
||||
|
||||
MOUSE_DOWN() {
|
||||
if (!this.isPanning) {
|
||||
this.isPanning = true;
|
||||
changeCursor("grabbing", this.ctx.renderer);
|
||||
}
|
||||
}
|
||||
|
||||
MOUSE_UP() {
|
||||
if (this.isPanning) {
|
||||
this.isPanning = false;
|
||||
changeCursor("grab", this.ctx.renderer);
|
||||
}
|
||||
}
|
||||
|
||||
MOUSE_MOVE(mouse: Mouse) {
|
||||
if (this.isPanning && mouse.delta !== null) {
|
||||
this.ctx.renderer.scrollToDelta(mouse.delta.x, mouse.delta.y);
|
||||
}
|
||||
}
|
||||
}
|
35
src/modes/Select.ts
Normal file
35
src/modes/Select.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import { ModeBase } from "./ModeBase";
|
||||
import { Mouse } from "./types";
|
||||
|
||||
export class Select extends ModeBase {
|
||||
entry(mouse: Mouse) {
|
||||
const tile = this.ctx.renderer.getTileFromMouse(
|
||||
mouse.position.x,
|
||||
mouse.position.y
|
||||
);
|
||||
|
||||
this.ctx.renderer.sceneElements.cursor.displayAt(tile.x, tile.y);
|
||||
this.ctx.renderer.sceneElements.cursor.enable();
|
||||
}
|
||||
|
||||
exit() {
|
||||
this.ctx.renderer.sceneElements.cursor.disable();
|
||||
}
|
||||
|
||||
MOUSE_ENTER() {
|
||||
this.ctx.renderer.sceneElements.cursor.enable();
|
||||
}
|
||||
|
||||
MOUSE_LEAVE() {
|
||||
this.ctx.renderer.sceneElements.cursor.disable();
|
||||
}
|
||||
|
||||
MOUSE_MOVE(mouse: Mouse) {
|
||||
const tile = this.ctx.renderer.getTileFromMouse(
|
||||
mouse.position.x,
|
||||
mouse.position.y
|
||||
);
|
||||
|
||||
this.ctx.renderer.sceneElements.cursor.displayAt(tile.x, tile.y);
|
||||
}
|
||||
}
|
34
src/modes/tests/ModeManager.test.ts
Normal file
34
src/modes/tests/ModeManager.test.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
import { ModeManager } from "../ModeManager";
|
||||
import { Renderer } from "../../renderer/Renderer";
|
||||
import { TestMode } from "./fixtures/TestMode";
|
||||
|
||||
jest.mock("../../renderer/Renderer", () => ({
|
||||
Renderer: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("Mode manager functions correctly", () => {
|
||||
it("Activating a mode works correctly", () => {
|
||||
const entrySpy = jest.spyOn(TestMode.prototype, "entry");
|
||||
const exitSpy = jest.spyOn(TestMode.prototype, "exit");
|
||||
const eventSpy = jest.spyOn(TestMode.prototype, "TEST_EVENT");
|
||||
const mouseEventSpy = jest.spyOn(TestMode.prototype, "MOUSE_MOVE");
|
||||
const renderer = new Renderer({} as unknown as HTMLDivElement);
|
||||
const modeManager = new ModeManager();
|
||||
modeManager.setRenderer(renderer);
|
||||
modeManager.activateMode(TestMode);
|
||||
|
||||
modeManager.send("TEST_EVENT");
|
||||
modeManager.send("TEST_EVENT");
|
||||
modeManager.send("MOUSE_MOVE", { x: 10, y: 10 });
|
||||
|
||||
expect(entrySpy).toHaveBeenCalled();
|
||||
expect(eventSpy).toHaveBeenCalledTimes(2);
|
||||
expect(mouseEventSpy).toHaveBeenCalledWith({
|
||||
x: 10,
|
||||
y: 10,
|
||||
});
|
||||
|
||||
modeManager.activateMode(TestMode);
|
||||
expect(exitSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
12
src/modes/tests/fixtures/TestMode.ts
vendored
Normal file
12
src/modes/tests/fixtures/TestMode.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { MouseCoords, ModeContext } from "../../types";
|
||||
import { ModeBase } from "../../ModeBase";
|
||||
|
||||
export class TestMode extends ModeBase {
|
||||
entry() {}
|
||||
|
||||
exit() {}
|
||||
|
||||
TEST_EVENT() {}
|
||||
|
||||
MOUSE_MOVE(e: MouseCoords) {}
|
||||
}
|
24
src/modes/types.ts
Normal file
24
src/modes/types.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { Renderer } from "../renderer/Renderer";
|
||||
import type { ModeBase } from "./ModeBase";
|
||||
|
||||
export interface Mode {
|
||||
initial: string;
|
||||
ctx: ModeContext;
|
||||
destroy?: () => void;
|
||||
}
|
||||
|
||||
export interface MouseCoords {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Mouse {
|
||||
position: MouseCoords;
|
||||
delta: MouseCoords | null;
|
||||
}
|
||||
|
||||
export interface ModeContext {
|
||||
renderer: Renderer;
|
||||
activateMode: (mode: typeof ModeBase) => void;
|
||||
deactivate: () => void;
|
||||
}
|
Loading…
Reference in a new issue