mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2025-12-24 06:58:48 -05:00
feat: adds ability to remove a node
This commit is contained in:
@@ -6,11 +6,12 @@ import { theme } from "./theme";
|
||||
import { SideNav } from "./components/SideNav";
|
||||
import { Sidebar } from "./components/Sidebars";
|
||||
import { ToolMenu } from "./components/ToolMenu";
|
||||
import { ContextMenu } from "./components/ContextMenus";
|
||||
import { RendererContainer } from "./components/RendererContainer";
|
||||
import { SceneI } from "./validation/SceneSchema";
|
||||
import { ModeManagerProvider } from "./contexts/ModeManagerContext";
|
||||
import { useGlobalState } from "./hooks/useGlobalState";
|
||||
import { OnSceneChange } from "./renderer/types";
|
||||
import { OnSceneChange } from "./types";
|
||||
|
||||
interface Props {
|
||||
initialScene: SceneI;
|
||||
@@ -33,6 +34,7 @@ const InnerApp = React.memo(
|
||||
}}
|
||||
>
|
||||
<RendererContainer />
|
||||
<ContextMenu />
|
||||
<Sidebar />
|
||||
<SideNav />
|
||||
<ToolMenu />
|
||||
@@ -42,7 +44,6 @@ const InnerApp = React.memo(
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const App = observer(
|
||||
({ initialScene, width, height, onSceneChange }: Props) => {
|
||||
const setInitialScene = useGlobalState((state) => state.setInitialScene);
|
||||
|
||||
53
src/components/ContextMenus/ContextMenu.tsx
Normal file
53
src/components/ContextMenus/ContextMenu.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
import { List, Box, Card } from "@mui/material";
|
||||
import { keyframes } from "@emotion/react";
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
position: { x: number; y: number };
|
||||
}
|
||||
|
||||
const COLOR = "grey.900";
|
||||
const SIZE = 14;
|
||||
const ANIMATIONS = {
|
||||
in: keyframes`
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(15px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
export const ContextMenu = ({ position, children }: Props) => {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
top: position.y,
|
||||
left: position.x + SIZE,
|
||||
animation: `${ANIMATIONS.in} 0.2s ease-in-out`,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
left: -(SIZE - 2),
|
||||
top: 8,
|
||||
width: 0,
|
||||
height: 0,
|
||||
borderTop: `${SIZE}px solid transparent`,
|
||||
borderBottom: `${SIZE}px solid transparent`,
|
||||
borderRight: `${SIZE}px solid`,
|
||||
borderRightColor: COLOR,
|
||||
}}
|
||||
/>
|
||||
<Card sx={{ borderRadius: 2 }}>
|
||||
<List sx={{ p: 0 }}>{children}</List>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
42
src/components/ContextMenus/ContextMenuItem.tsx
Normal file
42
src/components/ContextMenus/ContextMenuItem.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React, { useMemo } from "react";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import ListItemButton from "@mui/material/ListItemButton";
|
||||
import ListItemIcon from "@mui/material/ListItemIcon";
|
||||
import ListItemText from "@mui/material/ListItemText";
|
||||
|
||||
interface Props {
|
||||
onClick: () => void;
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const ContextMenuItem = ({ onClick, icon, label }: Props) => {
|
||||
return (
|
||||
<ListItem
|
||||
sx={{
|
||||
p: 0,
|
||||
"&:not(:last-child)": {
|
||||
borderBottom: "1px solid",
|
||||
borderBottomColor: "grey.800",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemButton onClick={onClick} sx={{ py: 0.5, px: 2 }}>
|
||||
<ListItemIcon
|
||||
sx={{
|
||||
pr: 1,
|
||||
minWidth: "auto",
|
||||
color: "grey.400",
|
||||
svg: {
|
||||
maxWidth: 18,
|
||||
maxHeight: 18,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
</ListItemIcon>
|
||||
<ListItemText secondary={label} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
34
src/components/ContextMenus/NodeContextMenu.tsx
Normal file
34
src/components/ContextMenus/NodeContextMenu.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import { useGlobalState } from "../../hooks/useGlobalState";
|
||||
import { ContextMenu } from "./ContextMenu";
|
||||
import { ContextMenuItem } from "./ContextMenuItem";
|
||||
import { Node } from "../../renderer/elements/Node";
|
||||
import { ArrowRightAlt, Delete } from "@mui/icons-material";
|
||||
|
||||
interface Props {
|
||||
node: Node;
|
||||
}
|
||||
|
||||
export const NodeContextMenu = ({ node }: Props) => {
|
||||
const renderer = useGlobalState((state) => state.renderer);
|
||||
const position = renderer.getTileScreenPosition(
|
||||
node.position.x,
|
||||
node.position.y
|
||||
);
|
||||
|
||||
return (
|
||||
<ContextMenu position={position}>
|
||||
<ContextMenuItem
|
||||
onClick={() => {}}
|
||||
icon={<ArrowRightAlt />}
|
||||
label="Connect"
|
||||
/>
|
||||
<ContextMenuItem
|
||||
onClick={node.destroy}
|
||||
icon={<Delete />}
|
||||
label="Remove"
|
||||
/>
|
||||
</ContextMenu>
|
||||
);
|
||||
};
|
||||
13
src/components/ContextMenus/index.tsx
Normal file
13
src/components/ContextMenus/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react";
|
||||
import { useGlobalState } from "../../hooks/useGlobalState";
|
||||
import { NodeContextMenu } from "./NodeContextMenu";
|
||||
|
||||
export const ContextMenu = () => {
|
||||
const targetElement = useGlobalState((state) => state.showContextMenuAt);
|
||||
|
||||
if (!targetElement) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <NodeContextMenu node={targetElement} />;
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useRef, useEffect, useContext } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Box from "@mui/material/Box";
|
||||
import { Renderer } from "../renderer/Renderer";
|
||||
import { useGlobalState } from "../hooks/useGlobalState";
|
||||
import { useMouseInput } from "../hooks/useMouseInput";
|
||||
@@ -17,10 +16,10 @@ export const RendererContainer = observer(() => {
|
||||
useEffect(() => {
|
||||
if (!rendererEl.current) return;
|
||||
|
||||
const renderer = new Renderer(rendererEl.current, onSceneChange);
|
||||
setRenderer(renderer);
|
||||
const renderer = setRenderer(rendererEl.current);
|
||||
setDomEl(rendererEl.current);
|
||||
modeManager.setRenderer(renderer);
|
||||
modeManager.setEventEmitter(renderer.callbacks.emitEvent);
|
||||
modeManager.activateMode(Select);
|
||||
|
||||
setCallbacks({
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { create } from "zustand";
|
||||
import { SceneI } from "../validation/SceneSchema";
|
||||
import { Node } from "../renderer/elements/Node";
|
||||
import { Renderer } from "../renderer/Renderer";
|
||||
import { OnSceneChange } from "../renderer/types";
|
||||
import { getRandom } from "../utils";
|
||||
import { OnSceneChange, SceneEventI } from "../types";
|
||||
|
||||
interface GlobalState {
|
||||
showContextMenuAt: Node | null;
|
||||
onSceneChange: OnSceneChange;
|
||||
setOnSceneChange: (onSceneChange: OnSceneChange) => void;
|
||||
initialScene: SceneI;
|
||||
@@ -13,10 +14,15 @@ interface GlobalState {
|
||||
setSelectedSideNavItem: (index: number) => void;
|
||||
closeSideNav: () => void;
|
||||
renderer: Renderer;
|
||||
setRenderer: (renderer: Renderer) => void;
|
||||
selectedElements: string[];
|
||||
setRenderer: (containerEl: HTMLDivElement) => Renderer;
|
||||
onRendererEvent: (event: SceneEventI) => void;
|
||||
}
|
||||
|
||||
export const useGlobalState = create<GlobalState>((set, get) => ({
|
||||
showContextMenuAt: null,
|
||||
selectedElements: [],
|
||||
selectedSideNavItem: null,
|
||||
onSceneChange: () => {},
|
||||
setOnSceneChange: (onSceneChange) => set({ onSceneChange }),
|
||||
initialScene: {
|
||||
@@ -28,30 +34,63 @@ export const useGlobalState = create<GlobalState>((set, get) => ({
|
||||
setInitialScene: (scene) => {
|
||||
set({ initialScene: scene });
|
||||
},
|
||||
selectedSideNavItem: null,
|
||||
setSelectedSideNavItem: (val) => {
|
||||
set({ selectedSideNavItem: val });
|
||||
},
|
||||
closeSideNav: () => {
|
||||
set({ selectedSideNavItem: null });
|
||||
},
|
||||
renderer: new Renderer(document.createElement("div"), () => {}),
|
||||
setRenderer: (renderer: Renderer) =>
|
||||
renderer: new Renderer(document.createElement("div")),
|
||||
onRendererEvent: (event) => {
|
||||
const { renderer } = get();
|
||||
|
||||
switch (event.type) {
|
||||
case "GRID_SELECTED":
|
||||
set({ showContextMenuAt: null, selectedElements: [] });
|
||||
break;
|
||||
case "NODES_SELECTED":
|
||||
set({ selectedElements: event.data.nodes });
|
||||
|
||||
if (event.data.nodes.length === 1) {
|
||||
const node = renderer.sceneElements.nodes.getNodeById(
|
||||
event.data.nodes[0]
|
||||
);
|
||||
set({ showContextMenuAt: node });
|
||||
}
|
||||
break;
|
||||
case "NODE_REMOVED":
|
||||
set({
|
||||
showContextMenuAt: null,
|
||||
selectedElements: [],
|
||||
selectedSideNavItem: null,
|
||||
});
|
||||
break;
|
||||
case "NODE_MOVED":
|
||||
set({ showContextMenuAt: null, selectedElements: [] });
|
||||
break;
|
||||
case "ZOOM_CHANGED":
|
||||
set({ showContextMenuAt: null, selectedElements: [] });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
setRenderer: (containerEl) => {
|
||||
set((state) => {
|
||||
if (state.renderer) {
|
||||
state.renderer.destroy();
|
||||
}
|
||||
|
||||
const scene = state.initialScene;
|
||||
const renderer = new Renderer(containerEl);
|
||||
|
||||
renderer.setEventHandler(state.onRendererEvent);
|
||||
renderer.loadScene(scene);
|
||||
renderer.setZoom(state.renderer.zoom);
|
||||
|
||||
// setInterval(() => {
|
||||
// const node = renderer.sceneElements.nodes.getNodeById("Node1");
|
||||
// node?.moveTo(getRandom(-5, 5), getRandom(-5, 5));
|
||||
// }, 1000);
|
||||
|
||||
return { renderer };
|
||||
}),
|
||||
});
|
||||
|
||||
return get().renderer;
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -3,14 +3,14 @@ import ReactDOM from "react-dom/client";
|
||||
import App from "./App";
|
||||
import GlobalStyles from "@mui/material/GlobalStyles";
|
||||
import { mockScene } from "./mockData";
|
||||
import { OnSceneChange } from "./renderer/types";
|
||||
import { OnSceneChange } from "./types";
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById("root") as HTMLElement
|
||||
);
|
||||
|
||||
const DataLayer = () => {
|
||||
const onSceneChange = useCallback<OnSceneChange>((event, scene) => {}, []);
|
||||
const onSceneChange = useCallback<OnSceneChange>(() => {}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ModeContext, Mouse } from "./types";
|
||||
import { ModeContext, Mouse } from "../types";
|
||||
|
||||
export class ModeBase {
|
||||
ctx;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { makeAutoObservable, action } from "mobx";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import { Renderer } from "../renderer/Renderer";
|
||||
import { ModeBase } from "./ModeBase";
|
||||
import { Mouse } from "./types";
|
||||
import type { Mouse, OnSceneChange } from "../types";
|
||||
|
||||
export class ModeManager {
|
||||
// mobx requires all properties to be initialised explicitly (i.e. prop = undefined)
|
||||
@@ -15,6 +15,7 @@ export class ModeManager {
|
||||
position: { x: 0, y: 0 },
|
||||
delta: null,
|
||||
};
|
||||
emitEvent?: OnSceneChange;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
@@ -24,6 +25,10 @@ export class ModeManager {
|
||||
this.renderer = renderer;
|
||||
}
|
||||
|
||||
setEventEmitter(fn: OnSceneChange) {
|
||||
this.emitEvent = fn;
|
||||
}
|
||||
|
||||
activateMode<T extends typeof ModeBase>(
|
||||
Mode: T,
|
||||
init?: (instance: InstanceType<T>) => void
|
||||
@@ -39,6 +44,7 @@ export class ModeManager {
|
||||
instance: new Mode({
|
||||
renderer: this.renderer,
|
||||
activateMode: this.activateMode.bind(this),
|
||||
emitEvent: this.emitEvent ?? (() => {}),
|
||||
}),
|
||||
class: Mode,
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ModeBase } from "./ModeBase";
|
||||
import { Mouse } from "./types";
|
||||
import { Mouse } from "../types";
|
||||
import { Renderer } from "../renderer/Renderer";
|
||||
|
||||
const changeCursor = (cursorType: string, renderer: Renderer) => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ModeBase } from "./ModeBase";
|
||||
import { Mouse } from "./types";
|
||||
import { Mouse } from "../types";
|
||||
import { getTargetFromSelection } from "./utils";
|
||||
import { SelectNode } from "./SelectNode";
|
||||
import { Node } from "../renderer/elements/Node";
|
||||
@@ -38,7 +38,10 @@ export class Select extends ModeBase {
|
||||
|
||||
if (target instanceof Node) {
|
||||
this.ctx.activateMode(SelectNode, (instance) => (instance.node = target));
|
||||
return;
|
||||
}
|
||||
|
||||
this.ctx.emitEvent({ type: "GRID_SELECTED" });
|
||||
}
|
||||
|
||||
MOUSE_MOVE(mouse: Mouse) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { ModeBase } from "./ModeBase";
|
||||
import { Select } from "../modes/Select";
|
||||
import { Mouse, ModeContext } from "./types";
|
||||
import { Mouse, ModeContext } from "../types";
|
||||
import { Node } from "../renderer/elements/Node";
|
||||
|
||||
export class SelectNode extends ModeBase {
|
||||
node?: Node;
|
||||
hasMoved = false;
|
||||
|
||||
entry(mouse: Mouse) {
|
||||
const tile = this.ctx.renderer.getTileFromMouse(
|
||||
@@ -28,11 +29,21 @@ export class SelectNode extends ModeBase {
|
||||
mouse.position.y
|
||||
);
|
||||
|
||||
this.node.moveTo(tile.x, tile.y);
|
||||
this.ctx.renderer.sceneElements.cursor.displayAt(tile.x, tile.y);
|
||||
if (this.node.position.x !== tile.x || this.node.position.y !== tile.y) {
|
||||
this.node.moveTo(tile.x, tile.y);
|
||||
this.ctx.renderer.sceneElements.cursor.displayAt(tile.x, tile.y);
|
||||
this.hasMoved = true;
|
||||
}
|
||||
}
|
||||
|
||||
MOUSE_UP() {
|
||||
if (!this.node) return;
|
||||
|
||||
if (!this.hasMoved) {
|
||||
this.ctx.renderer.sceneElements.nodes.setSelectedNodes([this.node.id]);
|
||||
} else {
|
||||
}
|
||||
|
||||
this.ctx.activateMode(Select);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ describe("Mode manager functions correctly", () => {
|
||||
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 renderer = new Renderer({} as unknown as HTMLDivElement);
|
||||
const modeManager = new ModeManager();
|
||||
modeManager.setRenderer(renderer);
|
||||
modeManager.activateMode(TestMode);
|
||||
|
||||
2
src/modes/tests/fixtures/TestMode.ts
vendored
2
src/modes/tests/fixtures/TestMode.ts
vendored
@@ -1,4 +1,4 @@
|
||||
import { MouseCoords, ModeContext } from "../../types";
|
||||
import { MouseCoords, ModeContext } from "../../../types";
|
||||
import { ModeBase } from "../../ModeBase";
|
||||
|
||||
export class TestMode extends ModeBase {
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import { Renderer } from "../renderer/Renderer";
|
||||
import type { ModeManager } from "./ModeManager";
|
||||
|
||||
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: ModeManager["activateMode"];
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import { makeAutoObservable, toJS } from "mobx";
|
||||
import Paper, { Group } from "paper";
|
||||
import gsap from "gsap";
|
||||
import autobind from "auto-bind";
|
||||
@@ -8,9 +8,7 @@ import { PROJECTED_TILE_WIDTH, PROJECTED_TILE_HEIGHT } from "./constants";
|
||||
import { clamp } from "../utils";
|
||||
import { Nodes } from "./elements/Nodes";
|
||||
import { SceneI, IconI } from "../validation/SceneSchema";
|
||||
import { OnSceneChange } from "./types";
|
||||
import { createSceneEvent, SceneEvent } from "./SceneEvent";
|
||||
import { mockScene } from "../mockData";
|
||||
import { OnSceneChange, SceneEventI } from "../types";
|
||||
|
||||
interface Config {
|
||||
grid: {
|
||||
@@ -31,9 +29,8 @@ export class Renderer {
|
||||
},
|
||||
icons: [],
|
||||
};
|
||||
createSceneEvent: ReturnType<typeof createSceneEvent>;
|
||||
callbacks: {
|
||||
onSceneChange: OnSceneChange;
|
||||
emitEvent: OnSceneChange;
|
||||
};
|
||||
groups: {
|
||||
container: paper.Group;
|
||||
@@ -54,21 +51,19 @@ export class Renderer {
|
||||
};
|
||||
rafRef?: number;
|
||||
|
||||
constructor(containerEl: HTMLDivElement, onChange: OnSceneChange) {
|
||||
constructor(containerEl: HTMLDivElement) {
|
||||
makeAutoObservable(this);
|
||||
autobind(this);
|
||||
|
||||
this.createSceneEvent = createSceneEvent(this.onSceneChange);
|
||||
|
||||
this.callbacks = {
|
||||
onSceneChange: onChange,
|
||||
};
|
||||
|
||||
Paper.settings = {
|
||||
insertelements: false,
|
||||
applyMatrix: false,
|
||||
};
|
||||
|
||||
this.callbacks = {
|
||||
emitEvent: () => {},
|
||||
};
|
||||
|
||||
this.domElements = {
|
||||
container: containerEl,
|
||||
...this.initDOM(containerEl),
|
||||
@@ -106,18 +101,16 @@ export class Renderer {
|
||||
|
||||
init() {}
|
||||
|
||||
loadScene(scene: SceneI) {
|
||||
const sceneEvent = this.createSceneEvent({
|
||||
type: "SCENE_LOAD",
|
||||
});
|
||||
setEventHandler(eventHandler: OnSceneChange) {
|
||||
this.callbacks.emitEvent = eventHandler;
|
||||
}
|
||||
|
||||
loadScene(scene: SceneI) {
|
||||
this.config.icons = scene.icons;
|
||||
|
||||
scene.nodes.forEach((node) => {
|
||||
this.sceneElements.nodes.addNode(node, sceneEvent);
|
||||
this.sceneElements.nodes.addNode(node);
|
||||
});
|
||||
|
||||
sceneEvent.complete();
|
||||
}
|
||||
|
||||
getIconById(id: string) {
|
||||
@@ -192,6 +185,33 @@ export class Renderer {
|
||||
};
|
||||
}
|
||||
|
||||
getTileScreenPosition(x: number, y: number) {
|
||||
const { width: viewW, height: viewH } = Paper.view.bounds;
|
||||
const { offsetLeft: offsetX, offsetTop: offsetY } = this.domElements.canvas;
|
||||
const tilePosition = this.getTileBounds(x, y).center;
|
||||
const globalItemsGroupPosition = this.groups.elements.globalToLocal([0, 0]);
|
||||
const screenPosition = {
|
||||
x:
|
||||
(tilePosition.x +
|
||||
this.scrollPosition.x +
|
||||
globalItemsGroupPosition.x +
|
||||
this.groups.elements.position.x +
|
||||
viewW * 0.5) *
|
||||
this.zoom +
|
||||
offsetX,
|
||||
y:
|
||||
(tilePosition.y +
|
||||
this.scrollPosition.y +
|
||||
globalItemsGroupPosition.y +
|
||||
this.groups.elements.position.y +
|
||||
viewH * 0.5) *
|
||||
this.zoom +
|
||||
offsetY,
|
||||
};
|
||||
|
||||
return screenPosition;
|
||||
}
|
||||
|
||||
setGrid(width: number, height: number) {}
|
||||
|
||||
setZoom(zoom: number) {
|
||||
@@ -202,6 +222,11 @@ export class Renderer {
|
||||
duration: 0.3,
|
||||
zoom: this.zoom,
|
||||
});
|
||||
|
||||
this.emitEvent({
|
||||
type: "ZOOM_CHANGED",
|
||||
data: { level: zoom },
|
||||
});
|
||||
}
|
||||
|
||||
scrollTo(x: number, y: number) {
|
||||
@@ -247,7 +272,7 @@ export class Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
exportScene() {
|
||||
exportScene(): SceneI {
|
||||
const exported = {
|
||||
icons: this.config.icons,
|
||||
nodes: this.sceneElements.nodes.export(),
|
||||
@@ -258,17 +283,13 @@ export class Renderer {
|
||||
return exported;
|
||||
}
|
||||
|
||||
onSceneChange(sceneEvent: SceneEvent) {
|
||||
this.callbacks.onSceneChange(sceneEvent.event, this.exportScene());
|
||||
emitEvent(event: SceneEventI) {
|
||||
this.callbacks.emitEvent(event);
|
||||
}
|
||||
|
||||
getItemsByTile(x: number, y: number) {
|
||||
const node = this.nodes.getNodeByTile(x, y);
|
||||
const node = this.sceneElements.nodes.getNodeByTile(x, y);
|
||||
|
||||
return [node].filter((i) => Boolean(i));
|
||||
}
|
||||
|
||||
get nodes() {
|
||||
return this.sceneElements.nodes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Group } from "paper";
|
||||
import { Context } from "./types";
|
||||
import { Context } from "../types";
|
||||
|
||||
export class SceneElement {
|
||||
container = new Group();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import cuid from "cuid";
|
||||
import { SceneEventI } from "./types";
|
||||
import { SceneEventI } from "../types";
|
||||
|
||||
type OnSceneEventComplete = (event: SceneEvent) => void;
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
getBoundingBox,
|
||||
getTileBounds,
|
||||
} from "../utils/gridHelpers";
|
||||
import type { Context, Coords } from "../types";
|
||||
import type { Context, Coords } from "../../types";
|
||||
import { SceneElement } from "../SceneElement";
|
||||
import { tweenPosition } from "../../utils";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Group, Path, Point } from "paper";
|
||||
import { applyProjectionMatrix } from "../utils/projection";
|
||||
import type { Context } from "../types";
|
||||
import type { Context } from "../../types";
|
||||
import { TILE_SIZE, PIXEL_UNIT, SCALING_CONST } from "../constants";
|
||||
import { SceneElement } from "../SceneElement";
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import { Group, Raster } from "paper";
|
||||
import { Coords, Context } from "../types";
|
||||
import { IconI } from "../../validation/SceneSchema";
|
||||
import { Coords, Context } from "../../types";
|
||||
import { PROJECTED_TILE_WIDTH, PIXEL_UNIT } from "../constants";
|
||||
|
||||
const NODE_IMG_PADDING = 0 * PIXEL_UNIT;
|
||||
@@ -14,6 +13,7 @@ export interface NodeOptions {
|
||||
|
||||
interface Callbacks {
|
||||
onMove: (x: number, y: number, node: Node) => void;
|
||||
onDestroy: (node: Node) => void;
|
||||
}
|
||||
|
||||
export class Node {
|
||||
@@ -21,6 +21,7 @@ export class Node {
|
||||
container = new Group();
|
||||
|
||||
id;
|
||||
selected = false;
|
||||
callbacks: Callbacks;
|
||||
position;
|
||||
icon;
|
||||
@@ -41,6 +42,8 @@ export class Node {
|
||||
this.renderElements.iconContainer.addChild(this.renderElements.icon);
|
||||
this.container.addChild(this.renderElements.iconContainer);
|
||||
|
||||
this.destroy = this.destroy.bind(this);
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
@@ -87,5 +90,6 @@ export class Node {
|
||||
|
||||
destroy() {
|
||||
this.container.remove();
|
||||
this.callbacks.onDestroy(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Group } from "paper";
|
||||
import gsap from "gsap";
|
||||
import autobind from "auto-bind";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import { Context } from "../types";
|
||||
import { makeAutoObservable, toJS } from "mobx";
|
||||
import { Context } from "../../types";
|
||||
import { Node, NodeOptions } from "./Node";
|
||||
import cuid from "cuid";
|
||||
import { SceneElement } from "../SceneElement";
|
||||
import { SceneEvent } from "../SceneEvent";
|
||||
import { tweenPosition } from "../../utils";
|
||||
|
||||
@@ -13,6 +11,7 @@ export class Nodes {
|
||||
ctx: Context;
|
||||
container = new Group();
|
||||
nodes: Node[] = [];
|
||||
selected: Node[] = [];
|
||||
|
||||
constructor(ctx: Context) {
|
||||
makeAutoObservable(this);
|
||||
@@ -30,24 +29,23 @@ export class Nodes {
|
||||
},
|
||||
{
|
||||
onMove: this.onMove.bind(this),
|
||||
onDestroy: this.onDestroy.bind(this),
|
||||
}
|
||||
);
|
||||
|
||||
this.nodes.push(node);
|
||||
this.container.addChild(node.container);
|
||||
|
||||
this.ctx
|
||||
.createSceneEvent(
|
||||
{
|
||||
type: "NODE_CREATED",
|
||||
node,
|
||||
},
|
||||
sceneEvent
|
||||
)
|
||||
.complete();
|
||||
this.ctx.emitEvent({
|
||||
type: "NODE_CREATED",
|
||||
data: { node: node.id },
|
||||
});
|
||||
}
|
||||
|
||||
onMove(x: number, y: number, node: Node, opts?: { skipAnimation: boolean }) {
|
||||
const from = node.position;
|
||||
const to = { x, y };
|
||||
|
||||
const tile = this.ctx.getTileBounds(x, y);
|
||||
node.position = {
|
||||
x,
|
||||
@@ -58,6 +56,25 @@ export class Nodes {
|
||||
...tile.bottom,
|
||||
duration: opts?.skipAnimation ? 0 : 0.05,
|
||||
});
|
||||
|
||||
this.ctx.emitEvent({
|
||||
type: "NODE_MOVED",
|
||||
data: { node: node.id, from, to },
|
||||
});
|
||||
}
|
||||
|
||||
onDestroy(node: Node) {
|
||||
const id = node.id;
|
||||
const nodeIndex = this.nodes.indexOf(node);
|
||||
|
||||
if (nodeIndex === -1) return;
|
||||
|
||||
this.nodes.splice(nodeIndex, 1);
|
||||
|
||||
this.ctx.emitEvent({
|
||||
type: "NODE_REMOVED",
|
||||
data: { node: id },
|
||||
});
|
||||
}
|
||||
|
||||
getNodeById(id: string) {
|
||||
@@ -75,6 +92,17 @@ export class Nodes {
|
||||
this.nodes = [];
|
||||
}
|
||||
|
||||
setSelectedNodes(ids: string[]) {
|
||||
this.nodes.forEach((node) => {
|
||||
node.selected = ids.includes(node.id);
|
||||
});
|
||||
|
||||
this.ctx.emitEvent({
|
||||
type: "NODES_SELECTED",
|
||||
data: { nodes: ids },
|
||||
});
|
||||
}
|
||||
|
||||
export() {
|
||||
const exported = this.nodes.map((node) => node.export());
|
||||
return exported;
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
import type { Renderer } from "./Renderer";
|
||||
import { SceneI } from "../validation/SceneSchema";
|
||||
import type { Node } from "./elements/Node";
|
||||
|
||||
export interface Coords {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export type GeneralEventI = {
|
||||
type: "SCENE_LOAD";
|
||||
};
|
||||
|
||||
export type NodeEventI =
|
||||
| {
|
||||
type: "NODE_CREATED";
|
||||
node: Node;
|
||||
}
|
||||
| {
|
||||
type: "NODE_REMOVED";
|
||||
node: Node;
|
||||
};
|
||||
|
||||
export type SceneEventI = NodeEventI | GeneralEventI;
|
||||
|
||||
export type Context = Renderer;
|
||||
|
||||
export type OnSceneChange = (event: SceneEventI, scene: SceneI) => void;
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PROJECTED_TILE_HEIGHT, PROJECTED_TILE_WIDTH } from "../constants";
|
||||
import { Coords } from "../types";
|
||||
import { Coords } from "../../types";
|
||||
|
||||
// Iterates over every item in a 2 dimensional array
|
||||
// const tileIterator = (w, h, cb) => {
|
||||
|
||||
81
src/types.ts
Normal file
81
src/types.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Renderer } from "./renderer/Renderer";
|
||||
import type { ModeManager } from "./modes/ModeManager";
|
||||
import type { Node } from "./renderer/elements/Node";
|
||||
|
||||
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: ModeManager["activateMode"];
|
||||
emitEvent: OnSceneChange;
|
||||
}
|
||||
|
||||
export interface Coords {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export type GeneralEventI = {
|
||||
type: "SCENE_LOAD";
|
||||
data: {};
|
||||
};
|
||||
|
||||
export type NodeEventI =
|
||||
// Grid Events
|
||||
| {
|
||||
type: "GRID_SELECTED";
|
||||
}
|
||||
// Node Events
|
||||
| {
|
||||
type: "NODE_CREATED";
|
||||
data: {
|
||||
node: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "NODE_REMOVED";
|
||||
data: {
|
||||
node: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "NODES_SELECTED";
|
||||
data: {
|
||||
nodes: string[];
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "NODE_MOVED";
|
||||
data: {
|
||||
node: string;
|
||||
from: Coords;
|
||||
to: Coords;
|
||||
};
|
||||
}
|
||||
// Utility Events
|
||||
| {
|
||||
type: "ZOOM_CHANGED";
|
||||
data: {
|
||||
level: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type SceneEventI = NodeEventI | GeneralEventI;
|
||||
|
||||
export type Context = Renderer;
|
||||
|
||||
export type OnSceneChange = (event: SceneEventI) => void;
|
||||
Reference in New Issue
Block a user