mirror of
https://github.com/stan-smith/FossFLOW.git
synced 2026-04-23 08:31:16 -04:00
feat: renders a basic node to scene
This commit is contained in:
@@ -43,7 +43,7 @@ import Isoflow from 'isoflow';
|
||||
const App = () => (
|
||||
<Isoflow
|
||||
height={500}
|
||||
initialScene={{
|
||||
scene={{
|
||||
icons: [],
|
||||
nodes: [],
|
||||
connectors: [],
|
||||
|
||||
36
package-lock.json
generated
36
package-lock.json
generated
@@ -14,6 +14,8 @@
|
||||
"@mui/icons-material": "^5.11.9",
|
||||
"@mui/material": "^5.11.10",
|
||||
"auto-bind": "^5.0.1",
|
||||
"cuid": "^3.0.0",
|
||||
"deep-diff": "^1.0.2",
|
||||
"gsap": "^3.11.4",
|
||||
"mobx": "^6.8.0",
|
||||
"mobx-react": "^7.6.0",
|
||||
@@ -27,6 +29,7 @@
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/deep-diff": "^1.0.2",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/jsdom": "^21.1.0",
|
||||
"@types/node": "^16.18.12",
|
||||
@@ -2327,6 +2330,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/deep-diff": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-diff/-/deep-diff-1.0.2.tgz",
|
||||
"integrity": "sha512-WD2O611C7Oz7RSwKbSls8LaznKfWfXh39CHY9Amd8FhQz+NJRe20nUHhYpOopVq9M2oqDZd4L6AzqJIXQycxiA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/eslint": {
|
||||
"version": "8.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.1.tgz",
|
||||
@@ -3982,6 +3991,12 @@
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
|
||||
"integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
|
||||
},
|
||||
"node_modules/cuid": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cuid/-/cuid-3.0.0.tgz",
|
||||
"integrity": "sha512-WZYYkHdIDnaxdeP8Misq3Lah5vFjJwGuItJuV+tvMafosMzw0nF297T7mrm8IOWiPJkV6gc7sa8pzx27+w25Zg==",
|
||||
"deprecated": "Cuid and other k-sortable and non-cryptographic ids (Ulid, ObjectId, KSUID, all UUIDs) are all insecure. Use @paralleldrive/cuid2 instead."
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz",
|
||||
@@ -4038,6 +4053,11 @@
|
||||
"integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/deep-diff": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz",
|
||||
"integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg=="
|
||||
},
|
||||
"node_modules/deep-equal": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz",
|
||||
@@ -13141,6 +13161,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/deep-diff": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/deep-diff/-/deep-diff-1.0.2.tgz",
|
||||
"integrity": "sha512-WD2O611C7Oz7RSwKbSls8LaznKfWfXh39CHY9Amd8FhQz+NJRe20nUHhYpOopVq9M2oqDZd4L6AzqJIXQycxiA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/eslint": {
|
||||
"version": "8.21.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.1.tgz",
|
||||
@@ -14490,6 +14516,11 @@
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz",
|
||||
"integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw=="
|
||||
},
|
||||
"cuid": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cuid/-/cuid-3.0.0.tgz",
|
||||
"integrity": "sha512-WZYYkHdIDnaxdeP8Misq3Lah5vFjJwGuItJuV+tvMafosMzw0nF297T7mrm8IOWiPJkV6gc7sa8pzx27+w25Zg=="
|
||||
},
|
||||
"data-urls": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz",
|
||||
@@ -14528,6 +14559,11 @@
|
||||
"integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==",
|
||||
"dev": true
|
||||
},
|
||||
"deep-diff": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz",
|
||||
"integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg=="
|
||||
},
|
||||
"deep-equal": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.0.tgz",
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/deep-diff": "^1.0.2",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/jsdom": "^21.1.0",
|
||||
"@types/node": "^16.18.12",
|
||||
@@ -47,6 +48,8 @@
|
||||
"@mui/icons-material": "^5.11.9",
|
||||
"@mui/material": "^5.11.10",
|
||||
"auto-bind": "^5.0.1",
|
||||
"cuid": "^3.0.0",
|
||||
"deep-diff": "^1.0.2",
|
||||
"gsap": "^3.11.4",
|
||||
"mobx": "^6.8.0",
|
||||
"mobx-react": "^7.6.0",
|
||||
|
||||
70
src/App.tsx
70
src/App.tsx
@@ -1,4 +1,5 @@
|
||||
import React, { useEffect } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import Box from "@mui/material/Box";
|
||||
import { theme } from "./theme";
|
||||
@@ -9,41 +10,56 @@ 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";
|
||||
|
||||
interface Props {
|
||||
initialScene: SceneI;
|
||||
onSceneChange: OnSceneChange;
|
||||
width?: number | string;
|
||||
height: number | string;
|
||||
}
|
||||
|
||||
const App = ({ initialScene, width, height }: Props) => {
|
||||
const setInitialScene = useGlobalState((state) => state.setInitialScene);
|
||||
const InnerApp = React.memo(
|
||||
({ height, width }: Pick<Props, "height" | "width">) => {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<ModeManagerProvider>
|
||||
<Box
|
||||
sx={{
|
||||
width: width ?? "100%",
|
||||
height,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<RendererContainer />
|
||||
<Sidebar />
|
||||
<SideNav />
|
||||
<ToolMenu />
|
||||
</Box>
|
||||
</ModeManagerProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setInitialScene(initialScene);
|
||||
}, [initialScene]);
|
||||
const App = observer(
|
||||
({ initialScene, width, height, onSceneChange }: Props) => {
|
||||
const setInitialScene = useGlobalState((state) => state.setInitialScene);
|
||||
const setOnSceneChange = useGlobalState((state) => state.setOnSceneChange);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<ModeManagerProvider>
|
||||
<Box
|
||||
sx={{
|
||||
width: width ?? "100%",
|
||||
height,
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<RendererContainer />
|
||||
<Sidebar />
|
||||
<SideNav />
|
||||
<ToolMenu />
|
||||
</Box>
|
||||
</ModeManagerProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
};
|
||||
useEffect(() => {
|
||||
setOnSceneChange(onSceneChange);
|
||||
}, [setOnSceneChange, onSceneChange]);
|
||||
|
||||
useEffect(() => {
|
||||
setInitialScene(initialScene);
|
||||
}, [initialScene, setInitialScene]);
|
||||
|
||||
return <InnerApp height={height} width={width} />;
|
||||
}
|
||||
);
|
||||
|
||||
type Scene = SceneI;
|
||||
export { Scene };
|
||||
export { Scene, OnSceneChange };
|
||||
export default App;
|
||||
|
||||
@@ -12,11 +12,12 @@ export const RendererContainer = observer(() => {
|
||||
const rendererEl = useRef<HTMLDivElement>(null);
|
||||
const { setDomEl, setCallbacks } = useMouseInput();
|
||||
const setRenderer = useGlobalState((state) => state.setRenderer);
|
||||
const onSceneChange = useGlobalState((state) => state.onSceneChange);
|
||||
|
||||
useEffect(() => {
|
||||
if (!rendererEl.current) return;
|
||||
|
||||
const renderer = new Renderer(rendererEl.current);
|
||||
const renderer = new Renderer(rendererEl.current, onSceneChange);
|
||||
setRenderer(renderer);
|
||||
setDomEl(rendererEl.current);
|
||||
modeManager.setRenderer(renderer);
|
||||
@@ -39,7 +40,7 @@ export const RendererContainer = observer(() => {
|
||||
modeManager.onMouseEvent("MOUSE_LEAVE", event);
|
||||
},
|
||||
});
|
||||
}, [setRenderer, setDomEl, modeManager]);
|
||||
}, [setRenderer, setDomEl, modeManager, onSceneChange]);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -9,13 +9,11 @@ import { useGlobalState } from "../../hooks/useGlobalState";
|
||||
|
||||
export const Sidebar = () => {
|
||||
const theme = useTheme();
|
||||
const { selectedSideNavItem, closeSideNav, icons } = useGlobalState(
|
||||
(state) => ({
|
||||
selectedSideNavItem: state.selectedSideNavItem,
|
||||
closeSideNav: state.closeSideNav,
|
||||
icons: state.initialScene.icons,
|
||||
})
|
||||
const selectedSideNavItem = useGlobalState(
|
||||
(state) => state.selectedSideNavItem
|
||||
);
|
||||
const closeSideNav = useGlobalState((state) => state.closeSideNav);
|
||||
const icons = useGlobalState((state) => state.renderer.config.icons);
|
||||
|
||||
return (
|
||||
<Slide
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { create } from "zustand";
|
||||
import { SceneI } from "../validation/SceneSchema";
|
||||
import { Renderer } from "../renderer/Renderer";
|
||||
import { OnSceneChange } from "../renderer/types";
|
||||
import { getRandom } from "../utils";
|
||||
|
||||
interface GlobalState {
|
||||
onSceneChange: OnSceneChange;
|
||||
setOnSceneChange: (onSceneChange: OnSceneChange) => void;
|
||||
initialScene: SceneI;
|
||||
setInitialScene: (scene: SceneI) => void;
|
||||
selectedSideNavItem: number | null;
|
||||
@@ -12,12 +16,14 @@ interface GlobalState {
|
||||
setRenderer: (renderer: Renderer) => void;
|
||||
}
|
||||
|
||||
export const useGlobalState = create<GlobalState>((set) => ({
|
||||
export const useGlobalState = create<GlobalState>((set, get) => ({
|
||||
onSceneChange: () => {},
|
||||
setOnSceneChange: (onSceneChange) => set({ onSceneChange }),
|
||||
initialScene: {
|
||||
icons: [],
|
||||
nodes: [],
|
||||
groups: [],
|
||||
connectors: [],
|
||||
groups: [],
|
||||
},
|
||||
setInitialScene: (scene) => {
|
||||
set({ initialScene: scene });
|
||||
@@ -29,15 +35,23 @@ export const useGlobalState = create<GlobalState>((set) => ({
|
||||
closeSideNav: () => {
|
||||
set({ selectedSideNavItem: null });
|
||||
},
|
||||
renderer: new Renderer(document.createElement("div")),
|
||||
renderer: new Renderer(document.createElement("div"), () => {}),
|
||||
setRenderer: (renderer: Renderer) =>
|
||||
set((state) => {
|
||||
if (state.renderer) {
|
||||
state.renderer.destroy();
|
||||
}
|
||||
|
||||
const scene = state.initialScene;
|
||||
|
||||
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 };
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -1,22 +1,37 @@
|
||||
import React from "react";
|
||||
import React, { useCallback } from "react";
|
||||
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";
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById("root") as HTMLElement
|
||||
);
|
||||
|
||||
const DataLayer = () => {
|
||||
const onSceneChange = useCallback<OnSceneChange>((event, scene) => {}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<GlobalStyles
|
||||
styles={{
|
||||
body: {
|
||||
margin: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<App
|
||||
initialScene={mockScene}
|
||||
onSceneChange={onSceneChange}
|
||||
height="100vh"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<GlobalStyles
|
||||
styles={{
|
||||
body: {
|
||||
margin: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<App initialScene={mockScene} height="100vh" />
|
||||
<DataLayer />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Icon } from "./types";
|
||||
import type { SceneI } from "./validation/SceneSchema";
|
||||
import type { SceneI, IconI, NodeI } from "./validation/SceneSchema";
|
||||
|
||||
export const mockIcons: Icon[] = [
|
||||
export const icons: IconI[] = [
|
||||
{
|
||||
id: "block",
|
||||
name: "Block",
|
||||
@@ -51,9 +50,21 @@ export const mockIcons: Icon[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const nodes: NodeI[] = [
|
||||
{
|
||||
id: "Node1",
|
||||
label: "Node 1",
|
||||
icon: "block",
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const mockScene: SceneI = {
|
||||
icons: mockIcons,
|
||||
nodes: [],
|
||||
icons,
|
||||
nodes,
|
||||
connectors: [],
|
||||
groups: [],
|
||||
};
|
||||
|
||||
@@ -1,19 +1,39 @@
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import Paper, { Group } from "paper";
|
||||
import gsap from "gsap";
|
||||
import autobind from "auto-bind";
|
||||
import { Grid } from "./elements/Grid";
|
||||
import { Cursor } from "./elements/Cursor";
|
||||
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";
|
||||
|
||||
interface Config {
|
||||
grid: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
icons: IconI[];
|
||||
}
|
||||
|
||||
export class Renderer {
|
||||
activeLayer: paper.Layer;
|
||||
zoom: number = 1;
|
||||
zoom = 1;
|
||||
|
||||
config = {
|
||||
config: Config = {
|
||||
grid: {
|
||||
width: 51,
|
||||
height: 51,
|
||||
},
|
||||
icons: [],
|
||||
};
|
||||
createSceneEvent: ReturnType<typeof createSceneEvent>;
|
||||
callbacks: {
|
||||
onSceneChange: OnSceneChange;
|
||||
};
|
||||
groups: {
|
||||
container: paper.Group;
|
||||
@@ -22,6 +42,7 @@ export class Renderer {
|
||||
sceneElements: {
|
||||
grid: Grid;
|
||||
cursor: Cursor;
|
||||
nodes: Nodes;
|
||||
};
|
||||
domElements: {
|
||||
container: HTMLDivElement;
|
||||
@@ -33,7 +54,16 @@ export class Renderer {
|
||||
};
|
||||
rafRef?: number;
|
||||
|
||||
constructor(containerEl: HTMLDivElement) {
|
||||
constructor(containerEl: HTMLDivElement, onChange: OnSceneChange) {
|
||||
makeAutoObservable(this);
|
||||
autobind(this);
|
||||
|
||||
this.createSceneEvent = createSceneEvent(this.onSceneChange);
|
||||
|
||||
this.callbacks = {
|
||||
onSceneChange: onChange,
|
||||
};
|
||||
|
||||
Paper.settings = {
|
||||
insertelements: false,
|
||||
applyMatrix: false,
|
||||
@@ -47,8 +77,9 @@ export class Renderer {
|
||||
Paper.setup(this.domElements.canvas);
|
||||
|
||||
this.sceneElements = {
|
||||
grid: new Grid(this.config),
|
||||
cursor: new Cursor(this.config),
|
||||
grid: new Grid(this),
|
||||
cursor: new Cursor(this),
|
||||
nodes: new Nodes(this),
|
||||
};
|
||||
|
||||
this.groups = {
|
||||
@@ -58,6 +89,7 @@ export class Renderer {
|
||||
|
||||
this.groups.elements.addChild(this.sceneElements.grid.container);
|
||||
this.groups.elements.addChild(this.sceneElements.cursor.container);
|
||||
this.groups.elements.addChild(this.sceneElements.nodes.container);
|
||||
|
||||
this.groups.container.addChild(this.groups.elements);
|
||||
this.groups.container.set({ position: [0, 0] });
|
||||
@@ -68,6 +100,30 @@ export class Renderer {
|
||||
this.scrollTo(0, 0);
|
||||
|
||||
this.render();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {}
|
||||
|
||||
loadScene(scene: SceneI) {
|
||||
const sceneEvent = this.createSceneEvent({
|
||||
type: "SCENE_LOAD",
|
||||
});
|
||||
|
||||
this.config.icons = scene.icons;
|
||||
|
||||
scene.nodes.forEach((node) => {
|
||||
this.sceneElements.nodes.addNode(
|
||||
{
|
||||
...node,
|
||||
icon: mockScene.icons[0],
|
||||
},
|
||||
sceneEvent
|
||||
);
|
||||
});
|
||||
|
||||
sceneEvent.complete();
|
||||
}
|
||||
|
||||
initDOM(containerEl: HTMLDivElement) {
|
||||
@@ -104,6 +160,34 @@ export class Renderer {
|
||||
};
|
||||
}
|
||||
|
||||
getTilePosition(x: number, y: number) {
|
||||
const halfW = PROJECTED_TILE_WIDTH * 0.5;
|
||||
const halfH = PROJECTED_TILE_HEIGHT * 0.5;
|
||||
|
||||
return {
|
||||
x: x * halfW - y * halfW,
|
||||
y: x * halfH + y * halfH,
|
||||
};
|
||||
}
|
||||
|
||||
getTileBounds(x: number, y: number) {
|
||||
const position = this.getTilePosition(x, y);
|
||||
|
||||
return {
|
||||
left: {
|
||||
x: position.x - PROJECTED_TILE_WIDTH * 0.5,
|
||||
y: position.y - PROJECTED_TILE_HEIGHT * 0.5,
|
||||
},
|
||||
right: {
|
||||
x: position.x + PROJECTED_TILE_WIDTH * 0.5,
|
||||
y: position.y - PROJECTED_TILE_HEIGHT * 0.5,
|
||||
},
|
||||
top: { x: position.x, y: position.y - PROJECTED_TILE_HEIGHT },
|
||||
bottom: { x: position.x, y: position.y },
|
||||
center: { x: position.x, y: position.y - PROJECTED_TILE_HEIGHT * 0.5 },
|
||||
};
|
||||
}
|
||||
|
||||
setGrid(width: number, height: number) {}
|
||||
|
||||
setZoom(zoom: number) {
|
||||
@@ -139,6 +223,10 @@ export class Renderer {
|
||||
);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.sceneElements.nodes.clear();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.domElements.canvas.remove();
|
||||
|
||||
@@ -154,4 +242,23 @@ export class Renderer {
|
||||
Paper.view.update();
|
||||
}
|
||||
}
|
||||
|
||||
exportScene() {
|
||||
const exported = {
|
||||
icons: this.config.icons,
|
||||
nodes: this.sceneElements.nodes.export(),
|
||||
groups: [],
|
||||
connectors: [],
|
||||
};
|
||||
|
||||
return exported;
|
||||
}
|
||||
|
||||
onSceneChange(sceneEvent: SceneEvent) {
|
||||
this.callbacks.onSceneChange(sceneEvent.event, this.exportScene());
|
||||
}
|
||||
|
||||
get nodes() {
|
||||
return this.sceneElements.nodes;
|
||||
}
|
||||
}
|
||||
|
||||
22
src/renderer/SceneElement.ts
Normal file
22
src/renderer/SceneElement.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Group } from "paper";
|
||||
import { Context } from "./types";
|
||||
|
||||
export class SceneElement {
|
||||
container = new Group();
|
||||
ctx: Context;
|
||||
|
||||
constructor(ctx: Context) {
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.container.removeChildren();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.clear();
|
||||
this.container.remove();
|
||||
}
|
||||
|
||||
export() {}
|
||||
}
|
||||
43
src/renderer/SceneEvent.ts
Normal file
43
src/renderer/SceneEvent.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import cuid from "cuid";
|
||||
import { SceneEventI } from "./types";
|
||||
|
||||
type OnSceneEventComplete = (event: SceneEvent) => void;
|
||||
|
||||
interface SceneEventArgs {
|
||||
onComplete?: OnSceneEventComplete;
|
||||
parentEvent?: SceneEvent;
|
||||
}
|
||||
|
||||
export class SceneEvent {
|
||||
id = cuid();
|
||||
timeStarted = Date.now();
|
||||
timeCompleted?: number;
|
||||
event: SceneEventI;
|
||||
cascadedEvents: SceneEventI[] = [];
|
||||
onComplete?: (event: SceneEvent) => void;
|
||||
parentEvent?: SceneEvent;
|
||||
|
||||
constructor(event: SceneEventI, opts: SceneEventArgs) {
|
||||
this.event = event;
|
||||
this.onComplete = opts.onComplete;
|
||||
this.parentEvent = opts.parentEvent;
|
||||
}
|
||||
|
||||
attachEvent(event: SceneEventI) {
|
||||
this.cascadedEvents.push(event);
|
||||
}
|
||||
|
||||
complete() {
|
||||
if (this.parentEvent) this.parentEvent.attachEvent(this.event);
|
||||
|
||||
if (!this.parentEvent) this.onComplete?.(this);
|
||||
|
||||
this.timeCompleted = Date.now();
|
||||
}
|
||||
}
|
||||
|
||||
export const createSceneEvent =
|
||||
(onComplete: OnSceneEventComplete) =>
|
||||
(event: SceneEventI, opts?: SceneEventArgs) => {
|
||||
return new SceneEvent(event, { ...opts, onComplete });
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Group, Shape, Point } from "paper";
|
||||
import { Shape, Point } from "paper";
|
||||
import { gsap } from "gsap";
|
||||
import { applyProjectionMatrix } from "../utils/projection";
|
||||
import { TILE_SIZE, PIXEL_UNIT } from "../constants";
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
getBoundingBox,
|
||||
getTileBounds,
|
||||
} from "../utils/gridHelpers";
|
||||
import type { SceneElement, Context, Coords } from "../types";
|
||||
import type { Context, Coords } from "../types";
|
||||
import { SceneElement } from "../SceneElement";
|
||||
|
||||
export enum CURSOR_TYPES {
|
||||
OUTLINE = "OUTLINE",
|
||||
@@ -17,12 +18,9 @@ export enum CURSOR_TYPES {
|
||||
DOT = "DOT",
|
||||
}
|
||||
|
||||
export class Cursor implements SceneElement {
|
||||
ctx: Context;
|
||||
container = new Group();
|
||||
|
||||
export class Cursor extends SceneElement {
|
||||
renderElements = {
|
||||
rectangle: new Shape.Rectangle({}),
|
||||
rectangle: new Shape.Rectangle([0, 0]),
|
||||
};
|
||||
|
||||
animations: {
|
||||
@@ -42,7 +40,9 @@ export class Cursor implements SceneElement {
|
||||
currentType?: CURSOR_TYPES;
|
||||
|
||||
constructor(ctx: Context) {
|
||||
this.ctx = ctx;
|
||||
super(ctx);
|
||||
|
||||
this.renderElements.rectangle = new Shape.Rectangle({});
|
||||
|
||||
this.animations = {
|
||||
highlight: gsap
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import { Group, Path, Point } from "paper";
|
||||
import { applyProjectionMatrix } from "../utils/projection";
|
||||
import type { SceneElement, Context } from "../types";
|
||||
import type { Context } from "../types";
|
||||
import { TILE_SIZE, PIXEL_UNIT, SCALING_CONST } from "../constants";
|
||||
import { SceneElement } from "../SceneElement";
|
||||
|
||||
export class Grid implements SceneElement {
|
||||
ctx: Context;
|
||||
export class Grid extends SceneElement {
|
||||
container = new Group();
|
||||
|
||||
renderElements = {
|
||||
grid: new Group({ applyMatrix: true }),
|
||||
};
|
||||
|
||||
constructor(ctx: Context) {
|
||||
this.ctx = ctx;
|
||||
super(ctx);
|
||||
|
||||
this.container.addChild(this.renderElements.grid);
|
||||
|
||||
for (let x = 0; x <= this.ctx.grid.width; x++) {
|
||||
const lineLength = this.ctx.grid.height * TILE_SIZE;
|
||||
for (let x = 0; x <= this.ctx.config.grid.width; x++) {
|
||||
const lineLength = this.ctx.config.grid.height * TILE_SIZE;
|
||||
const start = x * TILE_SIZE - lineLength * 0.5;
|
||||
const line = new Path({
|
||||
segments: [
|
||||
@@ -31,8 +30,8 @@ export class Grid implements SceneElement {
|
||||
this.renderElements.grid.addChild(line);
|
||||
}
|
||||
|
||||
for (let y = 0; y <= this.ctx.grid.height; y++) {
|
||||
const lineLength = this.ctx.grid.width * TILE_SIZE;
|
||||
for (let y = 0; y <= this.ctx.config.grid.height; y++) {
|
||||
const lineLength = this.ctx.config.grid.width * TILE_SIZE;
|
||||
const start = y * TILE_SIZE - lineLength * 0.5;
|
||||
const line = new Path({
|
||||
segments: [
|
||||
|
||||
91
src/renderer/elements/Node.ts
Normal file
91
src/renderer/elements/Node.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import { Group, Raster } from "paper";
|
||||
import { Coords, Context } from "../types";
|
||||
import { IconI } from "../../validation/SceneSchema";
|
||||
import { PROJECTED_TILE_WIDTH, PIXEL_UNIT } from "../constants";
|
||||
|
||||
const NODE_IMG_PADDING = 0 * PIXEL_UNIT;
|
||||
|
||||
export interface NodeOptions {
|
||||
id: string;
|
||||
position: Coords;
|
||||
icon: IconI;
|
||||
}
|
||||
|
||||
interface Callbacks {
|
||||
onMove: (x: number, y: number, node: Node) => void;
|
||||
}
|
||||
|
||||
export class Node {
|
||||
ctx: Context;
|
||||
container = new Group();
|
||||
|
||||
id;
|
||||
callbacks: Callbacks;
|
||||
position;
|
||||
icon;
|
||||
renderElements = {
|
||||
iconContainer: new Group(),
|
||||
icon: new Raster(),
|
||||
};
|
||||
|
||||
constructor(ctx: Context, options: NodeOptions, callbacks: Callbacks) {
|
||||
makeAutoObservable(this);
|
||||
|
||||
this.ctx = ctx;
|
||||
this.id = options.id;
|
||||
this.position = options.position;
|
||||
this.icon = options.icon;
|
||||
this.callbacks = callbacks;
|
||||
|
||||
this.renderElements.iconContainer.addChild(this.renderElements.icon);
|
||||
this.container.addChild(this.renderElements.iconContainer);
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.updateIcon(this.icon);
|
||||
this.moveTo(this.position.x, this.position.y);
|
||||
}
|
||||
|
||||
async updateIcon(icon: IconI) {
|
||||
this.icon = icon;
|
||||
const { iconContainer, icon: iconEl } = this.renderElements;
|
||||
|
||||
await new Promise((resolve) => {
|
||||
iconEl.onLoad = () => {
|
||||
iconEl.scale(
|
||||
(PROJECTED_TILE_WIDTH - NODE_IMG_PADDING) / iconEl.bounds.width
|
||||
);
|
||||
|
||||
iconContainer.pivot = iconEl.bounds.bottomCenter;
|
||||
iconContainer.position.set(0, 0);
|
||||
|
||||
resolve(null);
|
||||
};
|
||||
|
||||
iconEl.source = this.icon.url;
|
||||
});
|
||||
}
|
||||
|
||||
moveTo(x: number, y: number) {
|
||||
this.callbacks.onMove(x, y, this);
|
||||
}
|
||||
|
||||
export() {
|
||||
return {
|
||||
id: this.id,
|
||||
position: this.position,
|
||||
icon: this.icon.id,
|
||||
};
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.container.removeChildren();
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.container.remove();
|
||||
}
|
||||
}
|
||||
66
src/renderer/elements/Nodes.ts
Normal file
66
src/renderer/elements/Nodes.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { Group } from "paper";
|
||||
import autobind from "auto-bind";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
import { Context } from "../types";
|
||||
import { Node, NodeOptions } from "./Node";
|
||||
import cuid from "cuid";
|
||||
import { SceneElement } from "../SceneElement";
|
||||
import { SceneEvent } from "../SceneEvent";
|
||||
|
||||
export class Nodes {
|
||||
ctx: Context;
|
||||
container = new Group();
|
||||
nodes: Node[] = [];
|
||||
|
||||
constructor(ctx: Context) {
|
||||
makeAutoObservable(this);
|
||||
autobind(this);
|
||||
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
getNodeById(id: string) {
|
||||
return this.nodes.find((node) => node.id === id);
|
||||
}
|
||||
|
||||
addNode(options: NodeOptions, sceneEvent?: SceneEvent) {
|
||||
const node = new Node(
|
||||
this.ctx,
|
||||
{
|
||||
...options,
|
||||
id: options.id ?? cuid(),
|
||||
},
|
||||
{
|
||||
onMove: this.onMove.bind(this),
|
||||
}
|
||||
);
|
||||
|
||||
this.nodes.push(node);
|
||||
this.container.addChild(node.container);
|
||||
|
||||
this.ctx
|
||||
.createSceneEvent(
|
||||
{
|
||||
type: "NODE_CREATED",
|
||||
node,
|
||||
},
|
||||
sceneEvent
|
||||
)
|
||||
.complete();
|
||||
}
|
||||
|
||||
onMove(x: number, y: number, node: Node) {
|
||||
const tile = this.ctx.getTileBounds(x, y);
|
||||
node.container.position.set(tile.bottom);
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.nodes.forEach((node) => node.destroy());
|
||||
this.nodes = [];
|
||||
}
|
||||
|
||||
export() {
|
||||
const exported = this.nodes.map((node) => node.export());
|
||||
return exported;
|
||||
}
|
||||
}
|
||||
15
src/renderer/elements/Positioner.ts
Normal file
15
src/renderer/elements/Positioner.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Shape, Group } from "paper";
|
||||
|
||||
export class Positioner {
|
||||
container = new Group();
|
||||
|
||||
constructor(color: string = "red") {
|
||||
this.container.addChild(
|
||||
new Shape.Circle({
|
||||
center: [0, 0],
|
||||
radius: 10,
|
||||
fillColor: color,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,27 @@
|
||||
import type { Renderer } from "./Renderer";
|
||||
import { SceneI } from "../validation/SceneSchema";
|
||||
import type { Node } from "./elements/Node";
|
||||
|
||||
export interface Coords {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Context {
|
||||
grid: {
|
||||
width: number;
|
||||
height: number;
|
||||
};
|
||||
}
|
||||
export type NodeEventI =
|
||||
| {
|
||||
type: "SCENE_LOAD";
|
||||
}
|
||||
| {
|
||||
type: "NODE_CREATED";
|
||||
node: Node;
|
||||
}
|
||||
| {
|
||||
type: "NODE_REMOVED";
|
||||
node: Node;
|
||||
};
|
||||
|
||||
export interface SceneElement {
|
||||
container: paper.Group;
|
||||
ctx: Context;
|
||||
renderElements?: {
|
||||
[k: string]: paper.Item;
|
||||
};
|
||||
}
|
||||
export type SceneEventI = NodeEventI;
|
||||
|
||||
export type Context = Renderer;
|
||||
|
||||
export type OnSceneChange = (event: SceneEventI, scene: SceneI) => void;
|
||||
|
||||
10
src/types.ts
10
src/types.ts
@@ -1,10 +0,0 @@
|
||||
export interface Icon {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
export interface IsoflowProps {
|
||||
icons: Icon[];
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
export const clamp = (num: number, min: number, max: number) => {
|
||||
return num <= min ? min : num >= max ? max : num;
|
||||
};
|
||||
|
||||
export const getRandom = (min: number, max: number) => {
|
||||
return Math.floor(Math.random() * (max - min) + min);
|
||||
};
|
||||
|
||||
@@ -11,6 +11,10 @@ export const NodeSchema = z.object({
|
||||
id: z.string(),
|
||||
label: z.string().optional(),
|
||||
icon: z.string(),
|
||||
position: z.object({
|
||||
x: z.number(),
|
||||
y: z.number(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const ConnectorSchema = z.object({
|
||||
|
||||
Reference in New Issue
Block a user