From cf3f5b12241ed79943d22a5ff7dd30930bfc3af0 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Sun, 22 Jan 2023 22:03:56 +0100 Subject: [PATCH 01/44] use shapes and pngs together --- .vscode/launch.json | 15 -- admin-client/src/components/Tile.tsx | 6 +- backend/src/Controller/socketController.ts | 4 + backend/src/Controller/tileController.ts | 21 ++- backend/src/Model/tileModel.ts | 22 ++- backend/types/socket.types.d.ts | 10 +- frontend/src/components/Board/Board.tsx | 10 +- frontend/src/components/Forms/AddTileForm.tsx | 128 ------------------ frontend/src/components/Sidebar/Category.tsx | 7 +- frontend/src/components/Tiles/MenuTile.tsx | 9 +- frontend/src/components/Tiles/Tile.tsx | 29 ++-- frontend/src/hooks/useMouse.tsx | 80 +++++++---- frontend/src/json/kacheln.json | 22 +-- frontend/src/pages/CanvasPage.tsx | 11 -- frontend/src/state/BoardState.tsx | 19 ++- frontend/src/types.d.ts | 38 ++---- 16 files changed, 174 insertions(+), 257 deletions(-) delete mode 100644 .vscode/launch.json delete mode 100644 frontend/src/components/Forms/AddTileForm.tsx diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 0e16d72..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "chrome", - "request": "launch", - "name": "Launch Chrome against localhost", - "url": "http://localhost:3000", - "webRoot": "${workspaceFolder}" - } - ] -} diff --git a/admin-client/src/components/Tile.tsx b/admin-client/src/components/Tile.tsx index 1f57d56..9735d46 100644 --- a/admin-client/src/components/Tile.tsx +++ b/admin-client/src/components/Tile.tsx @@ -3,15 +3,15 @@ import React from 'react'; type TileProps = { category: string; name: string; - url: string; + src: string; onClickFunc: () => void; }; -const Tile: React.FC = ({ category, name, url, onClickFunc }) => { +const Tile: React.FC = ({ category, name, src, onClickFunc }) => { return (
- {name} + {name}
{name}
diff --git a/backend/src/Controller/socketController.ts b/backend/src/Controller/socketController.ts index bd5eb6d..e0cb569 100644 --- a/backend/src/Controller/socketController.ts +++ b/backend/src/Controller/socketController.ts @@ -73,10 +73,14 @@ export const tileDrop = ( room.tiles.push({ tile: { id: data.tile.id, + name: data.tile.name, category: data.tile.category, src: data.tile.src, x: data.tile.x, y: data.tile.y, + points: data.tile.points, + color: data.tile.color, + textPosition: data.tile.textPosition, }, }); io.to(data.roomId).emit("room-data", room); diff --git a/backend/src/Controller/tileController.ts b/backend/src/Controller/tileController.ts index 6beffd4..a63a407 100644 --- a/backend/src/Controller/tileController.ts +++ b/backend/src/Controller/tileController.ts @@ -35,7 +35,13 @@ export const createTile = async (req: Request, res: Response) => { const tile = new Tile({ category: formData.category, name: formData.name, - url: filePath, + src: filePath, + points: formData.points, + color: formData.color, + textPosition: { + x: formData.textPositionX, + y: formData.textPositionY, + }, }); const newTile = await tile.save(); res.status(200).json(newTile); @@ -82,8 +88,13 @@ export const updateTile = async (req: Request, res: Response) => { } tile.category = req.body.category ? req.body.category : tile.category; tile.name = req.body.name ? req.body.name : tile.name; - tile.url = - req.file != undefined ? `${backendUrl}/${req.file.path}` : tile.url; + tile.src = + req.file != undefined ? `${backendUrl}/${req.file.path}` : tile.src; + tile.points = req.body.points ? req.body.points : tile.points; + tile.color = req.body.color ? req.body.color : tile.color; + tile.textPosition = req.body.textPosition + ? req.body.textPosition + : tile.textPosition; const updatedTile = await tile.save(); res.status(200).json(updatedTile); } else { @@ -104,8 +115,8 @@ export const deleteTile = async (req: Request, res: Response) => { try { const tile = await Tile.findByIdAndDelete(req.params.id); if (tile) { - const url = tile.url.split("/"); - const fileName = url[url.length - 1]; + const src = tile.src.split("/"); + const fileName = src[src.length - 1]; fs.unlink(`./uploads/${fileName}`, (err) => { if (err) { console.log(err); diff --git a/backend/src/Model/tileModel.ts b/backend/src/Model/tileModel.ts index 3dd1ebd..a5c3057 100644 --- a/backend/src/Model/tileModel.ts +++ b/backend/src/Model/tileModel.ts @@ -1,5 +1,14 @@ import { Schema, model } from "mongoose"; - +const textPositionSchema = new Schema({ + x: { + type: Number, + required: true, + }, + y: { + type: Number, + required: true, + }, +}); //Schema for the Tile model const TileSchema = new Schema({ @@ -11,10 +20,19 @@ const TileSchema = new Schema({ type: String, required: true, }, - url: { + src: { + type: String, + required: true, + }, + points: { + type: [Number], + required: true, + }, + color: { type: String, required: true, }, + textPosition: textPositionSchema, }); export const Tile = model("Tile", TileSchema); diff --git a/backend/types/socket.types.d.ts b/backend/types/socket.types.d.ts index 2a84554..40360aa 100644 --- a/backend/types/socket.types.d.ts +++ b/backend/types/socket.types.d.ts @@ -15,12 +15,16 @@ export type SocketDeleteData = { id: string; }; -export type NewNode = { +export type NewTile = { id: string; category: string; src: string; x: number; y: number; + name: string; + points: number[]; + color: string; + textPosition: { x: number; y: number }; }; export type TabFocusData = { @@ -31,7 +35,7 @@ export type TabFocusData = { export type SocketDragTile = { remoteUser: string; - tile: NewNode; + tile: NewTile; roomId: string; remoteUserColor: string; }; @@ -45,7 +49,7 @@ export type SocketCursorData = { //state object for each room export type TileData = { - tile: NewNode; + tile: NewTile; }; export type RoomData = { diff --git a/frontend/src/components/Board/Board.tsx b/frontend/src/components/Board/Board.tsx index 0ff56ad..8089df8 100644 --- a/frontend/src/components/Board/Board.tsx +++ b/frontend/src/components/Board/Board.tsx @@ -69,12 +69,16 @@ const Board = () => { {gridComponents} {room?.tiles?.map((tileObject) => ( ))} diff --git a/frontend/src/components/Forms/AddTileForm.tsx b/frontend/src/components/Forms/AddTileForm.tsx deleted file mode 100644 index 1e4be77..0000000 --- a/frontend/src/components/Forms/AddTileForm.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { InnerObject } from '../../types'; -import { useToggle } from '../../hooks/useToggle'; -import React, { useEffect, useState } from 'react'; -import { useBoardState } from '../../state/BoardState'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faAngleDown, faAngleUp, faX } from '@fortawesome/free-solid-svg-icons'; -import { getTileType } from '../../hooks/useCategory'; - -type AddTileFormProps = { - closeForm: () => void; -}; - -const AddTileForm: React.FC = ({ closeForm }) => { - const { toggleForm } = useToggle(); - const [isOpen, setIsOpen] = useState(false); - const allTiles = useBoardState((state) => state.allTiles); - const setAllTiles = useBoardState((state) => state.setAllTiles); - const [categories, setCategories] = useState>([]); - const [selectedCategory, setSelectedCategory] = useState(allTiles[0].category); - const [selectedName, setSelectedName] = useState(); - - const setCategory = (category: string) => { - setSelectedCategory(category); - setIsOpen(false); - }; - - // TODO: Add a database to save the new Tiles to. - const handleSubmit = () => { - if (selectedName && selectedCategory) { - const svgData = getTileType(selectedCategory); - const newNode: InnerObject = { - category: selectedCategory, - name: selectedName, - svgPath: svgData.svgPath, - fill: svgData.fill, - svgRotate: svgData.rotation, - url: '', - }; - setAllTiles([...allTiles, newNode]); - } - closeForm(); - setIsOpen(false); - }; - - const handleChange = (event: React.FormEvent) => { - setSelectedName(event.currentTarget.value); - }; - - useEffect(() => { - const categoryArray: Array = []; - const categorySet: Set = new Set(); - allTiles.forEach((item) => { - categorySet.add(item.category); - }); - categorySet.forEach((item) => categoryArray.push(item)); - setCategories(categoryArray); - }, []); - - return ( -
-
-
- toggleForm()} - className='mb-2 relative text-green-800' - /> -
-
- - -
-
- - - {isOpen && ( - - )} -
-
- -
-
-
- ); -}; - -export default AddTileForm; diff --git a/frontend/src/components/Sidebar/Category.tsx b/frontend/src/components/Sidebar/Category.tsx index 038c617..6a37d59 100644 --- a/frontend/src/components/Sidebar/Category.tsx +++ b/frontend/src/components/Sidebar/Category.tsx @@ -1,5 +1,5 @@ import MenuTile from '../Tiles/MenuTile'; -import { InnerObject } from '../../types'; +import { Tile } from '../../types'; import { useMouse } from '../../hooks/useMouse'; import React, { useEffect, useState } from 'react'; import { useBoardState } from '../../state/BoardState'; @@ -8,14 +8,13 @@ type CategoryProps = { category: string; }; - const Category: React.FC = ({ category }) => { - const [stateItems, setStateItems] = useState([]); + const [stateItems, setStateItems] = useState([]); const allTiles = useBoardState((state) => state.allTiles); const { handleDragStart } = useMouse(); useEffect(() => { - const InnerObjectArray: InnerObject[] = []; + const InnerObjectArray: Tile[] = []; allTiles.forEach((object) => { object.category === category && InnerObjectArray.push(object); }); diff --git a/frontend/src/components/Tiles/MenuTile.tsx b/frontend/src/components/Tiles/MenuTile.tsx index 8701828..aa5befa 100644 --- a/frontend/src/components/Tiles/MenuTile.tsx +++ b/frontend/src/components/Tiles/MenuTile.tsx @@ -3,19 +3,16 @@ import React from 'react'; type MenuProps = { name: string; category: string; - svgPath: string; - fill: string; - svgRotate: number; - url: string; + src: string; dragFunction: (event: React.DragEvent) => void; }; -const MenuTile: React.FC = ({ url, name, category, dragFunction }) => { +const MenuTile: React.FC = ({ src, name, category, dragFunction }) => { return ( = ({ src, x, y, category, id }) => { +const Tile: React.FC = ({ + x, + y, + id, + src, + name, + color, + points, + category, + textPosition, +}) => { const tileRef = React.useRef(null); const userColor = useWebSocketState( (state) => state.room?.users?.find((user) => user.userId === state.socket?.id)?.color, ); const { handleMouseEnL, updateTilePosition, setActiveDragElement } = useMouse(); const { handleContextMenu } = useContextMenu(); - const [image] = useImage(src); - const remoteDragColor = useBoardState((state) => state.remoteDragColor); + // const [image] = useImage(src); + // const remoteDragColor = useBoardState((state) => state.remoteDragColor); // add border to image if active @@ -37,13 +47,16 @@ const Tile: React.FC = ({ src, x, y, category, id }) => { onMouseOver={(e) => handleMouseEnL(e, 4)} onMouseLeave={(e) => handleMouseEnL(e, 0)} > - + /> */} + + + )} diff --git a/frontend/src/hooks/useMouse.tsx b/frontend/src/hooks/useMouse.tsx index 49fcbc3..2bf8a5c 100644 --- a/frontend/src/hooks/useMouse.tsx +++ b/frontend/src/hooks/useMouse.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { NewNode, SocketDragTile } from '../types'; +import { SocketDragTile, Tile } from '../types'; import { v4 as uuidv4 } from 'uuid'; import { Group } from 'konva/lib/Group'; import { KonvaEventObject } from 'konva/lib/Node'; @@ -7,6 +7,7 @@ import { useBoardState } from '../state/BoardState'; import { useWebSocketState } from '../state/WebSocketState'; export const useMouse = () => { + const allTiles = useBoardState((state) => state.allTiles); const setTiles = useBoardState((state) => state.addTile); const updateTile = useBoardState((state) => state.updateTile); const stageRef = useBoardState((state) => state.stageReference); @@ -51,15 +52,19 @@ export const useMouse = () => { setActiveDragTile(activeTileReference); if (stageRef.current) { - const { 'data-src': url } = event.target.attrs; + const { 'data-src': src } = event.target.attrs; const stage = stageRef.current; const pos = stage.getRelativePointerPosition(); - const updatedTile: NewNode = { - id: event.target.attrs.id, - category: event.target.attrs.name, + const updatedTile: Tile = { + src: src, x: event.target.x(), y: event.target.y(), - src: url, + id: event.target.attrs.id, + name: event.target.attrs.name, + color: event.target.attrs.color, + points: event.target.attrs.points, + category: event.target.attrs.name, + textPosition: event.target.attrs.textPosition, }; if (socket !== null && roomId && userColor) { const socketDragTile: SocketDragTile = { @@ -90,15 +95,24 @@ export const useMouse = () => { const handleDragStart = (event: React.DragEvent) => { // use HTML DnD API to send Tile Information - const dragPayload = JSON.stringify({ - nodeClass: event.currentTarget.getAttribute('data-class'), - offsetX: event.nativeEvent.offsetX, - offsetY: event.nativeEvent.offsetY, - clientWidth: event.currentTarget.clientWidth, - clientHeight: event.currentTarget.clientHeight, - url: event.currentTarget.getAttribute('src'), - }); - event.dataTransfer.setData('dragStart/Tile', dragPayload); + const tile = allTiles.find( + (tile) => tile.name === event.currentTarget.getAttribute('data-name'), + ); + if (tile) { + const dragPayload = JSON.stringify({ + nodeClass: event.currentTarget.getAttribute('data-class'), + offsetX: event.nativeEvent.offsetX, + offsetY: event.nativeEvent.offsetY, + clientWidth: event.currentTarget.clientWidth, + clientHeight: event.currentTarget.clientHeight, + src: tile.src, + name: tile.name, + color: tile.color, + points: tile.points, + textPosition: tile.textPosition, + }); + event.dataTransfer.setData('dragStart/Tile', dragPayload); + } }; const updateTilePosition = (event: KonvaEventObject) => { @@ -106,14 +120,18 @@ export const useMouse = () => { * updates the Tile position in the state * also clears the active Drag Element */ - const { 'data-src': url } = event.target.attrs; + const { 'data-src': src } = event.target.attrs; if (stageRef.current) { - const updatedTile: NewNode = { - id: event.target.attrs.id, - category: event.target.attrs.name, + const updatedTile: Tile = { + src: src, x: event.target.x(), y: event.target.y(), - src: url, + id: event.target.attrs.id, + name: event.target.attrs.name, + color: event.target.attrs.fill, + category: event.target.attrs.name, + points: event.target.attrs.points, + textPosition: event.target.attrs.textPosition, }; updateTile(updatedTile); } @@ -126,15 +144,29 @@ export const useMouse = () => { if (draggedData && stageRef.current != null) { stageRef.current.setPointersPositions(event); const { x, y } = stageRef.current.getRelativePointerPosition(); - const { url, nodeClass, offsetX, offsetY, clientHeight, clientWidth } = - JSON.parse(draggedData); + const { + src, + color, + textPosition, + name, + points, + nodeClass, + offsetX, + offsetY, + clientHeight, + clientWidth, + } = JSON.parse(draggedData); if (x && y) { - const newTile: NewNode = { + const newTile: Tile = { id: uuidv4(), - src: url, + src: src, category: nodeClass, x: x - (offsetX - clientWidth / 2), y: y - (offsetY - clientHeight / 2), + color: color, + name: name, + points: points, + textPosition: textPosition, }; setTiles(newTile); if (socket !== null && roomId && userColor) { diff --git a/frontend/src/json/kacheln.json b/frontend/src/json/kacheln.json index 3a58575..56d7cb6 100644 --- a/frontend/src/json/kacheln.json +++ b/frontend/src/json/kacheln.json @@ -5,7 +5,7 @@ "svgPath": "M1 186L89 237V117L194 57L97 1L1 186Z", "svgRotate": 45, "fill": "#f9b43d", - "url": "https://i.ibb.co/8M0jgv1/When.png" + "src": "https://i.ibb.co/8M0jgv1/When.png" }, { "category": "End", @@ -13,7 +13,7 @@ "svgPath": "M193.5 1H2L105 66.5L2 131.5H193.5V1Z", "svgRotate": 0, "fill": "#f9b43d", - "url": "https://i.ibb.co/v47Vmqq/end.png" + "src": "https://i.ibb.co/v47Vmqq/end.png" }, { "category": "Objects", @@ -21,7 +21,7 @@ "svgPath": "M104 0L207.923 60V180L104 240L0.0769501 180V60L104 0Z", "svgRotate": 0, "fill": "#eb555b", - "url": "https://i.ibb.co/JrmGxY9/Lamp.png" + "src": "https://i.ibb.co/JrmGxY9/Lamp.png" }, { "category": "Objects", @@ -29,7 +29,7 @@ "svgPath": "M104 0L207.923 60V180L104 240L0.0769501 180V60L104 0Z", "svgRotate": 0, "fill": "#eb555b", - "url": "https://i.ibb.co/JrmGxY9/Lamp.png" + "src": "https://i.ibb.co/JrmGxY9/Lamp.png" }, { "category": "Objects", @@ -37,7 +37,7 @@ "svgPath": "M104 0L207.923 60V180L104 240L0.0769501 180V60L104 0Z", "svgRotate": 0, "fill": "#eb555b", - "url": "https://i.ibb.co/JrmGxY9/Lamp.png" + "src": "https://i.ibb.co/JrmGxY9/Lamp.png" }, { "category": "Actions", @@ -45,7 +45,7 @@ "svgPath": "M103 0L206 66.5L103 131L0 66.5L103 0Z", "svgRotate": 0, "fill": "#f4aece", - "url": "https://i.ibb.co/0996SHK/an.png" + "src": "https://i.ibb.co/0996SHK/an.png" }, { "category": "Actions", @@ -53,7 +53,7 @@ "svgPath": "M103 0L206 66.5L103 131L0 66.5L103 0Z", "svgRotate": 0, "fill": "#f4aece", - "url": "https://i.ibb.co/8MjkqL5/aus.png" + "src": "https://i.ibb.co/8MjkqL5/aus.png" }, { "category": "Conditions", @@ -61,7 +61,7 @@ "svgPath": "M105 0L208 60V149L105 85L0 149V60L105 0Z", "svgRotate": 0, "fill": "#bababa", - "url": "https://i.ibb.co/gVgx6Cf/solange.png" + "src": "https://i.ibb.co/gVgx6Cf/solange.png" }, { "category": "Conditions", @@ -69,7 +69,7 @@ "svgPath": "M105 0L208 60V149L105 85L0 149V60L105 0Z", "svgRotate": 0, "fill": "#bababa", - "url": "https://i.ibb.co/hLMHLr9/sonst.png" + "src": "https://i.ibb.co/hLMHLr9/sonst.png" }, { "category": "Negation", @@ -77,7 +77,7 @@ "svgPath": "M105 0L208 60V149L105 85L0 149V60L105 0Z", "svgRotate": 0, "fill": "#eb555b", - "url": "https://i.ibb.co/6Xd9LyJ/Not.png" + "src": "https://i.ibb.co/6Xd9LyJ/Not.png" }, { "category": "Union", @@ -85,6 +85,6 @@ "svgPath": "M0 0L102.5 66.5V181L0 246.5V0Z", "svgRotate": 0, "fill": "#bababa", - "url": "https://i.ibb.co/4j5Mznp/Und.png" + "src": "https://i.ibb.co/4j5Mznp/Und.png" } ] diff --git a/frontend/src/pages/CanvasPage.tsx b/frontend/src/pages/CanvasPage.tsx index d0d7103..3cdf012 100644 --- a/frontend/src/pages/CanvasPage.tsx +++ b/frontend/src/pages/CanvasPage.tsx @@ -1,30 +1,19 @@ import React from 'react'; import Board from '../components/Board/Board'; -import { useToggle } from '../hooks/useToggle'; import Sidebar from '../components/Sidebar/Sidebar'; -import AddTileForm from '../components/Forms/AddTileForm'; -import Cursor from '../components/Cursor/Cursor'; -import { useWebSocketState } from '../state/WebSocketState'; import RightClickMenu from '../components/ContextMenus/RightClickMenu'; import { useContextMenuState } from '../state/ContextMenuState'; import InfoComponent from '../components/Forms/InfoComponent'; import { useWindowFocus } from '../hooks/useWindowFocus'; const CanvasPage = () => { - const { isOpen, toggleForm } = useToggle(); // Add Cursor here. const socket = useWebSocketState((state) => state.socket); const contextMenuOpen = useContextMenuState((state) => state.contextMenuOpen); - const room = useWebSocketState((state) => state.room); useWindowFocus(); return ( <> {contextMenuOpen === true && } - {isOpen && ( -
- toggleForm()} /> -
- )} diff --git a/frontend/src/state/BoardState.tsx b/frontend/src/state/BoardState.tsx index 67e74aa..f08ef9b 100644 --- a/frontend/src/state/BoardState.tsx +++ b/frontend/src/state/BoardState.tsx @@ -3,24 +3,24 @@ import create from 'zustand'; import { Stage } from 'konva/lib/Stage'; import { Group } from 'konva/lib/Group'; import React, { createRef } from 'react'; -import { NewNode, InnerObject } from '../types'; +import { Tile } from '../types'; import { mountStoreDevtool } from 'simple-zustand-devtools'; export type BoardContextType = { modalOpen: boolean; categoriesOpen: boolean; - allTiles: InnerObject[]; - tilesOnBoard: NewNode[]; + allTiles: Tile[]; + tilesOnBoard: Tile[]; remoteDragColor: string | null; activeDragTile: React.RefObject | null; stageReference: React.RefObject; clearActiveDragTile: () => void; - addTile: (newNode: NewNode) => void; + addTile: (newTile: Tile) => void; toggleModal: (toggle: boolean) => void; - updateTile: (updatedNode: NewNode) => void; + updateTile: (updatedNode: Tile) => void; removeTile: (nodeToRemove: string) => void; setCategoriesOpen: (toggle: boolean) => void; - setAllTiles: (tilesArray: InnerObject[]) => void; + setAllTiles: (tilesArray: Tile[]) => void; setRemoteDragColor: (color: string | null) => void; setActiveDragTile: (newActiveTile: React.RefObject) => void; setStageReference: (stage: React.RefObject) => void; @@ -40,10 +40,9 @@ export const useBoardState = create((set) => ({ set(() => ({ activeDragTile: newActiveTile })), setCategoriesOpen: (isOpen: boolean) => set(() => ({ categoriesOpen: isOpen })), toggleModal: (toggle: boolean) => set(() => ({ modalOpen: toggle })), - setAllTiles: (tilesArray: InnerObject[]) => set(() => ({ allTiles: tilesArray })), - addTile: (newTile: NewNode) => - set((state) => ({ tilesOnBoard: [...state.tilesOnBoard, newTile] })), - updateTile: (updatedTile: NewNode) => + setAllTiles: (tilesArray: Tile[]) => set(() => ({ allTiles: tilesArray })), + addTile: (newTile: Tile) => set((state) => ({ tilesOnBoard: [...state.tilesOnBoard, newTile] })), + updateTile: (updatedTile: Tile) => set((state) => ({ tilesOnBoard: state.tilesOnBoard.map((tile) => tile.id === updatedTile.id ? updatedTile : tile, diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index a28c546..4b0bc9b 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -12,12 +12,13 @@ export type UserData = { }; -export type NewNode = { - id: string; +export type NewTile = { category: string; - x: number; - y: number; src: string; + name: string; + points: number[]; + color: string; + textPosition: { x: number; y: number }; }; export type CursorData = { @@ -27,20 +28,15 @@ export type CursorData = { }; export type Tile = { - src: string; - id: string; x: number; y: number; - category: string; -}; - -export type MenuTileProps = { + id: string; + src: string; name: string; + color: string; + points: number[]; category: string; - svgPath: string; - fill: string; - svgRotate: number; - url: string; + textPosition: { x: number; y: number }; }; export type InnerObject = { @@ -49,28 +45,22 @@ export type InnerObject = { svgPath: string; fill: string; svgRotate: number; - url: string; + src: string; }; export type SocketDragTile = { remoteUser: string; - tile: NewNode; + tile: Tile; roomId: string; remoteUserColor: string; }; export type TileData = { - tile: { - id: string; - category: string; - src: string; - x: number; - y: number; - }; + tile: Tile; }; export type RoomData = { roomId: string; users: UserData[]; tiles?: TileData[]; -}; +}; \ No newline at end of file From 069c2bea2189892cccba0669a9074b4b13ada9e1 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Sun, 22 Jan 2023 23:55:45 +0100 Subject: [PATCH 02/44] updated points --- frontend/src/hooks/useCategory.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/hooks/useCategory.tsx b/frontend/src/hooks/useCategory.tsx index 76522a0..53e582f 100644 --- a/frontend/src/hooks/useCategory.tsx +++ b/frontend/src/hooks/useCategory.tsx @@ -12,7 +12,7 @@ export const getTileType = (categoryName: string): TileTypeProps => { switch (categoryName) { case 'Start': return { - points: [0, -100, 100, -50, 0, 0, 0, 100, -90, 50], + points: [0, -200, 200, -200, 100, -50, 200, 100, 0, 100], textPosition: { x: -60, y: 30 }, fill: '#f9b43d', rotation: -60, @@ -20,7 +20,7 @@ export const getTileType = (categoryName: string): TileTypeProps => { }; case 'End': return { - points: [-200, -50, 0, -50, 0, 50, -200, 50, -100, 0], + points: [-200, -250, 0, -250, 0, 50, -200, 50, -100, -100], textPosition: { x: -125, y: -5 }, fill: '#f9b43d', rotation: 0, @@ -29,7 +29,7 @@ export const getTileType = (categoryName: string): TileTypeProps => { case 'Objects': return { // points for a top pointed hexagon - points: [0, -100, 100, -50, 100, 50, 0, 100, -100, 50, -100, -50], + points: [0, -100, 200, -100, 300, 50, 200, 200, 0, 200, -100, 50], textPosition: { x: -60, y: 0 }, fill: '#eb555b', rotation: 0, @@ -37,7 +37,7 @@ export const getTileType = (categoryName: string): TileTypeProps => { }; case 'Actions': return { - points: [0, -50, -100, 0, 0, 50, 100, 0], + points: [-200, -250, 0, -250, 100, -100, 0, 50, -200, 50, -100, -100], textPosition: { x: -60, y: -5 }, fill: '#f4aece', rotation: 0, @@ -61,7 +61,7 @@ export const getTileType = (categoryName: string): TileTypeProps => { }; case 'Union': return { - points: [-50, -100, -50, 100, 50, 50, 50, -50], + points: [-100, -50, 0, -200, 100, -200, 200, -50, 200, 50, 100, 200, 0, 200, -100, 50], textPosition: { x: -55, y: 0 }, fill: '#bababa', rotation: 0, From db85cc01e7d7a0d54caa7ed7bd7ee03d28c9a6a1 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Tue, 24 Jan 2023 20:54:43 +0100 Subject: [PATCH 03/44] use single DEfinition of TIle on everywhere --- frontend/src/components/Board/Board.tsx | 12 ++++++++ frontend/src/components/Tiles/Tile.tsx | 20 ++++--------- frontend/src/components/Tiles/TileBorder.tsx | 30 ++++++++++++++++++++ frontend/src/hooks/useMouse.tsx | 30 ++++++++++++++++---- frontend/src/state/BoardState.tsx | 10 +++++-- frontend/src/types.d.ts | 1 + 6 files changed, 79 insertions(+), 24 deletions(-) create mode 100644 frontend/src/components/Tiles/TileBorder.tsx diff --git a/frontend/src/components/Board/Board.tsx b/frontend/src/components/Board/Board.tsx index 8089df8..44dba6c 100644 --- a/frontend/src/components/Board/Board.tsx +++ b/frontend/src/components/Board/Board.tsx @@ -1,6 +1,7 @@ import Tile from '../Tiles/Tile'; import React, { useEffect } from 'react'; import { Stage, Layer } from 'react-konva'; +import TileBorder from '../Tiles/TileBorder'; import { useGrid } from '../../hooks/useGrid'; import { useMouse } from '../../hooks/useMouse'; import { Stage as StageType } from 'konva/lib/Stage'; @@ -27,6 +28,7 @@ const Board = () => { const setRoom = useWebSocketState((state) => state.setRoom); const room = useWebSocketState((state) => state.room); const setRemoteDragColor = useBoardState((state) => state.setRemoteDragColor); + const selectedTile = useBoardState((state) => state.selectedTile); setStageReference(stageRef); useEffect(() => { @@ -67,6 +69,16 @@ const Board = () => { > {gridComponents} + {selectedTile != null && ( + + )} {room?.tiles?.map((tileObject) => ( = ({ - x, - y, - id, - src, - name, - color, - points, - category, - textPosition, -}) => { +const Tile: React.FC = ({ x, y, id, src, name, color, points, category, textPosition }) => { const tileRef = React.useRef(null); const userColor = useWebSocketState( (state) => state.room?.users?.find((user) => user.userId === state.socket?.id)?.color, ); - const { handleMouseEnL, updateTilePosition, setActiveDragElement } = useMouse(); + const { setClickedTile, handleMouseEnL, updateTilePosition, setActiveDragElement } = useMouse(); const { handleContextMenu } = useContextMenu(); // const [image] = useImage(src); // const remoteDragColor = useBoardState((state) => state.remoteDragColor); // add border to image if active - return ( <> {userColor && ( @@ -42,6 +31,7 @@ const Tile: React.FC = ({ y={y} id={id} name={category} + onClick={(e) => setClickedTile(e)} onDragMove={(event) => setActiveDragElement(tileRef, event)} onDragEnd={updateTilePosition} onMouseOver={(e) => handleMouseEnL(e, 4)} @@ -55,12 +45,12 @@ const Tile: React.FC = ({ strokeWidth={4} /> */} - + )} ); -}; +};; export default Tile; diff --git a/frontend/src/components/Tiles/TileBorder.tsx b/frontend/src/components/Tiles/TileBorder.tsx new file mode 100644 index 0000000..47c3c6f --- /dev/null +++ b/frontend/src/components/Tiles/TileBorder.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Line } from 'react-konva'; + +type Props = { + tilePosition: { + x: number; + y: number; + }; + points?: number[]; + id: string; +}; + +const TileBorder: React.FC = ({ tilePosition, id, points }) => { + const SIZE = 50; + const defaultPoints = [0, 0, SIZE, 0, SIZE, SIZE, 0, SIZE, 0, 0]; + const { x, y } = tilePosition; + return ( + + ); +}; + +export default TileBorder; diff --git a/frontend/src/hooks/useMouse.tsx b/frontend/src/hooks/useMouse.tsx index 2bf8a5c..83397f6 100644 --- a/frontend/src/hooks/useMouse.tsx +++ b/frontend/src/hooks/useMouse.tsx @@ -8,6 +8,7 @@ import { useWebSocketState } from '../state/WebSocketState'; export const useMouse = () => { const allTiles = useBoardState((state) => state.allTiles); + const tilesOnBoard = useBoardState((state) => state.tilesOnBoard); const setTiles = useBoardState((state) => state.addTile); const updateTile = useBoardState((state) => state.updateTile); const stageRef = useBoardState((state) => state.stageReference); @@ -19,9 +20,11 @@ export const useMouse = () => { const userColor = useWebSocketState( (state) => state.room?.users.find((user) => user.userId === socket?.id)?.color, ); + const setSelectedTile = useBoardState((state) => state.setSelectedTile); const toggleCategory = () => { if (categoriesOpen) { + setSelectedTile(null); setCategoriesOpen(false); } }; @@ -98,18 +101,21 @@ export const useMouse = () => { const tile = allTiles.find( (tile) => tile.name === event.currentTarget.getAttribute('data-name'), ); + console.log(tile?.id); if (tile) { + const { _id: id, name, src, color, points, textPosition } = tile; const dragPayload = JSON.stringify({ nodeClass: event.currentTarget.getAttribute('data-class'), offsetX: event.nativeEvent.offsetX, offsetY: event.nativeEvent.offsetY, clientWidth: event.currentTarget.clientWidth, clientHeight: event.currentTarget.clientHeight, - src: tile.src, - name: tile.name, - color: tile.color, - points: tile.points, - textPosition: tile.textPosition, + id: id, + src: src, + name: name, + color: color, + points: points, + textPosition: textPosition, }); event.dataTransfer.setData('dragStart/Tile', dragPayload); } @@ -145,6 +151,7 @@ export const useMouse = () => { stageRef.current.setPointersPositions(event); const { x, y } = stageRef.current.getRelativePointerPosition(); const { + id, src, color, textPosition, @@ -158,7 +165,7 @@ export const useMouse = () => { } = JSON.parse(draggedData); if (x && y) { const newTile: Tile = { - id: uuidv4(), + id: id, src: src, category: nodeClass, x: x - (offsetX - clientWidth / 2), @@ -210,6 +217,16 @@ export const useMouse = () => { } }; + const setClickedTile = (event: KonvaEventObject) => { + // set the actively clicked TIle + const clickedTile = tilesOnBoard.find((tile) => tile.id === event.target.attrs.id); + if (clickedTile) { + setSelectedTile(clickedTile); + } else { + setSelectedTile(null); + } + }; + return { handleMouseMove, handleDragOver, @@ -218,6 +235,7 @@ export const useMouse = () => { handleWheel, toggleCategory, handleMouseEnL, + setClickedTile, updateTilePosition, setActiveDragElement, }; diff --git a/frontend/src/state/BoardState.tsx b/frontend/src/state/BoardState.tsx index f08ef9b..2d87fa5 100644 --- a/frontend/src/state/BoardState.tsx +++ b/frontend/src/state/BoardState.tsx @@ -11,19 +11,21 @@ export type BoardContextType = { categoriesOpen: boolean; allTiles: Tile[]; tilesOnBoard: Tile[]; + selectedTile: Tile | null; remoteDragColor: string | null; - activeDragTile: React.RefObject | null; stageReference: React.RefObject; + activeDragTile: React.RefObject | null; clearActiveDragTile: () => void; addTile: (newTile: Tile) => void; + setSelectedTile: (tile: Tile | null) => void; toggleModal: (toggle: boolean) => void; updateTile: (updatedNode: Tile) => void; + setAllTiles: (tilesArray: Tile[]) => void; removeTile: (nodeToRemove: string) => void; setCategoriesOpen: (toggle: boolean) => void; - setAllTiles: (tilesArray: Tile[]) => void; setRemoteDragColor: (color: string | null) => void; - setActiveDragTile: (newActiveTile: React.RefObject) => void; setStageReference: (stage: React.RefObject) => void; + setActiveDragTile: (newActiveTile: React.RefObject) => void; }; export const useBoardState = create((set) => ({ @@ -31,11 +33,13 @@ export const useBoardState = create((set) => ({ modalOpen: false, categoriesOpen: false, tilesOnBoard: [], + selectedTile: null, activeDragTile: null, remoteDragColor: '', stageReference: createRef(), clearActiveDragTile: () => set(() => ({ activeDragTile: null })), + setSelectedTile: (tile: Tile | null) => set(() => ({ selectedTile: tile })), setActiveDragTile: (newActiveTile: React.RefObject) => set(() => ({ activeDragTile: newActiveTile })), setCategoriesOpen: (isOpen: boolean) => set(() => ({ categoriesOpen: isOpen })), diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index 4b0bc9b..cfefda5 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -30,6 +30,7 @@ export type CursorData = { export type Tile = { x: number; y: number; + _id?: string; id: string; src: string; name: string; From 673105cb286e7173e3be14b3b43e2bf71a96d7cd Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Wed, 25 Jan 2023 22:00:08 +0100 Subject: [PATCH 04/44] added anchorpoints and line to Tile --- frontend/src/components/Board/Board.tsx | 10 - frontend/src/components/Sidebar/Sidebar.tsx | 25 ++- frontend/src/components/Tiles/Tile.tsx | 193 ++++++++++++++---- frontend/src/components/Tiles/TileBorder.tsx | 15 +- .../components/Tiles/TileBorderAnchors.tsx | 47 +++++ frontend/src/hooks/useAnchor.tsx | 118 +++++++++++ frontend/src/hooks/useMouse.tsx | 22 +- frontend/src/json/kacheln.json | 140 ++++++------- frontend/src/types.d.ts | 5 + 9 files changed, 428 insertions(+), 147 deletions(-) create mode 100644 frontend/src/components/Tiles/TileBorderAnchors.tsx create mode 100644 frontend/src/hooks/useAnchor.tsx diff --git a/frontend/src/components/Board/Board.tsx b/frontend/src/components/Board/Board.tsx index 44dba6c..d968f18 100644 --- a/frontend/src/components/Board/Board.tsx +++ b/frontend/src/components/Board/Board.tsx @@ -69,16 +69,6 @@ const Board = () => { > {gridComponents} - {selectedTile != null && ( - - )} {room?.tiles?.map((tileObject) => ( { }; useEffect(() => { - (async () => { - try { - const response = await fetch(`${backendUrl}`, { - method: 'GET', - }); - const data = await response.json(); - setAllTiles(data); - } catch (error) { - notify('error', 'There was an error fetching the tiles, please try again later', false); - } - })(); + // (async () => { + // try { + // const response = await fetch(`${backendUrl}`, { + // method: 'GET', + // }); + // const data = await response.json(); + // setAllTiles(data); + // } catch (error) { + // notify('error', 'There was an error fetching the tiles, please try again later', false); + // } + // })(); + + setAllTiles(kacheln as any); }, []); useEffect(() => { diff --git a/frontend/src/components/Tiles/Tile.tsx b/frontend/src/components/Tiles/Tile.tsx index b614ce5..81f1d99 100644 --- a/frontend/src/components/Tiles/Tile.tsx +++ b/frontend/src/components/Tiles/Tile.tsx @@ -1,56 +1,175 @@ import React from 'react'; -import { Tile as TileProps } from '../../types'; -import { Group, Image, Text, Line } from 'react-konva'; +import TileBorder from './TileBorder'; +import { Tile as TileProps, Coordinates } from '../../types'; import { useMouse } from '../../hooks/useMouse'; +import TileBorderAnchors from './TileBorderAnchors'; import { Group as GroupType } from 'konva/lib/Group'; +import { Group, Text, Line } from 'react-konva'; import { useContextMenu } from '../../hooks/useContextMenu'; import { useWebSocketState } from '../../state/WebSocketState'; -// import useImage from 'use-image'; -// import { useBoardState } from '../../state/BoardState'; +import { KonvaEventObject } from 'konva/lib/Node'; +import useAnchor from '../../hooks/useAnchor'; +import { useBoardState } from '../../state/BoardState'; -const Tile: React.FC = ({ x, y, id, src, name, color, points, category, textPosition }) => { +const Tile: React.FC = ({ + x, + y, + id, + src, + name, + color, + points, + category, + textPosition, +}) => { + const { getAnchorPoints } = useAnchor(); + const [showBorder, setShowBoarder] = React.useState(false); const tileRef = React.useRef(null); + const { handleContextMenu } = useContextMenu(); + const { handleMouseEnL, updateTilePosition, setActiveDragElement } = useMouse(); + const tilesOnBoard = useBoardState((state) => state.tilesOnBoard); + const [connectionPreview, setConnectionPreview] = React.useState(null); + const [connections, setConnections] = React.useState<{ source: string; destination: string }[]>( + [], + ); const userColor = useWebSocketState( (state) => state.room?.users?.find((user) => user.userId === state.socket?.id)?.color, ); - const { setClickedTile, handleMouseEnL, updateTilePosition, setActiveDragElement } = useMouse(); - const { handleContextMenu } = useContextMenu(); - // const [image] = useImage(src); - // const remoteDragColor = useBoardState((state) => state.remoteDragColor); + const [groupSize, setGroupSize] = React.useState<{ width: number; height: number }>({ + width: 0, + height: 0, + }); + + const createConnectionPoints = ( + source: { x: number; y: number }, + destination: { x: number; y: number }, + ) => { + return [source.x, source.y, destination.x, destination.y]; + }; + + const hasInterSection = (position: Coordinates, tilePosition: Coordinates) => { + return !(tilePosition.x > position.x || tilePosition.y > position.y); + }; + + const detectConnection = (position: Coordinates) => { + const intersectingStep: TileProps | undefined = tilesOnBoard.find((tile) => { + const tilePosition = { x: tile.x, y: tile.y }; + return hasInterSection(position, tilePosition); + }); + if (intersectingStep) { + return intersectingStep; + } + return null; + }; + + const handleAnchorDragStart = (e: KonvaEventObject) => { + const position = e.target.position(); + setConnectionPreview( + , + ); + }; + + const handleAnchorDragMove = (e: KonvaEventObject) => { + const position = e.target.position(); + const stage = e.target.getStage(); + const pointerPosition = stage?.getPointerPosition(); + if (pointerPosition) { + const mousePos = { + x: pointerPosition.x - position.x, + y: pointerPosition.y - position.y, + }; + setConnectionPreview( + , + ); + } + }; + + const handleAnchorDragEnd = (e: KonvaEventObject, id: string) => { + setConnectionPreview(null); + }; + + const handleClick = (event: KonvaEventObject) => { + setShowBoarder(!showBorder); + if (tileRef.current) { + setGroupSize({ + width: event.currentTarget.getClientRect({ + skipTransform: true, + }).width, + height: event.currentTarget.getClientRect({ + skipTransform: true, + }).height, + }); + } + }; - // add border to image if active return ( <> {userColor && ( - setClickedTile(e)} - onDragMove={(event) => setActiveDragElement(tileRef, event)} - onDragEnd={updateTilePosition} - onMouseOver={(e) => handleMouseEnL(e, 4)} - onMouseLeave={(e) => handleMouseEnL(e, 0)} - > - {/* */} - - - - + <> + {showBorder && ( + <> + + {getAnchorPoints(x, y, category, name, groupSize)?.map((point, index) => ( + + ))} + + )} + handleClick(event)} + onDragMove={(event) => setActiveDragElement(tileRef, event)} + onDragEnd={updateTilePosition} + onMouseOver={(event) => handleMouseEnL(event, showBorder, 4)} + onMouseLeave={(event) => handleMouseEnL(event, showBorder, 0)} + > + + + + )} + {connectionPreview} ); -};; +}; export default Tile; diff --git a/frontend/src/components/Tiles/TileBorder.tsx b/frontend/src/components/Tiles/TileBorder.tsx index 47c3c6f..2d81f03 100644 --- a/frontend/src/components/Tiles/TileBorder.tsx +++ b/frontend/src/components/Tiles/TileBorder.tsx @@ -2,27 +2,26 @@ import React from 'react'; import { Line } from 'react-konva'; type Props = { - tilePosition: { - x: number; - y: number; - }; + x: number; + y: number; points?: number[]; id: string; }; -const TileBorder: React.FC = ({ tilePosition, id, points }) => { +const TileBorder: React.FC = ({ x, y, id, points }) => { const SIZE = 50; const defaultPoints = [0, 0, SIZE, 0, SIZE, SIZE, 0, SIZE, 0, 0]; - const { x, y } = tilePosition; + return ( ); }; diff --git a/frontend/src/components/Tiles/TileBorderAnchors.tsx b/frontend/src/components/Tiles/TileBorderAnchors.tsx new file mode 100644 index 0000000..d5c0c4f --- /dev/null +++ b/frontend/src/components/Tiles/TileBorderAnchors.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { Circle, Line } from 'react-konva'; +import { Circle as CircleObject } from 'konva/lib/shapes/Circle'; +import { KonvaEventObject } from 'konva/lib/Node'; + +type Props = { + x: number; + y: number; + id: string; + dragStart: (e: KonvaEventObject) => void; + dragMove: (e: KonvaEventObject) => void; + dragEnd: (e: KonvaEventObject, id: string) => void; +}; + +const TileBorderAnchors: React.FC = ({ x, y, id, dragStart, dragMove, dragEnd }) => { + const dragBounds = (ref: React.RefObject) => { + if (ref.current !== null) { + return ref.current.getAbsolutePosition(); + } + return { + x: 0, + y: 0, + }; + }; + + const anchor = React.useRef(null); + return ( + <> + dragEnd(event, id)} + dragBoundFunc={() => dragBounds(anchor)} + perfectDrawEnabled={false} + ref={anchor} + /> + + ); +}; + +export default TileBorderAnchors; diff --git a/frontend/src/hooks/useAnchor.tsx b/frontend/src/hooks/useAnchor.tsx new file mode 100644 index 0000000..9201628 --- /dev/null +++ b/frontend/src/hooks/useAnchor.tsx @@ -0,0 +1,118 @@ +import { KonvaEventObject } from 'konva/lib/Node'; +import React from 'react'; +import { Line } from 'react-konva'; + +interface Coordinates { + x: number; + y: number; +} + +const useAnchor = () => { + const getAnchorPoints = ( + x: number, + y: number, + category: string, + tileName: string, + groupSize: { width: number; height: number }, + ) => { + switch (category) { + case 'Start': + return [{ x: x + groupSize.width / 2 + 50, y: y - 50 }]; + break; + case 'Ende': + return [{ x: x - groupSize.width / 2, y: y - 100 }]; + break; + case 'Objekte': + return [ + { x: x - groupSize.width / 5 + 10, y: y - 50 }, + { x: x + groupSize.width - 130, y: y - 50 }, + ]; + break; + case 'Zustand': + return [ + { x: x - groupSize.width / 2 - 10, y: y - 100 }, + { x: x + groupSize.width / 2, y: y - 100 }, + ]; + case 'Konditionen': + if (tileName === 'Dann') { + return [ + { x: x - groupSize.width / 2, y: y - 50 }, + { x: x + groupSize.width / 2, y: y - 50 }, + ]; + } else { + if (tileName === 'Und') { + return [ + { x: x + groupSize.width - 400, y: y - 150 }, + { x: x + groupSize.width / 2 + 40, y: y - 150 }, + { x: x + groupSize.width / 2 + 40, y: y + 150 }, + { x: x + groupSize.width - 400, y: y + 150 }, + ]; + } + } + break; + default: + console.warn(`Please provide a valid category. Given: ${category}`); + break; + } + }; + + const handleAnchorDragStart = ( + e: KonvaEventObject, + callback: (value: JSX.Element | null) => void, + createConnectionPoints: ( + fromPosition: Coordinates, + toPosition: Coordinates, + ) => [fx: number, fy: number, tx: number, ty: number], + ) => { + const position = e.target.position(); + callback( + , + ); + }; + + const handleAnchorDragMove = ( + e: KonvaEventObject, + callback: (value: JSX.Element) => void, + createConnectionPoints: ( + fromPosition: Coordinates, + toPosition: Coordinates, + ) => [fx: number, fy: number, tx: number, ty: number], + ) => { + const position = e.target.position(); + const stage = e.target.getStage(); + const pointerPosition = stage?.getPointerPosition(); + if (pointerPosition) { + const mousePos = { + x: pointerPosition.x - position.x, + y: pointerPosition.y - position.y, + }; + callback( + , + ); + } + }; + + const handleAnchorDragEnd = ( + e: KonvaEventObject, + id: string, + callback: (value: JSX.Element | null) => void, + ) => { + callback(null); + }; + + return { getAnchorPoints, handleAnchorDragStart, handleAnchorDragMove, handleAnchorDragEnd }; +}; + +export default useAnchor; diff --git a/frontend/src/hooks/useMouse.tsx b/frontend/src/hooks/useMouse.tsx index 83397f6..b54e34f 100644 --- a/frontend/src/hooks/useMouse.tsx +++ b/frontend/src/hooks/useMouse.tsx @@ -1,6 +1,5 @@ import React from 'react'; import { SocketDragTile, Tile } from '../types'; -import { v4 as uuidv4 } from 'uuid'; import { Group } from 'konva/lib/Group'; import { KonvaEventObject } from 'konva/lib/Node'; import { useBoardState } from '../state/BoardState'; @@ -14,7 +13,6 @@ export const useMouse = () => { const stageRef = useBoardState((state) => state.stageReference); const setActiveDragTile = useBoardState((state) => state.setActiveDragTile); const socket = useWebSocketState((state) => state.socket); - const categoriesOpen = useBoardState((state) => state.categoriesOpen); const setCategoriesOpen = useBoardState((state) => state.setCategoriesOpen); const roomId = useWebSocketState((state) => state.room?.roomId); const userColor = useWebSocketState( @@ -23,10 +21,8 @@ export const useMouse = () => { const setSelectedTile = useBoardState((state) => state.setSelectedTile); const toggleCategory = () => { - if (categoriesOpen) { - setSelectedTile(null); - setCategoriesOpen(false); - } + setSelectedTile(null); + setCategoriesOpen(false); }; const handleMouseMove = () => { @@ -88,10 +84,18 @@ export const useMouse = () => { } }; - const handleMouseEnL = (event: KonvaEventObject, strokeWidth: number) => { + const handleMouseEnL = ( + event: KonvaEventObject, + isClicked: boolean, + strokeWidth: number, + ) => { // for mouse Enter and Leave // function to set the stroke width when user hovers over a Tile - event.target.setAttr('strokeWidth', strokeWidth); + if (isClicked === true) { + event.target.setAttr('strokeWidth', 0); + } else { + event.target.setAttr('strokeWidth', strokeWidth); + } }; const handleDragOver = (event: React.DragEvent) => event.preventDefault(); @@ -101,7 +105,7 @@ export const useMouse = () => { const tile = allTiles.find( (tile) => tile.name === event.currentTarget.getAttribute('data-name'), ); - console.log(tile?.id); + if (tile) { const { _id: id, name, src, color, points, textPosition } = tile; const dragPayload = JSON.stringify({ diff --git a/frontend/src/json/kacheln.json b/frontend/src/json/kacheln.json index 56d7cb6..b64d706 100644 --- a/frontend/src/json/kacheln.json +++ b/frontend/src/json/kacheln.json @@ -1,90 +1,86 @@ [ { + "_id": "63cda4a235c0dc3e0c5f0455", "category": "Start", "name": "Wenn", - "svgPath": "M1 186L89 237V117L194 57L97 1L1 186Z", - "svgRotate": 45, - "fill": "#f9b43d", - "src": "https://i.ibb.co/8M0jgv1/When.png" + "src": "http://localhost:9001/uploads/1674421410153.png", + "points": [0, -200, 200, -200, 100, -50, 200, 100, 0, 100], + "color": "#f9b43d", + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cda4a235c0dc3e0c5f0456" + }, + "__v": 0 }, { - "category": "End", - "name": "Ende", - "svgPath": "M193.5 1H2L105 66.5L2 131.5H193.5V1Z", - "svgRotate": 0, - "fill": "#f9b43d", - "src": "https://i.ibb.co/v47Vmqq/end.png" + "_id": "63cda72e35c0dc3e0c5f0460", + "category": "Ende", + "name": " ", + "src": "http://localhost:9001/uploads/1674422062486.png", + "points": [-200, -250, 0, -250, 0, 50, -200, 50, -100, -100], + "color": "#f9b43d", + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cda72e35c0dc3e0c5f0461" + }, + "__v": 0 }, { - "category": "Objects", - "name": "Handy", - "svgPath": "M104 0L207.923 60V180L104 240L0.0769501 180V60L104 0Z", - "svgRotate": 0, - "fill": "#eb555b", - "src": "https://i.ibb.co/JrmGxY9/Lamp.png" + "_id": "63cdabf635c0dc3e0c5f046e", + "category": "Objekte", + "name": "Smarte Lampe", + "src": "http://localhost:9001/uploads/1674425498259.png", + "points": [0, -100, 200, -100, 300, 50, 200, 200, 0, 200, -100, 50], + "color": "#EB555B", + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cdabf635c0dc3e0c5f046f" + }, + "__v": 1 }, { - "category": "Objects", - "name": "Licht", - "svgPath": "M104 0L207.923 60V180L104 240L0.0769501 180V60L104 0Z", - "svgRotate": 0, - "fill": "#eb555b", - "src": "https://i.ibb.co/JrmGxY9/Lamp.png" - }, - { - "category": "Objects", - "name": "Lampe", - "svgPath": "M104 0L207.923 60V180L104 240L0.0769501 180V60L104 0Z", - "svgRotate": 0, - "fill": "#eb555b", - "src": "https://i.ibb.co/JrmGxY9/Lamp.png" - }, - { - "category": "Actions", + "_id": "63cdad5d35c0dc3e0c5f0475", + "category": "Zustand", "name": "an", - "svgPath": "M103 0L206 66.5L103 131L0 66.5L103 0Z", - "svgRotate": 0, - "fill": "#f4aece", - "src": "https://i.ibb.co/0996SHK/an.png" - }, - { - "category": "Actions", - "name": "aus", - "svgPath": "M103 0L206 66.5L103 131L0 66.5L103 0Z", - "svgRotate": 0, - "fill": "#f4aece", - "src": "https://i.ibb.co/8MjkqL5/aus.png" - }, - { - "category": "Conditions", - "name": "Solange", - "svgPath": "M105 0L208 60V149L105 85L0 149V60L105 0Z", - "svgRotate": 0, - "fill": "#bababa", - "src": "https://i.ibb.co/gVgx6Cf/solange.png" - }, - { - "category": "Conditions", - "name": "Sonst", - "svgPath": "M105 0L208 60V149L105 85L0 149V60L105 0Z", - "svgRotate": 0, - "fill": "#bababa", - "src": "https://i.ibb.co/hLMHLr9/sonst.png" + "src": "http://localhost:9001/uploads/1674423645286.png", + "points": [-200, -250, 0, -250, 100, -100, 0, 50, -200, 50, -100, -100], + "color": "#F4AECE", + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cdad5d35c0dc3e0c5f0476" + }, + "__v": 2 }, { - "category": "Negation", - "name": "Nicht", - "svgPath": "M105 0L208 60V149L105 85L0 149V60L105 0Z", - "svgRotate": 0, - "fill": "#eb555b", - "src": "https://i.ibb.co/6Xd9LyJ/Not.png" + "_id": "63cdaff535c0dc3e0c5f0478", + "category": "Konditionen", + "name": "Dann", + "src": "http://localhost:9001/uploads/1674424309409.png", + "points": [-100, -100, -200, 50, 200, 50, 100, -100], + "color": "#F9B43D", + "textPosition": { + "x": -50, + "y": -50, + "_id": "63cdaff535c0dc3e0c5f0479" + }, + "__v": 1 }, { - "category": "Union", + "_id": "63cdb2bc35c0dc3e0c5f047d", + "category": "Konditionen", "name": "Und", - "svgPath": "M0 0L102.5 66.5V181L0 246.5V0Z", - "svgRotate": 0, - "fill": "#bababa", - "src": "https://i.ibb.co/4j5Mznp/Und.png" + "src": "http://localhost:9001/uploads/1674425020844.png", + "points": [-100, -50, 0, -200, 100, -200, 200, -50, 200, 50, 100, 200, 0, 200, -100, 50], + "color": "#E2E7DF", + "textPosition": { + "x": -50, + "y": -50, + "_id": "63cdb2bc35c0dc3e0c5f047e" + }, + "__v": 0 } ] diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index cfefda5..e694116 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -64,4 +64,9 @@ export type RoomData = { roomId: string; users: UserData[]; tiles?: TileData[]; +}; + +export type Coordinates = { + x: number; + y: number; }; \ No newline at end of file From c31d03db85e7d868d15745bd54a6558126f76e01 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Thu, 26 Jan 2023 19:20:36 +0100 Subject: [PATCH 05/44] fixed _id problem --- frontend/src/hooks/useMouse.tsx | 63 ++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/frontend/src/hooks/useMouse.tsx b/frontend/src/hooks/useMouse.tsx index b54e34f..d7383a1 100644 --- a/frontend/src/hooks/useMouse.tsx +++ b/frontend/src/hooks/useMouse.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { SocketDragTile, Tile } from '../types'; +import { v4 as uuidv4 } from 'uuid'; import { Group } from 'konva/lib/Group'; import { KonvaEventObject } from 'konva/lib/Node'; import { useBoardState } from '../state/BoardState'; @@ -51,6 +52,7 @@ export const useMouse = () => { setActiveDragTile(activeTileReference); if (stageRef.current) { + // TODO: fix missing attribute const { 'data-src': src } = event.target.attrs; const stage = stageRef.current; const pos = stage.getRelativePointerPosition(); @@ -60,6 +62,8 @@ export const useMouse = () => { y: event.target.y(), id: event.target.attrs.id, name: event.target.attrs.name, + width: event.target.width(), + height: event.target.height(), color: event.target.attrs.color, points: event.target.attrs.points, category: event.target.attrs.name, @@ -107,14 +111,15 @@ export const useMouse = () => { ); if (tile) { - const { _id: id, name, src, color, points, textPosition } = tile; + const { _id, name, src, color, points, textPosition } = tile; const dragPayload = JSON.stringify({ nodeClass: event.currentTarget.getAttribute('data-class'), offsetX: event.nativeEvent.offsetX, offsetY: event.nativeEvent.offsetY, clientWidth: event.currentTarget.clientWidth, clientHeight: event.currentTarget.clientHeight, - id: id, + id: uuidv4(), + _id: _id, src: src, name: name, color: color, @@ -125,37 +130,17 @@ export const useMouse = () => { } }; - const updateTilePosition = (event: KonvaEventObject) => { - /** - * updates the Tile position in the state - * also clears the active Drag Element - */ - const { 'data-src': src } = event.target.attrs; - if (stageRef.current) { - const updatedTile: Tile = { - src: src, - x: event.target.x(), - y: event.target.y(), - id: event.target.attrs.id, - name: event.target.attrs.name, - color: event.target.attrs.fill, - category: event.target.attrs.name, - points: event.target.attrs.points, - textPosition: event.target.attrs.textPosition, - }; - updateTile(updatedTile); - } - }; - const handleDrop = (event: React.DragEvent) => { // add Tile to stage event.preventDefault(); const draggedData = event.dataTransfer.getData('dragStart/Tile'); + console.log(draggedData); if (draggedData && stageRef.current != null) { stageRef.current.setPointersPositions(event); const { x, y } = stageRef.current.getRelativePointerPosition(); const { id, + _id, src, color, textPosition, @@ -170,13 +155,16 @@ export const useMouse = () => { if (x && y) { const newTile: Tile = { id: id, + _id: _id, src: src, category: nodeClass, x: x - (offsetX - clientWidth / 2), y: y - (offsetY - clientHeight / 2), - color: color, name: name, + color: color, points: points, + width: clientWidth, + height: clientHeight, textPosition: textPosition, }; setTiles(newTile); @@ -193,6 +181,31 @@ export const useMouse = () => { } }; + const updateTilePosition = (event: KonvaEventObject) => { + /** + * updates the Tile position in the state + * also clears the active Drag Element + */ + const { 'data-src': src } = event.target.attrs; + if (stageRef.current) { + const rect = event.currentTarget.getClientRect(); + const updatedTile: Tile = { + src: src, + x: event.target.x(), + y: event.target.y(), + width: rect.width, + height: rect.height, + id: event.target.attrs.id, + name: event.target.attrs.name, + color: event.target.attrs.fill, + category: event.target.attrs.name, + points: event.target.attrs.points, + textPosition: event.target.attrs.textPosition, + }; + updateTile(updatedTile); + } + }; + const handleWheel = (event: KonvaEventObject) => { // scale a stage up or down event.evt.preventDefault(); From d8205c123376d701ec84f05ec16e79afef393f26 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Thu, 26 Jan 2023 19:20:58 +0100 Subject: [PATCH 06/44] added bounding width and height to Tile config --- frontend/src/types.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index e694116..0341fc3 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -30,11 +30,13 @@ export type CursorData = { export type Tile = { x: number; y: number; - _id?: string; id: string; + _id?: string; src: string; name: string; color: string; + width: number; + height: number; points: number[]; category: string; textPosition: { x: number; y: number }; From ead3171c24f287f2c71ddae89b84ff8390332e3b Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Thu, 26 Jan 2023 19:45:37 +0100 Subject: [PATCH 07/44] added width, height and _id as information --- backend/src/Controller/socketController.ts | 2 + backend/types/socket.types.d.ts | 10 +- frontend/src/components/Board/Board.tsx | 3 + frontend/src/components/Tiles/Tile.tsx | 56 ++++++--- frontend/src/hooks/useMouse.tsx | 127 +++++++++++---------- frontend/src/types.d.ts | 4 +- 6 files changed, 117 insertions(+), 85 deletions(-) diff --git a/backend/src/Controller/socketController.ts b/backend/src/Controller/socketController.ts index e0cb569..a4b43f3 100644 --- a/backend/src/Controller/socketController.ts +++ b/backend/src/Controller/socketController.ts @@ -78,6 +78,8 @@ export const tileDrop = ( src: data.tile.src, x: data.tile.x, y: data.tile.y, + width: data.tile.width, + height: data.tile.height, points: data.tile.points, color: data.tile.color, textPosition: data.tile.textPosition, diff --git a/backend/types/socket.types.d.ts b/backend/types/socket.types.d.ts index 40360aa..024b26f 100644 --- a/backend/types/socket.types.d.ts +++ b/backend/types/socket.types.d.ts @@ -16,14 +16,16 @@ export type SocketDeleteData = { }; export type NewTile = { - id: string; - category: string; - src: string; x: number; y: number; + id: string; + src: string; name: string; - points: number[]; + width: number; color: string; + height: number; + category: string; + points: number[]; textPosition: { x: number; y: number }; }; diff --git a/frontend/src/components/Board/Board.tsx b/frontend/src/components/Board/Board.tsx index d968f18..df621f0 100644 --- a/frontend/src/components/Board/Board.tsx +++ b/frontend/src/components/Board/Board.tsx @@ -74,10 +74,13 @@ const Board = () => { key={tileObject.tile.id} x={tileObject.tile.x} y={tileObject.tile.y} + _id={tileObject.tile._id} id={tileObject.tile.id} src={tileObject.tile.src} name={tileObject.tile.name} color={tileObject.tile.color} + width={tileObject.tile.width} + height={tileObject.tile.height} points={tileObject.tile.points} category={tileObject.tile.category} textPosition={tileObject.tile.textPosition} diff --git a/frontend/src/components/Tiles/Tile.tsx b/frontend/src/components/Tiles/Tile.tsx index 81f1d99..e3a8521 100644 --- a/frontend/src/components/Tiles/Tile.tsx +++ b/frontend/src/components/Tiles/Tile.tsx @@ -15,6 +15,7 @@ const Tile: React.FC = ({ x, y, id, + _id, src, name, color, @@ -29,9 +30,9 @@ const Tile: React.FC = ({ const { handleMouseEnL, updateTilePosition, setActiveDragElement } = useMouse(); const tilesOnBoard = useBoardState((state) => state.tilesOnBoard); const [connectionPreview, setConnectionPreview] = React.useState(null); - const [connections, setConnections] = React.useState<{ source: string; destination: string }[]>( - [], - ); + const [connections, setConnections] = React.useState< + { source: string; destination: Coordinates }[] + >([]); const userColor = useWebSocketState( (state) => state.room?.users?.find((user) => user.userId === state.socket?.id)?.color, ); @@ -52,13 +53,15 @@ const Tile: React.FC = ({ }; const detectConnection = (position: Coordinates) => { - const intersectingStep: TileProps | undefined = tilesOnBoard.find((tile) => { + const intersectingTile: TileProps | undefined = tilesOnBoard.find((tile) => { const tilePosition = { x: tile.x, y: tile.y }; return hasInterSection(position, tilePosition); }); - if (intersectingStep) { - return intersectingStep; + if (intersectingTile) { + console.log('intersectingTile: ', intersectingTile); + return intersectingTile; } + console.log('no interesction'); return null; }; @@ -98,18 +101,28 @@ const Tile: React.FC = ({ const handleAnchorDragEnd = (e: KonvaEventObject, id: string) => { setConnectionPreview(null); + const stage = e.target.getStage(); + const pointerPosition = stage?.getPointerPosition(); + if (pointerPosition) { + const intersectingTile = detectConnection(pointerPosition); + if (intersectingTile !== null) { + setConnections((prev) => [ + ...prev, + { source: id, destination: { x: intersectingTile.x, y: intersectingTile.y } }, + ]); + } + } }; const handleClick = (event: KonvaEventObject) => { setShowBoarder(!showBorder); if (tileRef.current) { + const rect = event.currentTarget.getClientRect({ + skipTransform: true, + }); setGroupSize({ - width: event.currentTarget.getClientRect({ - skipTransform: true, - }).width, - height: event.currentTarget.getClientRect({ - skipTransform: true, - }).height, + width: rect.width, + height: rect.height, }); } }; @@ -118,18 +131,18 @@ const Tile: React.FC = ({ <> {userColor && ( <> - {showBorder && ( + {showBorder && id && ( <> {getAnchorPoints(x, y, category, name, groupSize)?.map((point, index) => ( ))} @@ -138,14 +151,18 @@ const Tile: React.FC = ({ onContextMenu={handleContextMenu} ref={tileRef} draggable - data-src={src} x={x} y={y} id={id} + data-id={_id} + data-src={src} name={category} + data-fill={color} + onDragEnd={updateTilePosition} + data-points={JSON.stringify(points)} onClick={(event) => handleClick(event)} + data-textPosition={JSON.stringify(textPosition)} onDragMove={(event) => setActiveDragElement(tileRef, event)} - onDragEnd={updateTilePosition} onMouseOver={(event) => handleMouseEnL(event, showBorder, 4)} onMouseLeave={(event) => handleMouseEnL(event, showBorder, 0)} > @@ -172,4 +189,5 @@ const Tile: React.FC = ({ ); }; + export default Tile; diff --git a/frontend/src/hooks/useMouse.tsx b/frontend/src/hooks/useMouse.tsx index d7383a1..b4e87d8 100644 --- a/frontend/src/hooks/useMouse.tsx +++ b/frontend/src/hooks/useMouse.tsx @@ -7,19 +7,19 @@ import { useBoardState } from '../state/BoardState'; import { useWebSocketState } from '../state/WebSocketState'; export const useMouse = () => { - const allTiles = useBoardState((state) => state.allTiles); - const tilesOnBoard = useBoardState((state) => state.tilesOnBoard); const setTiles = useBoardState((state) => state.addTile); + const socket = useWebSocketState((state) => state.socket); + const allTiles = useBoardState((state) => state.allTiles); const updateTile = useBoardState((state) => state.updateTile); const stageRef = useBoardState((state) => state.stageReference); + const roomId = useWebSocketState((state) => state.room?.roomId); + const tilesOnBoard = useBoardState((state) => state.tilesOnBoard); + const setSelectedTile = useBoardState((state) => state.setSelectedTile); const setActiveDragTile = useBoardState((state) => state.setActiveDragTile); - const socket = useWebSocketState((state) => state.socket); const setCategoriesOpen = useBoardState((state) => state.setCategoriesOpen); - const roomId = useWebSocketState((state) => state.room?.roomId); const userColor = useWebSocketState( (state) => state.room?.users.find((user) => user.userId === socket?.id)?.color, ); - const setSelectedTile = useBoardState((state) => state.setSelectedTile); const toggleCategory = () => { setSelectedTile(null); @@ -45,48 +45,7 @@ export const useMouse = () => { } }; - const setActiveDragElement = ( - activeTileReference: React.RefObject, - event: KonvaEventObject, - ) => { - setActiveDragTile(activeTileReference); - if (stageRef.current) { - // TODO: fix missing attribute - const { 'data-src': src } = event.target.attrs; - const stage = stageRef.current; - const pos = stage.getRelativePointerPosition(); - const updatedTile: Tile = { - src: src, - x: event.target.x(), - y: event.target.y(), - id: event.target.attrs.id, - name: event.target.attrs.name, - width: event.target.width(), - height: event.target.height(), - color: event.target.attrs.color, - points: event.target.attrs.points, - category: event.target.attrs.name, - textPosition: event.target.attrs.textPosition, - }; - if (socket !== null && roomId && userColor) { - const socketDragTile: SocketDragTile = { - remoteUser: socket.id, - tile: updatedTile, - roomId: roomId, - remoteUserColor: userColor, - }; - const cursorPos = { - x: pos?.x, - y: pos?.y, - remoteUser: socket.id, - roomId: roomId, - }; - socket?.emit('tile-drag', socketDragTile); - socket?.emit('cursor', cursorPos); - } - } - }; const handleMouseEnL = ( event: KonvaEventObject, @@ -186,21 +145,29 @@ export const useMouse = () => { * updates the Tile position in the state * also clears the active Drag Element */ - const { 'data-src': src } = event.target.attrs; + + const { + 'data-src': src, + 'data-fill': color, + 'data-id': _id, + 'data-points': points, + 'data-textPosition': textPosition, + } = event.target.attrs; if (stageRef.current) { const rect = event.currentTarget.getClientRect(); const updatedTile: Tile = { + _id: _id, src: src, + color: color, + points: points, + width: rect.width, x: event.target.x(), y: event.target.y(), - width: rect.width, height: rect.height, id: event.target.attrs.id, + textPosition: textPosition, name: event.target.attrs.name, - color: event.target.attrs.fill, category: event.target.attrs.name, - points: event.target.attrs.points, - textPosition: event.target.attrs.textPosition, }; updateTile(updatedTile); } @@ -234,16 +201,57 @@ export const useMouse = () => { } }; - const setClickedTile = (event: KonvaEventObject) => { - // set the actively clicked TIle - const clickedTile = tilesOnBoard.find((tile) => tile.id === event.target.attrs.id); - if (clickedTile) { - setSelectedTile(clickedTile); - } else { - setSelectedTile(null); + const setActiveDragElement = ( + activeTileReference: React.RefObject, + event: KonvaEventObject, + ) => { + setActiveDragTile(activeTileReference); + + if (stageRef.current) { + // TODO: fix missing attribute + const { + 'data-src': src, + 'data-fill': color, + 'data-id': _id, + 'data-points': points, + 'data-textPosition': textPosition, + } = event.target.attrs; + const rect = event.currentTarget.getClientRect(); + const stage = stageRef.current; + const pos = stage.getRelativePointerPosition(); + const updatedTile: Tile = { + _id: _id, + src: src, + color: color, + points: Array.from(points), + width: rect.width, + x: event.target.x(), + y: event.target.y(), + height: rect.height, + id: event.target.attrs.id, + textPosition: JSON.parse(textPosition), + name: event.target.attrs.name, + category: event.target.attrs.name, + }; + if (socket !== null && roomId && userColor) { + const socketDragTile: SocketDragTile = { + remoteUser: socket.id, + tile: updatedTile, + roomId: roomId, + remoteUserColor: userColor, + }; + const cursorPos = { + x: pos?.x, + y: pos?.y, + remoteUser: socket.id, + roomId: roomId, + }; + socket?.emit('tile-drag', socketDragTile); + socket?.emit('cursor', cursorPos); + } } }; - + return { handleMouseMove, handleDragOver, @@ -252,7 +260,6 @@ export const useMouse = () => { handleWheel, toggleCategory, handleMouseEnL, - setClickedTile, updateTilePosition, setActiveDragElement, }; diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index 0341fc3..a11f716 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -30,8 +30,8 @@ export type CursorData = { export type Tile = { x: number; y: number; - id: string; - _id?: string; + id?: string; + _id: string; src: string; name: string; color: string; From ea70f32537be0a3cdc59fbfae4ff78450aff85d5 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Thu, 26 Jan 2023 21:58:14 +0100 Subject: [PATCH 08/44] added Line to connect tiles --- frontend/src/components/Tiles/Tile.tsx | 45 ++++++++++++++++++++------ frontend/src/hooks/useMouse.tsx | 1 - frontend/src/json/kacheln.json | 12 +++---- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/Tiles/Tile.tsx b/frontend/src/components/Tiles/Tile.tsx index e3a8521..6aa6321 100644 --- a/frontend/src/components/Tiles/Tile.tsx +++ b/frontend/src/components/Tiles/Tile.tsx @@ -4,7 +4,7 @@ import { Tile as TileProps, Coordinates } from '../../types'; import { useMouse } from '../../hooks/useMouse'; import TileBorderAnchors from './TileBorderAnchors'; import { Group as GroupType } from 'konva/lib/Group'; -import { Group, Text, Line } from 'react-konva'; +import { Group, Text, Line, Rect } from 'react-konva'; import { useContextMenu } from '../../hooks/useContextMenu'; import { useWebSocketState } from '../../state/WebSocketState'; import { KonvaEventObject } from 'konva/lib/Node'; @@ -31,7 +31,7 @@ const Tile: React.FC = ({ const tilesOnBoard = useBoardState((state) => state.tilesOnBoard); const [connectionPreview, setConnectionPreview] = React.useState(null); const [connections, setConnections] = React.useState< - { source: string; destination: Coordinates }[] + { source: string; destination: TileProps }[] >([]); const userColor = useWebSocketState( (state) => state.room?.users?.find((user) => user.userId === state.socket?.id)?.color, @@ -49,13 +49,20 @@ const Tile: React.FC = ({ }; const hasInterSection = (position: Coordinates, tilePosition: Coordinates) => { - return !(tilePosition.x > position.x || tilePosition.y > position.y); + return !( + ( + tilePosition.x > position.x || + // tilePosition.x + groupSize.width > position.x || + tilePosition.y > position.y + ) + // ||tilePosition.y + groupSize.height > position.y + ); }; const detectConnection = (position: Coordinates) => { const intersectingTile: TileProps | undefined = tilesOnBoard.find((tile) => { const tilePosition = { x: tile.x, y: tile.y }; - return hasInterSection(position, tilePosition); + return id !== tile.id && hasInterSection(position, tilePosition); }); if (intersectingTile) { console.log('intersectingTile: ', intersectingTile); @@ -92,7 +99,7 @@ const Tile: React.FC = ({ x={position.x} y={position.y} points={createConnectionPoints({ x: 0, y: 0 }, mousePos)} - stroke='green' + stroke='#1E7D73' strokeWidth={4} />, ); @@ -106,14 +113,33 @@ const Tile: React.FC = ({ if (pointerPosition) { const intersectingTile = detectConnection(pointerPosition); if (intersectingTile !== null) { - setConnections((prev) => [ - ...prev, - { source: id, destination: { x: intersectingTile.x, y: intersectingTile.y } }, - ]); + setConnections([...connections, { source: id, destination: intersectingTile }]); } } }; + const connectionObjs = connections.map((connection) => { + const fromTile = tilesOnBoard.find((tile) => tile.id === connection.source); + const toTile = tilesOnBoard.find((tile) => tile.id === connection.destination.id); + if (fromTile && toTile) { + const lineEnd = { + x: toTile.x - fromTile.x, + y: toTile.y - fromTile.y, + }; + const points = createConnectionPoints({ x: 0, y: 0 }, lineEnd); + return ( + + ); + } + }); + const handleClick = (event: KonvaEventObject) => { setShowBoarder(!showBorder); if (tileRef.current) { @@ -185,6 +211,7 @@ const Tile: React.FC = ({ )} {connectionPreview} + {connectionObjs} ); }; diff --git a/frontend/src/hooks/useMouse.tsx b/frontend/src/hooks/useMouse.tsx index b4e87d8..48b1146 100644 --- a/frontend/src/hooks/useMouse.tsx +++ b/frontend/src/hooks/useMouse.tsx @@ -93,7 +93,6 @@ export const useMouse = () => { // add Tile to stage event.preventDefault(); const draggedData = event.dataTransfer.getData('dragStart/Tile'); - console.log(draggedData); if (draggedData && stageRef.current != null) { stageRef.current.setPointersPositions(event); const { x, y } = stageRef.current.getRelativePointerPosition(); diff --git a/frontend/src/json/kacheln.json b/frontend/src/json/kacheln.json index b64d706..af7c573 100644 --- a/frontend/src/json/kacheln.json +++ b/frontend/src/json/kacheln.json @@ -4,7 +4,7 @@ "category": "Start", "name": "Wenn", "src": "http://localhost:9001/uploads/1674421410153.png", - "points": [0, -200, 200, -200, 100, -50, 200, 100, 0, 100], + "points": [0, 0, 200, 0, 100, 150, 200, 300, 0, 300], "color": "#f9b43d", "textPosition": { "x": 50, @@ -18,7 +18,7 @@ "category": "Ende", "name": " ", "src": "http://localhost:9001/uploads/1674422062486.png", - "points": [-200, -250, 0, -250, 0, 50, -200, 50, -100, -100], + "points": [0, 0, 200, 0, 200, 300, 0, 300, 100, 150], "color": "#f9b43d", "textPosition": { "x": 50, @@ -32,7 +32,7 @@ "category": "Objekte", "name": "Smarte Lampe", "src": "http://localhost:9001/uploads/1674425498259.png", - "points": [0, -100, 200, -100, 300, 50, 200, 200, 0, 200, -100, 50], + "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], "color": "#EB555B", "textPosition": { "x": 50, @@ -46,7 +46,7 @@ "category": "Zustand", "name": "an", "src": "http://localhost:9001/uploads/1674423645286.png", - "points": [-200, -250, 0, -250, 100, -100, 0, 50, -200, 50, -100, -100], + "points": [0, 0, 200, 0, 300, 150, 200, 300, 0, 300, 100, 150], "color": "#F4AECE", "textPosition": { "x": 50, @@ -60,7 +60,7 @@ "category": "Konditionen", "name": "Dann", "src": "http://localhost:9001/uploads/1674424309409.png", - "points": [-100, -100, -200, 50, 200, 50, 100, -100], + "points": [0, 0, 200, 0, 300, 150, -100, 150], "color": "#F9B43D", "textPosition": { "x": -50, @@ -74,7 +74,7 @@ "category": "Konditionen", "name": "Und", "src": "http://localhost:9001/uploads/1674425020844.png", - "points": [-100, -50, 0, -200, 100, -200, 200, -50, 200, 50, 100, 200, 0, 200, -100, 50], + "points": [0, 0, 150, 0, 250, 150, 250, 250, 150, 400, 0, 400, -100, 250, -100, 150], "color": "#E2E7DF", "textPosition": { "x": -50, From 3626bb14fc0d008f99f1aa8d4484b98d2bed8720 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Thu, 26 Jan 2023 22:52:39 +0100 Subject: [PATCH 09/44] abstracted away functions --- frontend/src/components/Tiles/Tile.tsx | 94 +++----------- frontend/src/components/Tiles/TileBorder.tsx | 5 +- .../components/Tiles/TileBorderAnchors.tsx | 4 +- frontend/src/hooks/useAnchor.tsx | 24 ++-- frontend/src/hooks/useLine.tsx | 120 ++++++++++++++++++ 5 files changed, 153 insertions(+), 94 deletions(-) create mode 100644 frontend/src/hooks/useLine.tsx diff --git a/frontend/src/components/Tiles/Tile.tsx b/frontend/src/components/Tiles/Tile.tsx index 6aa6321..77a4205 100644 --- a/frontend/src/components/Tiles/Tile.tsx +++ b/frontend/src/components/Tiles/Tile.tsx @@ -1,15 +1,16 @@ import React from 'react'; import TileBorder from './TileBorder'; -import { Tile as TileProps, Coordinates } from '../../types'; +import useAnchor from '../../hooks/useAnchor'; +import { useLine } from '../../hooks/useLine'; +import { Tile as TileProps } from '../../types'; import { useMouse } from '../../hooks/useMouse'; +import { Group, Text, Line } from 'react-konva'; +import { KonvaEventObject } from 'konva/lib/Node'; import TileBorderAnchors from './TileBorderAnchors'; import { Group as GroupType } from 'konva/lib/Group'; -import { Group, Text, Line, Rect } from 'react-konva'; +import { useBoardState } from '../../state/BoardState'; import { useContextMenu } from '../../hooks/useContextMenu'; import { useWebSocketState } from '../../state/WebSocketState'; -import { KonvaEventObject } from 'konva/lib/Node'; -import useAnchor from '../../hooks/useAnchor'; -import { useBoardState } from '../../state/BoardState'; const Tile: React.FC = ({ x, @@ -24,6 +25,7 @@ const Tile: React.FC = ({ textPosition, }) => { const { getAnchorPoints } = useAnchor(); + const [showBorder, setShowBoarder] = React.useState(false); const tileRef = React.useRef(null); const { handleContextMenu } = useContextMenu(); @@ -48,75 +50,7 @@ const Tile: React.FC = ({ return [source.x, source.y, destination.x, destination.y]; }; - const hasInterSection = (position: Coordinates, tilePosition: Coordinates) => { - return !( - ( - tilePosition.x > position.x || - // tilePosition.x + groupSize.width > position.x || - tilePosition.y > position.y - ) - // ||tilePosition.y + groupSize.height > position.y - ); - }; - - const detectConnection = (position: Coordinates) => { - const intersectingTile: TileProps | undefined = tilesOnBoard.find((tile) => { - const tilePosition = { x: tile.x, y: tile.y }; - return id !== tile.id && hasInterSection(position, tilePosition); - }); - if (intersectingTile) { - console.log('intersectingTile: ', intersectingTile); - return intersectingTile; - } - console.log('no interesction'); - return null; - }; - - const handleAnchorDragStart = (e: KonvaEventObject) => { - const position = e.target.position(); - setConnectionPreview( - , - ); - }; - - const handleAnchorDragMove = (e: KonvaEventObject) => { - const position = e.target.position(); - const stage = e.target.getStage(); - const pointerPosition = stage?.getPointerPosition(); - if (pointerPosition) { - const mousePos = { - x: pointerPosition.x - position.x, - y: pointerPosition.y - position.y, - }; - setConnectionPreview( - , - ); - } - }; - - const handleAnchorDragEnd = (e: KonvaEventObject, id: string) => { - setConnectionPreview(null); - const stage = e.target.getStage(); - const pointerPosition = stage?.getPointerPosition(); - if (pointerPosition) { - const intersectingTile = detectConnection(pointerPosition); - if (intersectingTile !== null) { - setConnections([...connections, { source: id, destination: intersectingTile }]); - } - } - }; + const { handleAnchorDragStart, handleAnchorDragMove, handleAnchorDragEnd } = useLine(); const connectionObjs = connections.map((connection) => { const fromTile = tilesOnBoard.find((tile) => tile.id === connection.source); @@ -166,9 +100,15 @@ const Tile: React.FC = ({ x={point.x} y={point.y} key={index} - dragStart={handleAnchorDragStart} - dragMove={handleAnchorDragMove} - dragEnd={handleAnchorDragEnd} + dragStart={(e) => + handleAnchorDragStart(e, createConnectionPoints, setConnectionPreview) + } + dragMove={(e) => + handleAnchorDragMove(e, createConnectionPoints, setConnectionPreview) + } + dragEnd={(e) => + handleAnchorDragEnd(e, id, connections, setConnections, setConnectionPreview) + } /> ))} diff --git a/frontend/src/components/Tiles/TileBorder.tsx b/frontend/src/components/Tiles/TileBorder.tsx index 2d81f03..6065f46 100644 --- a/frontend/src/components/Tiles/TileBorder.tsx +++ b/frontend/src/components/Tiles/TileBorder.tsx @@ -9,8 +9,7 @@ type Props = { }; const TileBorder: React.FC = ({ x, y, id, points }) => { - const SIZE = 50; - const defaultPoints = [0, 0, SIZE, 0, SIZE, SIZE, 0, SIZE, 0, 0]; + const defaultPoints = [0, 0, 100, 0, 100, 100, 0, 100, 0, 0]; return ( = ({ x, y, id, points }) => { x={x} y={y} points={points || defaultPoints} - stroke='green' + stroke='black' strokeWidth={12} perfectDrawEnabled={false} closed diff --git a/frontend/src/components/Tiles/TileBorderAnchors.tsx b/frontend/src/components/Tiles/TileBorderAnchors.tsx index d5c0c4f..bee6071 100644 --- a/frontend/src/components/Tiles/TileBorderAnchors.tsx +++ b/frontend/src/components/Tiles/TileBorderAnchors.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Circle, Line } from 'react-konva'; +import { Circle } from 'react-konva'; import { Circle as CircleObject } from 'konva/lib/shapes/Circle'; import { KonvaEventObject } from 'konva/lib/Node'; @@ -31,7 +31,7 @@ const TileBorderAnchors: React.FC = ({ x, y, id, dragStart, dragMove, dra y={y} id={id} radius={10} - fill='green' + fill='black' draggable onDragStart={dragStart} onDragMove={dragMove} diff --git a/frontend/src/hooks/useAnchor.tsx b/frontend/src/hooks/useAnchor.tsx index 9201628..de9f4f0 100644 --- a/frontend/src/hooks/useAnchor.tsx +++ b/frontend/src/hooks/useAnchor.tsx @@ -17,35 +17,35 @@ const useAnchor = () => { ) => { switch (category) { case 'Start': - return [{ x: x + groupSize.width / 2 + 50, y: y - 50 }]; + return [{ x: x + groupSize.width / 2 + 50, y: y + 150 }]; break; case 'Ende': - return [{ x: x - groupSize.width / 2, y: y - 100 }]; + return [{ x: x - groupSize.width / 6 + 80, y: y + 150 }]; break; case 'Objekte': return [ - { x: x - groupSize.width / 5 + 10, y: y - 50 }, - { x: x + groupSize.width - 130, y: y - 50 }, + { x: x - groupSize.width / 5 + 10, y: y + 50 }, + { x: x + groupSize.width - 130, y: y + 50 }, ]; break; case 'Zustand': return [ - { x: x - groupSize.width / 2 - 10, y: y - 100 }, - { x: x + groupSize.width / 2, y: y - 100 }, + { x: x - groupSize.width / 6 + 80, y: y + 150 }, + { x: x + groupSize.width + 50, y: y + 150 }, ]; case 'Konditionen': if (tileName === 'Dann') { return [ - { x: x - groupSize.width / 2, y: y - 50 }, - { x: x + groupSize.width / 2, y: y - 50 }, + { x: x - groupSize.width / 2 + 100, y: y + 50 }, + { x: x + groupSize.width / 2 + 100, y: y + 50 }, ]; } else { if (tileName === 'Und') { return [ - { x: x + groupSize.width - 400, y: y - 150 }, - { x: x + groupSize.width / 2 + 40, y: y - 150 }, - { x: x + groupSize.width / 2 + 40, y: y + 150 }, - { x: x + groupSize.width - 400, y: y + 150 }, + { x: x + groupSize.width - 440, y: y + 50 }, + { x: x + groupSize.width / 2 + 50, y: y + 50 }, + { x: x + groupSize.width / 2 + 40, y: y + 350 }, + { x: x + groupSize.width - 440, y: y + 350 }, ]; } } diff --git a/frontend/src/hooks/useLine.tsx b/frontend/src/hooks/useLine.tsx new file mode 100644 index 0000000..49fdb8b --- /dev/null +++ b/frontend/src/hooks/useLine.tsx @@ -0,0 +1,120 @@ +import { KonvaEventObject } from 'konva/lib/Node'; +import React from 'react'; +import { Line } from 'react-konva'; +import { useBoardState } from '../state/BoardState'; +import { Coordinates, Tile as TileProps } from '../types'; + +export const useLine = () => { + const tilesOnBoard = useBoardState((state) => state.tilesOnBoard); + const hasInterSection = (position: Coordinates, tilePosition: Coordinates) => { + return !(tilePosition.x > position.x || tilePosition.y > position.y); + }; + + const detectConnection = (position: Coordinates, id: string) => { + const intersectingTile: TileProps | undefined = tilesOnBoard.find((tile) => { + const tilePosition = { x: tile.x, y: tile.y }; + return id !== tile.id && hasInterSection(position, tilePosition); + }); + if (intersectingTile) { + console.log('intersectingTile: ', intersectingTile); + return intersectingTile; + } + console.log('no interesction'); + return null; + }; + + const handleAnchorDragStart = ( + e: KonvaEventObject, + createConnectionPoints: ( + source: { + x: number; + y: number; + }, + destination: { + x: number; + y: number; + }, + ) => number[], + setConnectionPreview: React.Dispatch>, + ) => { + const position = e.target.position(); + setConnectionPreview( + , + ); + }; + + const handleAnchorDragMove = ( + e: KonvaEventObject, + createConnectionPoints: ( + source: { + x: number; + y: number; + }, + destination: { + x: number; + y: number; + }, + ) => number[], + setConnectionPreview: React.Dispatch>, + ) => { + const position = e.target.position(); + const stage = e.target.getStage(); + const pointerPosition = stage?.getPointerPosition(); + if (pointerPosition) { + const mousePos = { + x: pointerPosition.x - position.x, + y: pointerPosition.y - position.y, + }; + setConnectionPreview( + , + ); + } + }; + + const handleAnchorDragEnd = ( + e: KonvaEventObject, + id: string, + connections: { + source: string; + destination: TileProps; + }[], + setConnections: React.Dispatch< + React.SetStateAction< + { + source: string; + destination: TileProps; + }[] + > + >, + setConnectionPreview: React.Dispatch>, + ) => { + setConnectionPreview(null); + const stage = e.target.getStage(); + const pointerPosition = stage?.getPointerPosition(); + if (pointerPosition) { + const intersectingTile = detectConnection(pointerPosition, id); + if (intersectingTile !== null) { + setConnections([...connections, { source: id, destination: intersectingTile }]); + } + } + }; + + return { + detectConnection, + handleAnchorDragStart, + handleAnchorDragMove, + handleAnchorDragEnd, + }; +}; From 6ef4c25bd396f22f6f75ceda23ccf8c8466135f8 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Fri, 27 Jan 2023 01:46:53 +0100 Subject: [PATCH 10/44] added form to change Lamp Type --- frontend/src/components/Board/Board.tsx | 15 +++- .../ContextMenus/MenuOptions/RemoveTile.tsx | 18 +++++ .../ContextMenus/MenuOptions/SetLamp.tsx | 21 ++++++ .../ContextMenus/RightClickMenu.tsx | 27 +++++--- .../src/components/Forms/InfoComponent.tsx | 2 +- .../src/components/Forms/SelectLampForm.tsx | 69 +++++++++++++++++++ frontend/src/hooks/useMouse.tsx | 17 +++-- frontend/src/pages/CanvasPage.tsx | 2 + frontend/src/state/ContextMenuState.tsx | 6 +- 9 files changed, 156 insertions(+), 21 deletions(-) create mode 100644 frontend/src/components/ContextMenus/MenuOptions/RemoveTile.tsx create mode 100644 frontend/src/components/ContextMenus/MenuOptions/SetLamp.tsx create mode 100644 frontend/src/components/Forms/SelectLampForm.tsx diff --git a/frontend/src/components/Board/Board.tsx b/frontend/src/components/Board/Board.tsx index df621f0..c651f3c 100644 --- a/frontend/src/components/Board/Board.tsx +++ b/frontend/src/components/Board/Board.tsx @@ -11,6 +11,7 @@ import useWindowDimensions from '../../hooks/useWindowDimensions'; import { useWebSocketState } from '../../state/WebSocketState'; import { SocketDragTile, RoomData } from '../../types'; + // Main Stage Component that holds the Canvas. Scales based on the window size. const Board = () => { @@ -19,7 +20,14 @@ const Board = () => { const { height, width } = useWindowDimensions(); const { gridComponents } = useGrid({ stageRef, gridLayer }); - const { handleDragOver, handleDrop, handleWheel, handleMouseMove, toggleCategory } = useMouse(); + const { + handleDragOver, + handleDrop, + handleWheel, + handleMouseMove, + toggleCategory, + handleBoardDrag, + } = useMouse(); const addTile = useBoardState((state) => state.addTile); const updateTile = useBoardState((state) => state.updateTile); const deleteTile = useBoardState((state) => state.removeTile); @@ -28,7 +36,7 @@ const Board = () => { const setRoom = useWebSocketState((state) => state.setRoom); const room = useWebSocketState((state) => state.room); const setRemoteDragColor = useBoardState((state) => state.setRemoteDragColor); - const selectedTile = useBoardState((state) => state.selectedTile); + setStageReference(stageRef); useEffect(() => { @@ -65,6 +73,7 @@ const Board = () => { height={height} ref={stageRef} draggable + onDragStart={handleBoardDrag} onWheel={(e) => handleWheel(e)} > @@ -93,4 +102,4 @@ const Board = () => { ); }; -export default Board; +export default Board; \ No newline at end of file diff --git a/frontend/src/components/ContextMenus/MenuOptions/RemoveTile.tsx b/frontend/src/components/ContextMenus/MenuOptions/RemoveTile.tsx new file mode 100644 index 0000000..afb226b --- /dev/null +++ b/frontend/src/components/ContextMenus/MenuOptions/RemoveTile.tsx @@ -0,0 +1,18 @@ +import { faTrash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import React from 'react'; + +type Props = { + onclick: () => void; +}; + +const RemoveTile: React.FC = ({ onclick }) => { + return ( +
  • +

    Baustein entfernen

    + +
  • + ); +}; + +export default RemoveTile; diff --git a/frontend/src/components/ContextMenus/MenuOptions/SetLamp.tsx b/frontend/src/components/ContextMenus/MenuOptions/SetLamp.tsx new file mode 100644 index 0000000..2e372a8 --- /dev/null +++ b/frontend/src/components/ContextMenus/MenuOptions/SetLamp.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { faLightbulb } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useContextMenuState } from '../../../state/ContextMenuState'; + +type Props = { + onclick: () => void; +}; + +const SetLamp: React.FC = ({ onclick }) => { + + + return ( +
  • +

    Lampe auswählen

    + +
  • + ); +}; + +export default SetLamp; diff --git a/frontend/src/components/ContextMenus/RightClickMenu.tsx b/frontend/src/components/ContextMenus/RightClickMenu.tsx index 4a25a7b..3978e16 100644 --- a/frontend/src/components/ContextMenus/RightClickMenu.tsx +++ b/frontend/src/components/ContextMenus/RightClickMenu.tsx @@ -1,12 +1,17 @@ import React from 'react'; +import SetLamp from './MenuOptions/SetLamp'; +import RemoveTile from './MenuOptions/RemoveTile'; import { useBoardState } from '../../state/BoardState'; +import { faXmark } from '@fortawesome/free-solid-svg-icons'; import { useContextMenu } from '../../hooks/useContextMenu'; -import { faTrashCan } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useContextMenuState } from '../../state/ContextMenuState'; const RightClickMenu = () => { - const { contextMenuAnchorPoint, handleClick } = useContextMenu(); const removeTile = useBoardState((state) => state.removeTile); + const { contextMenuAnchorPoint, handleClick } = useContextMenu(); + const setPanelOpen = useContextMenuState((state) => state.setPanelOpen); + const setContextMenuOpen = useContextMenuState((state) => state.setContextMenuOpen); const handleRemoveTile = () => { removeTile(contextMenuAnchorPoint.id); @@ -15,19 +20,23 @@ const RightClickMenu = () => { return (
    -
      -
    • -

      Remove Tile

      - -
    • +
      + setContextMenuOpen(false)} + icon={faXmark} + /> +
      +
        + + setPanelOpen(true)} />
    ); diff --git a/frontend/src/components/Forms/InfoComponent.tsx b/frontend/src/components/Forms/InfoComponent.tsx index ea960e1..a069fe8 100644 --- a/frontend/src/components/Forms/InfoComponent.tsx +++ b/frontend/src/components/Forms/InfoComponent.tsx @@ -18,7 +18,7 @@ const InfoComponent = () => { }; // Component that displays the connected users and Disconnect Button return ( -
    +
    {users?.map((user) => ( diff --git a/frontend/src/components/Forms/SelectLampForm.tsx b/frontend/src/components/Forms/SelectLampForm.tsx new file mode 100644 index 0000000..70558b5 --- /dev/null +++ b/frontend/src/components/Forms/SelectLampForm.tsx @@ -0,0 +1,69 @@ +import { faXmark } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import React from 'react'; +import { useContextMenuState } from '../../state/ContextMenuState'; +import Tile from '../Tiles/Tile'; + +const SelectLampForm = () => { + const [selectedLamp, setSelectedLamp] = React.useState(''); + const lamps = ['Floor Lamp', 'Table Lamp', 'Desk Lamp', 'Ceiling Lamp', 'Bedside Lamp']; + const panelOpen = useContextMenuState((state) => state.panelOpen); + const setPanelOpen = useContextMenuState((state) => state.setPanelOpen); + + const closePanel = () => { + if (panelOpen) setPanelOpen(false); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + }; + + return ( + <> + {panelOpen === true && ( +
    + +
    +
    +

    Wähle eine Lampe aus

    +
    + +
    + + + +
    +
    +
    + +
    +
    +
    +
    + )} + + ); +}; + +export default SelectLampForm; diff --git a/frontend/src/hooks/useMouse.tsx b/frontend/src/hooks/useMouse.tsx index 48b1146..6263e3b 100644 --- a/frontend/src/hooks/useMouse.tsx +++ b/frontend/src/hooks/useMouse.tsx @@ -5,6 +5,7 @@ import { Group } from 'konva/lib/Group'; import { KonvaEventObject } from 'konva/lib/Node'; import { useBoardState } from '../state/BoardState'; import { useWebSocketState } from '../state/WebSocketState'; +import { useContextMenuState } from '../state/ContextMenuState'; export const useMouse = () => { const setTiles = useBoardState((state) => state.addTile); @@ -13,8 +14,8 @@ export const useMouse = () => { const updateTile = useBoardState((state) => state.updateTile); const stageRef = useBoardState((state) => state.stageReference); const roomId = useWebSocketState((state) => state.room?.roomId); - const tilesOnBoard = useBoardState((state) => state.tilesOnBoard); - const setSelectedTile = useBoardState((state) => state.setSelectedTile); + const contextMenuOpen = useContextMenuState((state) => state.contextMenuOpen); + const setContextMenuOpen = useContextMenuState((state) => state.setContextMenuOpen); const setActiveDragTile = useBoardState((state) => state.setActiveDragTile); const setCategoriesOpen = useBoardState((state) => state.setCategoriesOpen); const userColor = useWebSocketState( @@ -22,7 +23,6 @@ export const useMouse = () => { ); const toggleCategory = () => { - setSelectedTile(null); setCategoriesOpen(false); }; @@ -45,8 +45,6 @@ export const useMouse = () => { } }; - - const handleMouseEnL = ( event: KonvaEventObject, isClicked: boolean, @@ -250,8 +248,15 @@ export const useMouse = () => { } } }; - + + const handleBoardDrag = () => { + if (contextMenuOpen === true) { + setContextMenuOpen(false); + } + }; + return { + handleBoardDrag, handleMouseMove, handleDragOver, handleDragStart, diff --git a/frontend/src/pages/CanvasPage.tsx b/frontend/src/pages/CanvasPage.tsx index 3cdf012..d8b5dcb 100644 --- a/frontend/src/pages/CanvasPage.tsx +++ b/frontend/src/pages/CanvasPage.tsx @@ -5,6 +5,7 @@ import RightClickMenu from '../components/ContextMenus/RightClickMenu'; import { useContextMenuState } from '../state/ContextMenuState'; import InfoComponent from '../components/Forms/InfoComponent'; import { useWindowFocus } from '../hooks/useWindowFocus'; +import SelectLampForm from '../components/Forms/SelectLampForm'; const CanvasPage = () => { // Add Cursor here. const socket = useWebSocketState((state) => state.socket); @@ -17,6 +18,7 @@ const CanvasPage = () => { + ); }; diff --git a/frontend/src/state/ContextMenuState.tsx b/frontend/src/state/ContextMenuState.tsx index e307709..ef747ae 100644 --- a/frontend/src/state/ContextMenuState.tsx +++ b/frontend/src/state/ContextMenuState.tsx @@ -4,7 +4,8 @@ import { mountStoreDevtool } from 'simple-zustand-devtools'; export type ContextMenuStateType = { contextMenuOpen: boolean; contextMenuAnchorPoint: { x: number; y: number; id: string }; - + panelOpen: boolean; + setPanelOpen: (value: boolean) => void; setContextMenuOpen: (value: boolean) => void; setContextMenuAnchorPoint: (value: { x: number; y: number; id: string }) => void; }; @@ -12,7 +13,8 @@ export type ContextMenuStateType = { export const useContextMenuState = create((set) => ({ contextMenuOpen: false, contextMenuAnchorPoint: { x: 0, y: 0, id: '' }, - + panelOpen: false, + setPanelOpen: (value: boolean) => set(() => ({ panelOpen: value })), setContextMenuOpen: (value: boolean) => set(() => ({ contextMenuOpen: value })), setContextMenuAnchorPoint: (value: { x: number; y: number; id: string }) => set(() => ({ contextMenuAnchorPoint: value })), From 2cc7eb8cf8459976c3511321985fdb874d6c2279 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Fri, 27 Jan 2023 01:50:22 +0100 Subject: [PATCH 11/44] fine tuning --- frontend/src/components/ContextMenus/RightClickMenu.tsx | 7 ++++++- frontend/src/components/Forms/SelectLampForm.tsx | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/ContextMenus/RightClickMenu.tsx b/frontend/src/components/ContextMenus/RightClickMenu.tsx index 3978e16..023e760 100644 --- a/frontend/src/components/ContextMenus/RightClickMenu.tsx +++ b/frontend/src/components/ContextMenus/RightClickMenu.tsx @@ -18,6 +18,11 @@ const RightClickMenu = () => { handleClick(); }; + const handleSelectLamp = () => { + setPanelOpen(true); + setContextMenuOpen(false); + }; + return (
    {
      - setPanelOpen(true)} /> +
    ); diff --git a/frontend/src/components/Forms/SelectLampForm.tsx b/frontend/src/components/Forms/SelectLampForm.tsx index 70558b5..e41a08c 100644 --- a/frontend/src/components/Forms/SelectLampForm.tsx +++ b/frontend/src/components/Forms/SelectLampForm.tsx @@ -27,7 +27,7 @@ const SelectLampForm = () => { onClick={closePanel} icon={faXmark} /> -
    +

    Wähle eine Lampe aus

    @@ -54,7 +54,10 @@ const SelectLampForm = () => {
    -
    From da7fb714e556f6f038d838c82bceb330d8e47108 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Mon, 6 Feb 2023 15:25:20 +0100 Subject: [PATCH 12/44] add DevTools only in Development --- frontend/src/state/BoardState.tsx | 7 +++++-- frontend/src/state/ContextMenuState.tsx | 5 ++++- frontend/src/state/SyntaxTreeState.tsx | 23 +++++++++++++++++++++++ frontend/src/state/WebSocketState.tsx | 6 +++++- 4 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 frontend/src/state/SyntaxTreeState.tsx diff --git a/frontend/src/state/BoardState.tsx b/frontend/src/state/BoardState.tsx index 2d87fa5..cc1816f 100644 --- a/frontend/src/state/BoardState.tsx +++ b/frontend/src/state/BoardState.tsx @@ -61,5 +61,8 @@ export const useBoardState = create((set) => ({ set(() => ({ stageReference: stageRef })), })); -// :TODO: Remove this before production -mountStoreDevtool('BoardStore', useBoardState); + + +if (process.env.NODE_ENV === 'development') { + mountStoreDevtool('BoardStore', useBoardState); +} diff --git a/frontend/src/state/ContextMenuState.tsx b/frontend/src/state/ContextMenuState.tsx index ef747ae..98e8d17 100644 --- a/frontend/src/state/ContextMenuState.tsx +++ b/frontend/src/state/ContextMenuState.tsx @@ -20,4 +20,7 @@ export const useContextMenuState = create((set) => ({ set(() => ({ contextMenuAnchorPoint: value })), })); -mountStoreDevtool('ContextMenuState', useContextMenuState); + +if (process.env.NODE_ENV === 'development') { + mountStoreDevtool('ContextMenuState', useContextMenuState); +} diff --git a/frontend/src/state/SyntaxTreeState.tsx b/frontend/src/state/SyntaxTreeState.tsx new file mode 100644 index 0000000..0a2e719 --- /dev/null +++ b/frontend/src/state/SyntaxTreeState.tsx @@ -0,0 +1,23 @@ +// state that saves Connected Tiles Globally +// also saves the connections between them + +import create from 'zustand'; +import { mountStoreDevtool } from 'simple-zustand-devtools'; + +export type ConnectedTilesContextType = { + connectedTiles: { [key: string]: string[] }; + connectionPreview: JSX.Element | null; + setConnectionPreview: (value: JSX.Element | null) => void; + setConnectedTiles: (value: { [key: string]: string[] }) => void; +}; + +export const useConnectedTilesContext = create((set) => ({ + connectedTiles: {}, + connectionPreview: null, + setConnectionPreview: (value: JSX.Element | null) => set(() => ({ connectionPreview: value })), + setConnectedTiles: (value: { [key: string]: string[] }) => set(() => ({ connectedTiles: value })), +})); + +if (process.env.NODE_ENV === 'development') { + mountStoreDevtool('ConnectedTilesContext', useConnectedTilesContext); +} diff --git a/frontend/src/state/WebSocketState.tsx b/frontend/src/state/WebSocketState.tsx index 1f746be..8ca9deb 100644 --- a/frontend/src/state/WebSocketState.tsx +++ b/frontend/src/state/WebSocketState.tsx @@ -20,4 +20,8 @@ export const useWebSocketState = create((set) => ({ clearRoom: () => set(() => ({ room: null })), })); -mountStoreDevtool('WebSocketState', useWebSocketState); + +if (process.env.NODE_ENV === 'development') { + mountStoreDevtool('WebSocketState', useWebSocketState); +} + From 5e972aefd9bf7ab92e74c58d392c08c5dc85d1db Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Mon, 6 Feb 2023 15:26:03 +0100 Subject: [PATCH 13/44] refactored ConnectionPreview --- frontend/src/components/Board/Board.tsx | 12 ++++++---- frontend/src/components/Tiles/Tile.tsx | 31 +++++++------------------ frontend/src/hooks/useLine.tsx | 5 ++-- 3 files changed, 18 insertions(+), 30 deletions(-) diff --git a/frontend/src/components/Board/Board.tsx b/frontend/src/components/Board/Board.tsx index c651f3c..5228bce 100644 --- a/frontend/src/components/Board/Board.tsx +++ b/frontend/src/components/Board/Board.tsx @@ -10,7 +10,7 @@ import { useBoardState } from '../../state/BoardState'; import useWindowDimensions from '../../hooks/useWindowDimensions'; import { useWebSocketState } from '../../state/WebSocketState'; import { SocketDragTile, RoomData } from '../../types'; - +import { useConnectedTilesContext } from '../../state/SyntaxTreeState'; // Main Stage Component that holds the Canvas. Scales based on the window size. @@ -28,14 +28,15 @@ const Board = () => { toggleCategory, handleBoardDrag, } = useMouse(); + const room = useWebSocketState((state) => state.room); const addTile = useBoardState((state) => state.addTile); - const updateTile = useBoardState((state) => state.updateTile); - const deleteTile = useBoardState((state) => state.removeTile); - const setStageReference = useBoardState((state) => state.setStageReference); const socket = useWebSocketState((state) => state.socket); const setRoom = useWebSocketState((state) => state.setRoom); - const room = useWebSocketState((state) => state.room); + const deleteTile = useBoardState((state) => state.removeTile); + const updateTile = useBoardState((state) => state.updateTile); + const setStageReference = useBoardState((state) => state.setStageReference); const setRemoteDragColor = useBoardState((state) => state.setRemoteDragColor); + const connectionPreview = useConnectedTilesContext((state) => state.connectionPreview); setStageReference(stageRef); @@ -78,6 +79,7 @@ const Board = () => { > {gridComponents} + {connectionPreview} {room?.tiles?.map((tileObject) => ( = ({ textPosition, }) => { const { getAnchorPoints } = useAnchor(); - - const [showBorder, setShowBoarder] = React.useState(false); const tileRef = React.useRef(null); const { handleContextMenu } = useContextMenu(); - const { handleMouseEnL, updateTilePosition, setActiveDragElement } = useMouse(); + const { updateTilePosition, setActiveDragElement } = useMouse(); const tilesOnBoard = useBoardState((state) => state.tilesOnBoard); const [connectionPreview, setConnectionPreview] = React.useState(null); const [connections, setConnections] = React.useState< @@ -74,10 +71,9 @@ const Tile: React.FC = ({ } }); - const handleClick = (event: KonvaEventObject) => { - setShowBoarder(!showBorder); + React.useEffect(() => { if (tileRef.current) { - const rect = event.currentTarget.getClientRect({ + const rect = tileRef.current.getClientRect({ skipTransform: true, }); setGroupSize({ @@ -85,13 +81,13 @@ const Tile: React.FC = ({ height: rect.height, }); } - }; + }, []); return ( <> {userColor && ( <> - {showBorder && id && ( + {id && ( <> {getAnchorPoints(x, y, category, name, groupSize)?.map((point, index) => ( @@ -100,15 +96,9 @@ const Tile: React.FC = ({ x={point.x} y={point.y} key={index} - dragStart={(e) => - handleAnchorDragStart(e, createConnectionPoints, setConnectionPreview) - } - dragMove={(e) => - handleAnchorDragMove(e, createConnectionPoints, setConnectionPreview) - } - dragEnd={(e) => - handleAnchorDragEnd(e, id, connections, setConnections, setConnectionPreview) - } + dragStart={(e) => handleAnchorDragStart(e, createConnectionPoints)} + dragMove={(e) => handleAnchorDragMove(e, createConnectionPoints)} + dragEnd={(e) => handleAnchorDragEnd(e, id, connections, setConnections)} /> ))} @@ -126,11 +116,8 @@ const Tile: React.FC = ({ data-fill={color} onDragEnd={updateTilePosition} data-points={JSON.stringify(points)} - onClick={(event) => handleClick(event)} data-textPosition={JSON.stringify(textPosition)} onDragMove={(event) => setActiveDragElement(tileRef, event)} - onMouseOver={(event) => handleMouseEnL(event, showBorder, 4)} - onMouseLeave={(event) => handleMouseEnL(event, showBorder, 0)} > = ({ )} - {connectionPreview} + {connectionObjs} ); diff --git a/frontend/src/hooks/useLine.tsx b/frontend/src/hooks/useLine.tsx index 49fdb8b..86d2e8e 100644 --- a/frontend/src/hooks/useLine.tsx +++ b/frontend/src/hooks/useLine.tsx @@ -2,9 +2,11 @@ import { KonvaEventObject } from 'konva/lib/Node'; import React from 'react'; import { Line } from 'react-konva'; import { useBoardState } from '../state/BoardState'; +import { useConnectedTilesContext } from '../state/SyntaxTreeState'; import { Coordinates, Tile as TileProps } from '../types'; export const useLine = () => { + const setConnectionPreview = useConnectedTilesContext((state) => state.setConnectionPreview); const tilesOnBoard = useBoardState((state) => state.tilesOnBoard); const hasInterSection = (position: Coordinates, tilePosition: Coordinates) => { return !(tilePosition.x > position.x || tilePosition.y > position.y); @@ -35,7 +37,6 @@ export const useLine = () => { y: number; }, ) => number[], - setConnectionPreview: React.Dispatch>, ) => { const position = e.target.position(); setConnectionPreview( @@ -61,7 +62,6 @@ export const useLine = () => { y: number; }, ) => number[], - setConnectionPreview: React.Dispatch>, ) => { const position = e.target.position(); const stage = e.target.getStage(); @@ -98,7 +98,6 @@ export const useLine = () => { }[] > >, - setConnectionPreview: React.Dispatch>, ) => { setConnectionPreview(null); const stage = e.target.getStage(); From 43a49cdaefa1e31bc957a0bfe245d144e371cb6b Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Mon, 6 Feb 2023 16:56:54 +0100 Subject: [PATCH 14/44] compare connection based on achors --- frontend/src/components/Tiles/Tile.tsx | 13 +++++++++++-- frontend/src/hooks/useLine.tsx | 25 ++++++++++++++++--------- frontend/src/types.d.ts | 1 + 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/Tiles/Tile.tsx b/frontend/src/components/Tiles/Tile.tsx index 6ec93e3..edc596a 100644 --- a/frontend/src/components/Tiles/Tile.tsx +++ b/frontend/src/components/Tiles/Tile.tsx @@ -1,6 +1,5 @@ import React from 'react'; import TileBorder from './TileBorder'; -import useAnchor from '../../hooks/useAnchor'; import { useLine } from '../../hooks/useLine'; import { Tile as TileProps } from '../../types'; import { useMouse } from '../../hooks/useMouse'; @@ -10,6 +9,7 @@ import { Group as GroupType } from 'konva/lib/Group'; import { useBoardState } from '../../state/BoardState'; import { useContextMenu } from '../../hooks/useContextMenu'; import { useWebSocketState } from '../../state/WebSocketState'; +import useAnchor from '../../hooks/useAnchor'; const Tile: React.FC = ({ x, @@ -26,9 +26,9 @@ const Tile: React.FC = ({ const { getAnchorPoints } = useAnchor(); const tileRef = React.useRef(null); const { handleContextMenu } = useContextMenu(); + const updateTile = useBoardState((state) => state.updateTile); const { updateTilePosition, setActiveDragElement } = useMouse(); const tilesOnBoard = useBoardState((state) => state.tilesOnBoard); - const [connectionPreview, setConnectionPreview] = React.useState(null); const [connections, setConnections] = React.useState< { source: string; destination: TileProps }[] >([]); @@ -80,6 +80,15 @@ const Tile: React.FC = ({ width: rect.width, height: rect.height, }); + + const tile = tilesOnBoard.find((tile) => tile.id === id); + tile && + updateTile({ + ...tile, + anchors: getAnchorPoints(x, y, category, name, groupSize), + width: rect.width, + height: rect.height, + }); } }, []); diff --git a/frontend/src/hooks/useLine.tsx b/frontend/src/hooks/useLine.tsx index 86d2e8e..60062ee 100644 --- a/frontend/src/hooks/useLine.tsx +++ b/frontend/src/hooks/useLine.tsx @@ -8,20 +8,23 @@ import { Coordinates, Tile as TileProps } from '../types'; export const useLine = () => { const setConnectionPreview = useConnectedTilesContext((state) => state.setConnectionPreview); const tilesOnBoard = useBoardState((state) => state.tilesOnBoard); - const hasInterSection = (position: Coordinates, tilePosition: Coordinates) => { - return !(tilePosition.x > position.x || tilePosition.y > position.y); + const hasInterSection = (linePosition: Coordinates, tilePosition: Coordinates) => { + return tilePosition.x - linePosition.x < 50 && tilePosition.y - linePosition.y < 50; }; - const detectConnection = (position: Coordinates, id: string) => { + const detectConnection = (linePosition: Coordinates, id: string) => { const intersectingTile: TileProps | undefined = tilesOnBoard.find((tile) => { - const tilePosition = { x: tile.x, y: tile.y }; - return id !== tile.id && hasInterSection(position, tilePosition); + if (tile.anchors) { + const found = tile.anchors?.find((anchor) => { + const tilePosition = { x: Math.floor(anchor.x), y: Math.floor(anchor.y) }; + return id !== tile.id && hasInterSection(linePosition, tilePosition) === true && tile; + }); + if (found !== undefined) return tile; + } }); - if (intersectingTile) { - console.log('intersectingTile: ', intersectingTile); + if (intersectingTile !== undefined) { return intersectingTile; } - console.log('no interesction'); return null; }; @@ -103,7 +106,11 @@ export const useLine = () => { const stage = e.target.getStage(); const pointerPosition = stage?.getPointerPosition(); if (pointerPosition) { - const intersectingTile = detectConnection(pointerPosition, id); + const pos = { + x: Math.floor(pointerPosition.x), + y: Math.floor(pointerPosition.y), + }; + const intersectingTile = detectConnection(pos, id); if (intersectingTile !== null) { setConnections([...connections, { source: id, destination: intersectingTile }]); } diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index a11f716..5d16f6c 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -39,6 +39,7 @@ export type Tile = { height: number; points: number[]; category: string; + anchors?: { x: number; y: number }[]; textPosition: { x: number; y: number }; }; From c0f6e83aec77d2927c5188b9268dc85d3dfea9db Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Tue, 7 Feb 2023 14:17:47 +0100 Subject: [PATCH 15/44] refactor and add anchors in json --- backend/src/Controller/socketController.ts | 11 ++- backend/src/Controller/tileController.ts | 12 +-- backend/src/Model/tileModel.ts | 18 ++++ backend/types/socket.types.d.ts | 1 + frontend/src/components/Board/Board.tsx | 1 + frontend/src/components/Tiles/Tile.tsx | 95 ++++++++----------- ...BorderAnchors.tsx => TileBorderAnchor.tsx} | 6 +- frontend/src/hooks/useAnchor.tsx | 24 ++--- frontend/src/hooks/useLine.tsx | 25 +++-- frontend/src/hooks/useMouse.tsx | 86 +++++++++-------- frontend/src/json/kacheln.json | 84 ++++++++++++++++ frontend/src/types.d.ts | 2 +- 12 files changed, 237 insertions(+), 128 deletions(-) rename frontend/src/components/Tiles/{TileBorderAnchors.tsx => TileBorderAnchor.tsx} (93%) diff --git a/backend/src/Controller/socketController.ts b/backend/src/Controller/socketController.ts index a4b43f3..60e1401 100644 --- a/backend/src/Controller/socketController.ts +++ b/backend/src/Controller/socketController.ts @@ -72,16 +72,17 @@ export const tileDrop = ( if (room) { room.tiles.push({ tile: { - id: data.tile.id, - name: data.tile.name, - category: data.tile.category, - src: data.tile.src, x: data.tile.x, y: data.tile.y, + id: data.tile.id, + src: data.tile.src, + name: data.tile.name, width: data.tile.width, + color: data.tile.color, height: data.tile.height, points: data.tile.points, - color: data.tile.color, + anchors: data.tile.anchors, + category: data.tile.category, textPosition: data.tile.textPosition, }, }); diff --git a/backend/src/Controller/tileController.ts b/backend/src/Controller/tileController.ts index a63a407..ee96dab 100644 --- a/backend/src/Controller/tileController.ts +++ b/backend/src/Controller/tileController.ts @@ -86,15 +86,15 @@ export const updateTile = async (req: Request, res: Response) => { if (process.env.NODE_ENV === "production") { backendUrl = process.env.PROD_BACKEND_URL; } - tile.category = req.body.category ? req.body.category : tile.category; - tile.name = req.body.name ? req.body.name : tile.name; - tile.src = - req.file != undefined ? `${backendUrl}/${req.file.path}` : tile.src; - tile.points = req.body.points ? req.body.points : tile.points; - tile.color = req.body.color ? req.body.color : tile.color; tile.textPosition = req.body.textPosition ? req.body.textPosition : tile.textPosition; + tile.color = req.body.color ? req.body.color : tile.color; + tile.points = req.body.points ? req.body.points : tile.points; + tile.src = tile.name = req.body.name ? req.body.name : tile.name; + tile.anchors = req.body.anchors ? req.body.anchors : tile.anchors; + req.file != undefined ? `${backendUrl}/${req.file.path}` : tile.src; + tile.category = req.body.category ? req.body.category : tile.category; const updatedTile = await tile.save(); res.status(200).json(updatedTile); } else { diff --git a/backend/src/Model/tileModel.ts b/backend/src/Model/tileModel.ts index a5c3057..27d0c64 100644 --- a/backend/src/Model/tileModel.ts +++ b/backend/src/Model/tileModel.ts @@ -32,6 +32,24 @@ const TileSchema = new Schema({ type: String, required: true, }, + anchors: { + type: [ + { + type: String, + x: Number, + y: Number, + }, + ], + required: true, + }, + width: { + type: Number, + required: true, + }, + height: { + type: Number, + required: true, + }, textPosition: textPositionSchema, }); diff --git a/backend/types/socket.types.d.ts b/backend/types/socket.types.d.ts index 024b26f..30392be 100644 --- a/backend/types/socket.types.d.ts +++ b/backend/types/socket.types.d.ts @@ -27,6 +27,7 @@ export type NewTile = { category: string; points: number[]; textPosition: { x: number; y: number }; + anchors: { type: string; x: number; y: number }[]; }; export type TabFocusData = { diff --git a/frontend/src/components/Board/Board.tsx b/frontend/src/components/Board/Board.tsx index 5228bce..efe7ddc 100644 --- a/frontend/src/components/Board/Board.tsx +++ b/frontend/src/components/Board/Board.tsx @@ -89,6 +89,7 @@ const Board = () => { id={tileObject.tile.id} src={tileObject.tile.src} name={tileObject.tile.name} + anchors={tileObject.tile.anchors} color={tileObject.tile.color} width={tileObject.tile.width} height={tileObject.tile.height} diff --git a/frontend/src/components/Tiles/Tile.tsx b/frontend/src/components/Tiles/Tile.tsx index edc596a..d3c6800 100644 --- a/frontend/src/components/Tiles/Tile.tsx +++ b/frontend/src/components/Tiles/Tile.tsx @@ -3,13 +3,12 @@ import TileBorder from './TileBorder'; import { useLine } from '../../hooks/useLine'; import { Tile as TileProps } from '../../types'; import { useMouse } from '../../hooks/useMouse'; -import { Group, Text, Line } from 'react-konva'; -import TileBorderAnchors from './TileBorderAnchors'; +import { Group, Text, Line, Circle } from 'react-konva'; +import TileBorderAnchor from './TileBorderAnchor'; import { Group as GroupType } from 'konva/lib/Group'; import { useBoardState } from '../../state/BoardState'; import { useContextMenu } from '../../hooks/useContextMenu'; import { useWebSocketState } from '../../state/WebSocketState'; -import useAnchor from '../../hooks/useAnchor'; const Tile: React.FC = ({ x, @@ -19,26 +18,23 @@ const Tile: React.FC = ({ src, name, color, + width, + height, points, + anchors, category, textPosition, }) => { - const { getAnchorPoints } = useAnchor(); const tileRef = React.useRef(null); const { handleContextMenu } = useContextMenu(); - const updateTile = useBoardState((state) => state.updateTile); const { updateTilePosition, setActiveDragElement } = useMouse(); const tilesOnBoard = useBoardState((state) => state.tilesOnBoard); const [connections, setConnections] = React.useState< - { source: string; destination: TileProps }[] + { source: string; destination: TileProps; destinationAnchorType: string }[] >([]); const userColor = useWebSocketState( (state) => state.room?.users?.find((user) => user.userId === state.socket?.id)?.color, ); - const [groupSize, setGroupSize] = React.useState<{ width: number; height: number }>({ - width: 0, - height: 0, - }); const createConnectionPoints = ( source: { x: number; y: number }, @@ -53,77 +49,64 @@ const Tile: React.FC = ({ const fromTile = tilesOnBoard.find((tile) => tile.id === connection.source); const toTile = tilesOnBoard.find((tile) => tile.id === connection.destination.id); if (fromTile && toTile) { - const lineEnd = { - x: toTile.x - fromTile.x, - y: toTile.y - fromTile.y, - }; - const points = createConnectionPoints({ x: 0, y: 0 }, lineEnd); - return ( - + const toTileAnchor = toTile.anchors?.find( + (anchor) => anchor.type === connection.destinationAnchorType, ); + if (toTileAnchor) { + const lineEnd = { + x: toTileAnchor.x - fromTile.x, + y: toTileAnchor.y - fromTile.y, + }; + const points = createConnectionPoints({ x: 0, y: 0 }, lineEnd); + return ( + + ); + } } }); - - React.useEffect(() => { - if (tileRef.current) { - const rect = tileRef.current.getClientRect({ - skipTransform: true, - }); - setGroupSize({ - width: rect.width, - height: rect.height, - }); - - const tile = tilesOnBoard.find((tile) => tile.id === id); - tile && - updateTile({ - ...tile, - anchors: getAnchorPoints(x, y, category, name, groupSize), - width: rect.width, - height: rect.height, - }); - } - }, []); - return ( <> - {userColor && ( + {userColor && id && ( <> {id && ( <> - {getAnchorPoints(x, y, category, name, groupSize)?.map((point, index) => ( - ( + handleAnchorDragStart(e, createConnectionPoints)} + x={x + point.x} + y={y + point.y} + type={point.type} dragMove={(e) => handleAnchorDragMove(e, createConnectionPoints)} + dragStart={(e) => handleAnchorDragStart(e, createConnectionPoints)} dragEnd={(e) => handleAnchorDragEnd(e, id, connections, setConnections)} /> ))} )} + setActiveDragElement(tileRef, event)} @@ -131,10 +114,10 @@ const Tile: React.FC = ({ ) => void; dragMove: (e: KonvaEventObject) => void; dragEnd: (e: KonvaEventObject, id: string) => void; }; -const TileBorderAnchors: React.FC = ({ x, y, id, dragStart, dragMove, dragEnd }) => { +const TileBorderAnchors: React.FC = ({ x, y, id, dragStart, dragMove, dragEnd, type }) => { const dragBounds = (ref: React.RefObject) => { if (ref.current !== null) { return ref.current.getAbsolutePosition(); @@ -30,9 +31,10 @@ const TileBorderAnchors: React.FC = ({ x, y, id, dragStart, dragMove, dra x={x} y={y} id={id} + draggable radius={10} fill='black' - draggable + data-type={type} onDragStart={dragStart} onDragMove={dragMove} onDragEnd={(event) => dragEnd(event, id)} diff --git a/frontend/src/hooks/useAnchor.tsx b/frontend/src/hooks/useAnchor.tsx index de9f4f0..c1846c2 100644 --- a/frontend/src/hooks/useAnchor.tsx +++ b/frontend/src/hooks/useAnchor.tsx @@ -17,35 +17,35 @@ const useAnchor = () => { ) => { switch (category) { case 'Start': - return [{ x: x + groupSize.width / 2 + 50, y: y + 150 }]; + return [{ type: 'L', x: x + groupSize.width / 2 + 50, y: y + 150 }]; break; case 'Ende': - return [{ x: x - groupSize.width / 6 + 80, y: y + 150 }]; + return [{ type: 'R', x: x - groupSize.width / 6 + 80, y: y + 150 }]; break; case 'Objekte': return [ - { x: x - groupSize.width / 5 + 10, y: y + 50 }, - { x: x + groupSize.width - 130, y: y + 50 }, + { type: 'L', x: x - groupSize.width / 5 + 10, y: y + 50 }, + { type: 'R', x: x + groupSize.width - 130, y: y + 50 }, ]; break; case 'Zustand': return [ - { x: x - groupSize.width / 6 + 80, y: y + 150 }, - { x: x + groupSize.width + 50, y: y + 150 }, + { type: 'L', x: x - groupSize.width / 6 + 80, y: y + 150 }, + { type: 'R', x: x + groupSize.width + 50, y: y + 150 }, ]; case 'Konditionen': if (tileName === 'Dann') { return [ - { x: x - groupSize.width / 2 + 100, y: y + 50 }, - { x: x + groupSize.width / 2 + 100, y: y + 50 }, + { type: 'L', x: x - groupSize.width / 2 + 100, y: y + 50 }, + { type: 'R', x: x + groupSize.width / 2 + 100, y: y + 50 }, ]; } else { if (tileName === 'Und') { return [ - { x: x + groupSize.width - 440, y: y + 50 }, - { x: x + groupSize.width / 2 + 50, y: y + 50 }, - { x: x + groupSize.width / 2 + 40, y: y + 350 }, - { x: x + groupSize.width - 440, y: y + 350 }, + { type: 'TL', x: x + groupSize.width - 440, y: y + 50 }, + { type: 'TR', x: x + groupSize.width / 2 + 50, y: y + 50 }, + { type: 'BL', x: x + groupSize.width / 2 + 40, y: y + 350 }, + { type: 'BR', x: x + groupSize.width - 440, y: y + 350 }, ]; } } diff --git a/frontend/src/hooks/useLine.tsx b/frontend/src/hooks/useLine.tsx index 60062ee..d686f93 100644 --- a/frontend/src/hooks/useLine.tsx +++ b/frontend/src/hooks/useLine.tsx @@ -13,17 +13,21 @@ export const useLine = () => { }; const detectConnection = (linePosition: Coordinates, id: string) => { + let foundAnchor = ''; const intersectingTile: TileProps | undefined = tilesOnBoard.find((tile) => { if (tile.anchors) { const found = tile.anchors?.find((anchor) => { - const tilePosition = { x: Math.floor(anchor.x), y: Math.floor(anchor.y) }; + const tilePosition = { x: anchor.x, y: anchor.y }; return id !== tile.id && hasInterSection(linePosition, tilePosition) === true && tile; }); - if (found !== undefined) return tile; + if (found !== undefined) { + foundAnchor = found.type; + return tile; + } } }); if (intersectingTile !== undefined) { - return intersectingTile; + return { intersectingTile, foundAnchor }; } return null; }; @@ -92,12 +96,14 @@ export const useLine = () => { connections: { source: string; destination: TileProps; + destinationAnchorType: string; }[], setConnections: React.Dispatch< React.SetStateAction< { source: string; destination: TileProps; + destinationAnchorType: string; }[] > >, @@ -107,12 +113,19 @@ export const useLine = () => { const pointerPosition = stage?.getPointerPosition(); if (pointerPosition) { const pos = { - x: Math.floor(pointerPosition.x), - y: Math.floor(pointerPosition.y), + x: pointerPosition.x, + y: pointerPosition.y, }; const intersectingTile = detectConnection(pos, id); if (intersectingTile !== null) { - setConnections([...connections, { source: id, destination: intersectingTile }]); + setConnections([ + ...connections, + { + source: id, + destination: intersectingTile.intersectingTile, + destinationAnchorType: intersectingTile.foundAnchor, + }, + ]); } } }; diff --git a/frontend/src/hooks/useMouse.tsx b/frontend/src/hooks/useMouse.tsx index 6263e3b..ce4e317 100644 --- a/frontend/src/hooks/useMouse.tsx +++ b/frontend/src/hooks/useMouse.tsx @@ -14,13 +14,13 @@ export const useMouse = () => { const updateTile = useBoardState((state) => state.updateTile); const stageRef = useBoardState((state) => state.stageReference); const roomId = useWebSocketState((state) => state.room?.roomId); - const contextMenuOpen = useContextMenuState((state) => state.contextMenuOpen); - const setContextMenuOpen = useContextMenuState((state) => state.setContextMenuOpen); const setActiveDragTile = useBoardState((state) => state.setActiveDragTile); const setCategoriesOpen = useBoardState((state) => state.setCategoriesOpen); + const contextMenuOpen = useContextMenuState((state) => state.contextMenuOpen); const userColor = useWebSocketState( (state) => state.room?.users.find((user) => user.userId === socket?.id)?.color, ); + const setContextMenuOpen = useContextMenuState((state) => state.setContextMenuOpen); const toggleCategory = () => { setCategoriesOpen(false); @@ -35,8 +35,8 @@ export const useMouse = () => { const cursorPos = { x: x, y: y, - remoteUser: socket.id, roomId: roomId, + remoteUser: socket.id, }; if (socket !== null) { socket?.emit('cursor', cursorPos); @@ -68,20 +68,20 @@ export const useMouse = () => { ); if (tile) { - const { _id, name, src, color, points, textPosition } = tile; + const { _id, name, src, color, points, textPosition, anchors, width, height } = tile; const dragPayload = JSON.stringify({ - nodeClass: event.currentTarget.getAttribute('data-class'), - offsetX: event.nativeEvent.offsetX, - offsetY: event.nativeEvent.offsetY, - clientWidth: event.currentTarget.clientWidth, - clientHeight: event.currentTarget.clientHeight, id: uuidv4(), _id: _id, src: src, name: name, color: color, + width: width, + height: height, points: points, + anchors: anchors, textPosition: textPosition, + offsetX: event.nativeEvent.offsetX, + offsetY: event.nativeEvent.offsetY, }); event.dataTransfer.setData('dragStart/Tile', dragPayload); } @@ -98,37 +98,39 @@ export const useMouse = () => { id, _id, src, - color, - textPosition, name, + color, + width, points, - nodeClass, + height, + anchors, offsetX, offsetY, - clientHeight, - clientWidth, + nodeClass, + textPosition, } = JSON.parse(draggedData); if (x && y) { const newTile: Tile = { id: id, _id: _id, src: src, - category: nodeClass, - x: x - (offsetX - clientWidth / 2), - y: y - (offsetY - clientHeight / 2), name: name, + width: width, color: color, + height: height, points: points, - width: clientWidth, - height: clientHeight, + anchors: anchors, + category: nodeClass, textPosition: textPosition, + x: x - (offsetX - width / 2), + y: y - (offsetY - height / 2), }; setTiles(newTile); if (socket !== null && roomId && userColor) { const socketDragTile: SocketDragTile = { - remoteUser: socket.id, tile: newTile, roomId: roomId, + remoteUser: socket.id, remoteUserColor: userColor, }; socket?.emit('tile-drop', socketDragTile); @@ -144,23 +146,26 @@ export const useMouse = () => { */ const { + 'data-id': _id, 'data-src': src, 'data-fill': color, - 'data-id': _id, + 'data-width': width, 'data-points': points, + 'data-height': height, + 'data-anchors': anchors, 'data-textPosition': textPosition, } = event.target.attrs; if (stageRef.current) { - const rect = event.currentTarget.getClientRect(); const updatedTile: Tile = { _id: _id, src: src, + width: width, color: color, + height: height, points: points, - width: rect.width, + anchors: anchors, x: event.target.x(), y: event.target.y(), - height: rect.height, id: event.target.attrs.id, textPosition: textPosition, name: event.target.attrs.name, @@ -203,48 +208,49 @@ export const useMouse = () => { event: KonvaEventObject, ) => { setActiveDragTile(activeTileReference); - if (stageRef.current) { - // TODO: fix missing attribute const { + 'data-id': _id, 'data-src': src, 'data-fill': color, - 'data-id': _id, + 'data-width': width, + 'data-height': height, 'data-points': points, + 'data-anchors': anchors, 'data-textPosition': textPosition, } = event.target.attrs; - const rect = event.currentTarget.getClientRect(); const stage = stageRef.current; const pos = stage.getRelativePointerPosition(); const updatedTile: Tile = { _id: _id, src: src, color: color, - points: Array.from(points), - width: rect.width, + width: width, + height: height, + anchors: anchors, x: event.target.x(), y: event.target.y(), - height: rect.height, id: event.target.attrs.id, - textPosition: JSON.parse(textPosition), + points: Array.from(points), name: event.target.attrs.name, category: event.target.attrs.name, + textPosition: JSON.parse(textPosition), }; if (socket !== null && roomId && userColor) { const socketDragTile: SocketDragTile = { - remoteUser: socket.id, - tile: updatedTile, roomId: roomId, + tile: updatedTile, + remoteUser: socket.id, remoteUserColor: userColor, }; const cursorPos = { x: pos?.x, y: pos?.y, - remoteUser: socket.id, roomId: roomId, + remoteUser: socket.id, }; - socket?.emit('tile-drag', socketDragTile); socket?.emit('cursor', cursorPos); + socket?.emit('tile-drag', socketDragTile); } } }; @@ -256,14 +262,14 @@ export const useMouse = () => { }; return { - handleBoardDrag, - handleMouseMove, - handleDragOver, - handleDragStart, handleDrop, handleWheel, toggleCategory, handleMouseEnL, + handleDragOver, + handleDragStart, + handleBoardDrag, + handleMouseMove, updateTilePosition, setActiveDragElement, }; diff --git a/frontend/src/json/kacheln.json b/frontend/src/json/kacheln.json index af7c573..f87bf8b 100644 --- a/frontend/src/json/kacheln.json +++ b/frontend/src/json/kacheln.json @@ -6,6 +6,15 @@ "src": "http://localhost:9001/uploads/1674421410153.png", "points": [0, 0, 200, 0, 100, 150, 200, 300, 0, 300], "color": "#f9b43d", + "width": 200, + "height": 300, + "anchors": [ + { + "type": "L", + "x": 150, + "y": 150 + } + ], "textPosition": { "x": 50, "y": 50, @@ -20,6 +29,15 @@ "src": "http://localhost:9001/uploads/1674422062486.png", "points": [0, 0, 200, 0, 200, 300, 0, 300, 100, 150], "color": "#f9b43d", + "width": 200, + "height": 300, + "anchors": [ + { + "type": "R", + "x": -115, + "y": 150 + } + ], "textPosition": { "x": 50, "y": 50, @@ -34,6 +52,20 @@ "src": "http://localhost:9001/uploads/1674425498259.png", "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], "color": "#EB555B", + "width": 400, + "height": 300, + "anchors": [ + { + "type": "L", + "x": -70, + "y": 450 + }, + { + "type": "R", + "x": 170, + "y": 450 + } + ], "textPosition": { "x": 50, "y": 50, @@ -48,6 +80,20 @@ "src": "http://localhost:9001/uploads/1674423645286.png", "points": [0, 0, 200, 0, 300, 150, 200, 300, 0, 300, 100, 150], "color": "#F4AECE", + "width": 300, + "height": 300, + "anchors": [ + { + "type": "L", + "x": -70, + "y": 250 + }, + { + "type": "R", + "x": 170, + "y": 250 + } + ], "textPosition": { "x": 50, "y": 50, @@ -62,6 +108,20 @@ "src": "http://localhost:9001/uploads/1674424309409.png", "points": [0, 0, 200, 0, 300, 150, -100, 150], "color": "#F9B43D", + "width": 400, + "height": 200, + "anchors": [ + { + "type": "L", + "x": -300, + "y": 250 + }, + { + "type": "R", + "x": 300, + "y": 250 + } + ], "textPosition": { "x": -50, "y": -50, @@ -76,6 +136,30 @@ "src": "http://localhost:9001/uploads/1674425020844.png", "points": [0, 0, 150, 0, 250, 150, 250, 250, 150, 400, 0, 400, -100, 250, -100, 150], "color": "#E2E7DF", + "width": 350, + "height": 450, + "anchors": [ + { + "type": "TL", + "x": -90, + "y": 500 + }, + { + "type": "TR", + "x": 225, + "y": 500 + }, + { + "type": "BL", + "x": 215, + "y": 800 + }, + { + "type": "BR", + "x": -90, + "y": 800 + } + ], "textPosition": { "x": -50, "y": -50, diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index 5d16f6c..b55b68d 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -39,8 +39,8 @@ export type Tile = { height: number; points: number[]; category: string; - anchors?: { x: number; y: number }[]; textPosition: { x: number; y: number }; + anchors: { type: string; x: number; y: number }[]; }; export type InnerObject = { From c6371494771e9463b1af5137b45b44b3d03d0640 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Tue, 7 Feb 2023 14:45:47 +0100 Subject: [PATCH 16/44] fixed anchor positions --- frontend/src/json/kacheln.json | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/frontend/src/json/kacheln.json b/frontend/src/json/kacheln.json index f87bf8b..bad9045 100644 --- a/frontend/src/json/kacheln.json +++ b/frontend/src/json/kacheln.json @@ -33,8 +33,8 @@ "height": 300, "anchors": [ { - "type": "R", - "x": -115, + "type": "L", + "x": 50, "y": 150 } ], @@ -58,12 +58,12 @@ { "type": "L", "x": -70, - "y": 450 + "y": 50 }, { "type": "R", - "x": 170, - "y": 450 + "x": 270, + "y": 50 } ], "textPosition": { @@ -85,13 +85,13 @@ "anchors": [ { "type": "L", - "x": -70, - "y": 250 + "x": -10, + "y": 150 }, { "type": "R", - "x": 170, - "y": 250 + "x": 350, + "y": 150 } ], "textPosition": { @@ -113,13 +113,13 @@ "anchors": [ { "type": "L", - "x": -300, - "y": 250 + "x": -100, + "y": 50 }, { "type": "R", "x": 300, - "y": 250 + "y": 50 } ], "textPosition": { @@ -142,22 +142,22 @@ { "type": "TL", "x": -90, - "y": 500 + "y": 50 }, { "type": "TR", "x": 225, - "y": 500 + "y": 50 }, { "type": "BL", "x": 215, - "y": 800 + "y": 350 }, { "type": "BR", "x": -90, - "y": 800 + "y": 350 } ], "textPosition": { From dd75daaa777db208740316d7629c3278b960b5cf Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Wed, 8 Feb 2023 19:44:27 +0100 Subject: [PATCH 17/44] beautify code and added lines between anchors --- frontend/src/components/Board/Board.tsx | 38 ++++- frontend/src/components/Tiles/Tile.tsx | 95 +++++------- .../src/components/Tiles/TileBorderAnchor.tsx | 18 +-- frontend/src/hooks/useLine.tsx | 139 ------------------ frontend/src/hooks/useMouse.tsx | 19 ++- frontend/src/state/BoardState.tsx | 32 ++-- frontend/src/state/ContextMenuState.tsx | 8 +- frontend/src/state/SyntaxTreeState.tsx | 15 +- frontend/src/state/WebSocketState.tsx | 8 +- 9 files changed, 118 insertions(+), 254 deletions(-) delete mode 100644 frontend/src/hooks/useLine.tsx diff --git a/frontend/src/components/Board/Board.tsx b/frontend/src/components/Board/Board.tsx index efe7ddc..6e12726 100644 --- a/frontend/src/components/Board/Board.tsx +++ b/frontend/src/components/Board/Board.tsx @@ -1,16 +1,15 @@ import Tile from '../Tiles/Tile'; import React, { useEffect } from 'react'; -import { Stage, Layer } from 'react-konva'; -import TileBorder from '../Tiles/TileBorder'; import { useGrid } from '../../hooks/useGrid'; import { useMouse } from '../../hooks/useMouse'; +import { Stage, Layer, Line } from 'react-konva'; import { Stage as StageType } from 'konva/lib/Stage'; import { Layer as LayerType } from 'konva/lib/Layer'; +import { SocketDragTile, RoomData } from '../../types'; import { useBoardState } from '../../state/BoardState'; -import useWindowDimensions from '../../hooks/useWindowDimensions'; import { useWebSocketState } from '../../state/WebSocketState'; -import { SocketDragTile, RoomData } from '../../types'; -import { useConnectedTilesContext } from '../../state/SyntaxTreeState'; +import useWindowDimensions from '../../hooks/useWindowDimensions'; +import { useConnectedTilesState } from '../../state/SyntaxTreeState'; // Main Stage Component that holds the Canvas. Scales based on the window size. @@ -34,12 +33,36 @@ const Board = () => { const setRoom = useWebSocketState((state) => state.setRoom); const deleteTile = useBoardState((state) => state.removeTile); const updateTile = useBoardState((state) => state.updateTile); + const connections = useConnectedTilesState((state) => state.connections); const setStageReference = useBoardState((state) => state.setStageReference); const setRemoteDragColor = useBoardState((state) => state.setRemoteDragColor); - const connectionPreview = useConnectedTilesContext((state) => state.connectionPreview); - + const connectionPreview = useConnectedTilesState((state) => state.connectionPreview); setStageReference(stageRef); + const connectionLines = connections.map((connection) => { + const [toId, toAnchorType] = connection.to.split('_'); + const [fromId, fromAnchorType] = connection.from.split('_'); + const toTile = room?.tiles?.find((tile) => tile.tile.id === toId); + const fromTile = room?.tiles?.find((tile) => tile.tile.id === fromId); + const toAnchor = toTile?.tile.anchors.find((anchor) => anchor.type === toAnchorType); + const fromAnchor = fromTile?.tile.anchors.find((anchor) => anchor.type === fromAnchorType); + + if (fromTile && toTile && fromAnchor && toAnchor) { + return ( + + ); + } + }); useEffect(() => { if (socket) { socket?.on('tile-drop', (data: SocketDragTile) => { @@ -80,6 +103,7 @@ const Board = () => { {gridComponents} {connectionPreview} + {connectionLines} {room?.tiles?.map((tileObject) => ( = ({ x, @@ -28,86 +27,63 @@ const Tile: React.FC = ({ const tileRef = React.useRef(null); const { handleContextMenu } = useContextMenu(); const { updateTilePosition, setActiveDragElement } = useMouse(); - const tilesOnBoard = useBoardState((state) => state.tilesOnBoard); - const [connections, setConnections] = React.useState< - { source: string; destination: TileProps; destinationAnchorType: string }[] - >([]); + const { fromShapeId, setFromShapeId, connections, setConnections } = useConnectedTilesState( + (state) => state, + ); const userColor = useWebSocketState( (state) => state.room?.users?.find((user) => user.userId === state.socket?.id)?.color, ); - const createConnectionPoints = ( - source: { x: number; y: number }, - destination: { x: number; y: number }, - ) => { - return [source.x, source.y, destination.x, destination.y]; - }; - - const { handleAnchorDragStart, handleAnchorDragMove, handleAnchorDragEnd } = useLine(); - - const connectionObjs = connections.map((connection) => { - const fromTile = tilesOnBoard.find((tile) => tile.id === connection.source); - const toTile = tilesOnBoard.find((tile) => tile.id === connection.destination.id); - if (fromTile && toTile) { - const toTileAnchor = toTile.anchors?.find( - (anchor) => anchor.type === connection.destinationAnchorType, - ); - if (toTileAnchor) { - const lineEnd = { - x: toTileAnchor.x - fromTile.x, - y: toTileAnchor.y - fromTile.y, + const handleClick = (event: KonvaEventObject) => { + const { id, 'data-type': type } = event.target.attrs; + console.log('clicked', id); + if (fromShapeId === null) { + setFromShapeId(id); + } else { + if (fromShapeId !== id) { + const newConnection = { + from: `${fromShapeId}_${type}`, + to: `${id}_${type}`, }; - const points = createConnectionPoints({ x: 0, y: 0 }, lineEnd); - return ( - - ); + setConnections([...connections, newConnection]); + setFromShapeId(null); } } - }); + }; + return ( <> {userColor && id && ( <> - {id && ( - <> - - {anchors.map((point, index) => ( - handleAnchorDragMove(e, createConnectionPoints)} - dragStart={(e) => handleAnchorDragStart(e, createConnectionPoints)} - dragEnd={(e) => handleAnchorDragEnd(e, id, connections, setConnections)} - /> - ))} - - )} + {anchors.map((point, index) => ( + + ))} setActiveDragElement(tileRef, event)} > @@ -129,11 +105,8 @@ const Tile: React.FC = ({ )} - - {connectionObjs} ); }; - export default Tile; diff --git a/frontend/src/components/Tiles/TileBorderAnchor.tsx b/frontend/src/components/Tiles/TileBorderAnchor.tsx index f896ed7..8d062a5 100644 --- a/frontend/src/components/Tiles/TileBorderAnchor.tsx +++ b/frontend/src/components/Tiles/TileBorderAnchor.tsx @@ -7,13 +7,12 @@ type Props = { x: number; y: number; id: string; + fill: string; type: string; - dragStart: (e: KonvaEventObject) => void; - dragMove: (e: KonvaEventObject) => void; - dragEnd: (e: KonvaEventObject, id: string) => void; + onClick: (event: KonvaEventObject) => void; }; -const TileBorderAnchors: React.FC = ({ x, y, id, dragStart, dragMove, dragEnd, type }) => { +const TileBorderAnchors: React.FC = ({ x, y, id, onClick, fill, type }) => { const dragBounds = (ref: React.RefObject) => { if (ref.current !== null) { return ref.current.getAbsolutePosition(); @@ -33,14 +32,13 @@ const TileBorderAnchors: React.FC = ({ x, y, id, dragStart, dragMove, dra id={id} draggable radius={10} - fill='black' + fill={fill} + ref={anchor} + name='anchor' data-type={type} - onDragStart={dragStart} - onDragMove={dragMove} - onDragEnd={(event) => dragEnd(event, id)} - dragBoundFunc={() => dragBounds(anchor)} + onClick={onClick} perfectDrawEnabled={false} - ref={anchor} + dragBoundFunc={() => dragBounds(anchor)} /> ); diff --git a/frontend/src/hooks/useLine.tsx b/frontend/src/hooks/useLine.tsx deleted file mode 100644 index d686f93..0000000 --- a/frontend/src/hooks/useLine.tsx +++ /dev/null @@ -1,139 +0,0 @@ -import { KonvaEventObject } from 'konva/lib/Node'; -import React from 'react'; -import { Line } from 'react-konva'; -import { useBoardState } from '../state/BoardState'; -import { useConnectedTilesContext } from '../state/SyntaxTreeState'; -import { Coordinates, Tile as TileProps } from '../types'; - -export const useLine = () => { - const setConnectionPreview = useConnectedTilesContext((state) => state.setConnectionPreview); - const tilesOnBoard = useBoardState((state) => state.tilesOnBoard); - const hasInterSection = (linePosition: Coordinates, tilePosition: Coordinates) => { - return tilePosition.x - linePosition.x < 50 && tilePosition.y - linePosition.y < 50; - }; - - const detectConnection = (linePosition: Coordinates, id: string) => { - let foundAnchor = ''; - const intersectingTile: TileProps | undefined = tilesOnBoard.find((tile) => { - if (tile.anchors) { - const found = tile.anchors?.find((anchor) => { - const tilePosition = { x: anchor.x, y: anchor.y }; - return id !== tile.id && hasInterSection(linePosition, tilePosition) === true && tile; - }); - if (found !== undefined) { - foundAnchor = found.type; - return tile; - } - } - }); - if (intersectingTile !== undefined) { - return { intersectingTile, foundAnchor }; - } - return null; - }; - - const handleAnchorDragStart = ( - e: KonvaEventObject, - createConnectionPoints: ( - source: { - x: number; - y: number; - }, - destination: { - x: number; - y: number; - }, - ) => number[], - ) => { - const position = e.target.position(); - setConnectionPreview( - , - ); - }; - - const handleAnchorDragMove = ( - e: KonvaEventObject, - createConnectionPoints: ( - source: { - x: number; - y: number; - }, - destination: { - x: number; - y: number; - }, - ) => number[], - ) => { - const position = e.target.position(); - const stage = e.target.getStage(); - const pointerPosition = stage?.getPointerPosition(); - if (pointerPosition) { - const mousePos = { - x: pointerPosition.x - position.x, - y: pointerPosition.y - position.y, - }; - setConnectionPreview( - , - ); - } - }; - - const handleAnchorDragEnd = ( - e: KonvaEventObject, - id: string, - connections: { - source: string; - destination: TileProps; - destinationAnchorType: string; - }[], - setConnections: React.Dispatch< - React.SetStateAction< - { - source: string; - destination: TileProps; - destinationAnchorType: string; - }[] - > - >, - ) => { - setConnectionPreview(null); - const stage = e.target.getStage(); - const pointerPosition = stage?.getPointerPosition(); - if (pointerPosition) { - const pos = { - x: pointerPosition.x, - y: pointerPosition.y, - }; - const intersectingTile = detectConnection(pos, id); - if (intersectingTile !== null) { - setConnections([ - ...connections, - { - source: id, - destination: intersectingTile.intersectingTile, - destinationAnchorType: intersectingTile.foundAnchor, - }, - ]); - } - } - }; - - return { - detectConnection, - handleAnchorDragStart, - handleAnchorDragMove, - handleAnchorDragEnd, - }; -}; diff --git a/frontend/src/hooks/useMouse.tsx b/frontend/src/hooks/useMouse.tsx index ce4e317..f2061b9 100644 --- a/frontend/src/hooks/useMouse.tsx +++ b/frontend/src/hooks/useMouse.tsx @@ -68,7 +68,8 @@ export const useMouse = () => { ); if (tile) { - const { _id, name, src, color, points, textPosition, anchors, width, height } = tile; + const { _id, name, src, color, points, textPosition, anchors, width, height, category } = + tile; const dragPayload = JSON.stringify({ id: uuidv4(), _id: _id, @@ -79,6 +80,7 @@ export const useMouse = () => { height: height, points: points, anchors: anchors, + category: category, textPosition: textPosition, offsetX: event.nativeEvent.offsetX, offsetY: event.nativeEvent.offsetY, @@ -106,7 +108,7 @@ export const useMouse = () => { anchors, offsetX, offsetY, - nodeClass, + category, textPosition, } = JSON.parse(draggedData); if (x && y) { @@ -120,7 +122,7 @@ export const useMouse = () => { height: height, points: points, anchors: anchors, - category: nodeClass, + category: category, textPosition: textPosition, x: x - (offsetX - width / 2), y: y - (offsetY - height / 2), @@ -146,30 +148,31 @@ export const useMouse = () => { */ const { - 'data-id': _id, + 'data-id': uid, 'data-src': src, 'data-fill': color, 'data-width': width, 'data-points': points, 'data-height': height, 'data-anchors': anchors, + 'data-category': tileName, 'data-textPosition': textPosition, } = event.target.attrs; if (stageRef.current) { const updatedTile: Tile = { - _id: _id, + _id: uid, src: src, width: width, color: color, height: height, - points: points, - anchors: anchors, + category: tileName, x: event.target.x(), y: event.target.y(), id: event.target.attrs.id, + points: JSON.parse(points), textPosition: textPosition, + anchors: JSON.parse(anchors), name: event.target.attrs.name, - category: event.target.attrs.name, }; updateTile(updatedTile); } diff --git a/frontend/src/state/BoardState.tsx b/frontend/src/state/BoardState.tsx index cc1816f..6e816e4 100644 --- a/frontend/src/state/BoardState.tsx +++ b/frontend/src/state/BoardState.tsx @@ -7,22 +7,22 @@ import { Tile } from '../types'; import { mountStoreDevtool } from 'simple-zustand-devtools'; export type BoardContextType = { - modalOpen: boolean; - categoriesOpen: boolean; allTiles: Tile[]; + modalOpen: boolean; tilesOnBoard: Tile[]; + categoriesOpen: boolean; selectedTile: Tile | null; remoteDragColor: string | null; stageReference: React.RefObject; activeDragTile: React.RefObject | null; clearActiveDragTile: () => void; addTile: (newTile: Tile) => void; - setSelectedTile: (tile: Tile | null) => void; toggleModal: (toggle: boolean) => void; updateTile: (updatedNode: Tile) => void; setAllTiles: (tilesArray: Tile[]) => void; removeTile: (nodeToRemove: string) => void; setCategoriesOpen: (toggle: boolean) => void; + setSelectedTile: (tile: Tile | null) => void; setRemoteDragColor: (color: string | null) => void; setStageReference: (stage: React.RefObject) => void; setActiveDragTile: (newActiveTile: React.RefObject) => void; @@ -30,35 +30,35 @@ export type BoardContextType = { export const useBoardState = create((set) => ({ allTiles: [], - modalOpen: false, - categoriesOpen: false, tilesOnBoard: [], + modalOpen: false, selectedTile: null, - activeDragTile: null, remoteDragColor: '', + activeDragTile: null, + categoriesOpen: false, stageReference: createRef(), - clearActiveDragTile: () => set(() => ({ activeDragTile: null })), - setSelectedTile: (tile: Tile | null) => set(() => ({ selectedTile: tile })), - setActiveDragTile: (newActiveTile: React.RefObject) => - set(() => ({ activeDragTile: newActiveTile })), - setCategoriesOpen: (isOpen: boolean) => set(() => ({ categoriesOpen: isOpen })), - toggleModal: (toggle: boolean) => set(() => ({ modalOpen: toggle })), - setAllTiles: (tilesArray: Tile[]) => set(() => ({ allTiles: tilesArray })), - addTile: (newTile: Tile) => set((state) => ({ tilesOnBoard: [...state.tilesOnBoard, newTile] })), updateTile: (updatedTile: Tile) => set((state) => ({ tilesOnBoard: state.tilesOnBoard.map((tile) => tile.id === updatedTile.id ? updatedTile : tile, ), })), + setStageReference: (stageRef: React.RefObject) => + set(() => ({ stageReference: stageRef })), + setActiveDragTile: (newActiveTile: React.RefObject) => + set(() => ({ activeDragTile: newActiveTile })), + clearActiveDragTile: () => set(() => ({ activeDragTile: null })), + toggleModal: (toggle: boolean) => set(() => ({ modalOpen: toggle })), + setAllTiles: (tilesArray: Tile[]) => set(() => ({ allTiles: tilesArray })), + setSelectedTile: (tile: Tile | null) => set(() => ({ selectedTile: tile })), + setCategoriesOpen: (isOpen: boolean) => set(() => ({ categoriesOpen: isOpen })), removeTile: (tileToRemove: string) => set((state) => ({ tilesOnBoard: state.tilesOnBoard.filter((tile) => tile.id !== tileToRemove), })), setRemoteDragColor: (color: string | null) => set(() => ({ remoteDragColor: color })), - setStageReference: (stageRef: React.RefObject) => - set(() => ({ stageReference: stageRef })), + addTile: (newTile: Tile) => set((state) => ({ tilesOnBoard: [...state.tilesOnBoard, newTile] })), })); diff --git a/frontend/src/state/ContextMenuState.tsx b/frontend/src/state/ContextMenuState.tsx index 98e8d17..265863e 100644 --- a/frontend/src/state/ContextMenuState.tsx +++ b/frontend/src/state/ContextMenuState.tsx @@ -2,22 +2,22 @@ import create from 'zustand'; import { mountStoreDevtool } from 'simple-zustand-devtools'; export type ContextMenuStateType = { - contextMenuOpen: boolean; - contextMenuAnchorPoint: { x: number; y: number; id: string }; panelOpen: boolean; + contextMenuOpen: boolean; setPanelOpen: (value: boolean) => void; setContextMenuOpen: (value: boolean) => void; + contextMenuAnchorPoint: { x: number; y: number; id: string }; setContextMenuAnchorPoint: (value: { x: number; y: number; id: string }) => void; }; export const useContextMenuState = create((set) => ({ + panelOpen: false, contextMenuOpen: false, contextMenuAnchorPoint: { x: 0, y: 0, id: '' }, - panelOpen: false, setPanelOpen: (value: boolean) => set(() => ({ panelOpen: value })), - setContextMenuOpen: (value: boolean) => set(() => ({ contextMenuOpen: value })), setContextMenuAnchorPoint: (value: { x: number; y: number; id: string }) => set(() => ({ contextMenuAnchorPoint: value })), + setContextMenuOpen: (value: boolean) => set(() => ({ contextMenuOpen: value })), })); diff --git a/frontend/src/state/SyntaxTreeState.tsx b/frontend/src/state/SyntaxTreeState.tsx index 0a2e719..264d22e 100644 --- a/frontend/src/state/SyntaxTreeState.tsx +++ b/frontend/src/state/SyntaxTreeState.tsx @@ -5,19 +5,24 @@ import create from 'zustand'; import { mountStoreDevtool } from 'simple-zustand-devtools'; export type ConnectedTilesContextType = { - connectedTiles: { [key: string]: string[] }; + fromShapeId: string | null; connectionPreview: JSX.Element | null; + connections: { from: string; to: string }[]; + setFromShapeId: (value: string | null) => void; setConnectionPreview: (value: JSX.Element | null) => void; - setConnectedTiles: (value: { [key: string]: string[] }) => void; + setConnections: (value: { from: string; to: string }[]) => void; }; -export const useConnectedTilesContext = create((set) => ({ +export const useConnectedTilesState = create((set) => ({ + connections: [], + fromShapeId: null, connectedTiles: {}, connectionPreview: null, + setFromShapeId: (value: string | null) => set(() => ({ fromShapeId: value })), + setConnections: (value: { from: string; to: string }[]) => set(() => ({ connections: value })), setConnectionPreview: (value: JSX.Element | null) => set(() => ({ connectionPreview: value })), - setConnectedTiles: (value: { [key: string]: string[] }) => set(() => ({ connectedTiles: value })), })); if (process.env.NODE_ENV === 'development') { - mountStoreDevtool('ConnectedTilesContext', useConnectedTilesContext); + mountStoreDevtool('ConnectedTilesContext', useConnectedTilesState); } diff --git a/frontend/src/state/WebSocketState.tsx b/frontend/src/state/WebSocketState.tsx index 8ca9deb..d61cb05 100644 --- a/frontend/src/state/WebSocketState.tsx +++ b/frontend/src/state/WebSocketState.tsx @@ -5,19 +5,19 @@ import { RoomData } from '../types'; // TODO: split up room and user data export type WebSocketContextType = { + clearRoom: () => void; socket: Socket | null; room: RoomData | null; - setSocket: (socket: Socket) => void; setRoom: (room: RoomData) => void; - clearRoom: () => void; + setSocket: (socket: Socket) => void; }; export const useWebSocketState = create((set) => ({ - socket: null, room: null, + socket: null, + clearRoom: () => set(() => ({ room: null })), setSocket: (socket: Socket) => set(() => ({ socket: socket })), setRoom: (roomData: RoomData) => set(() => ({ room: roomData })), - clearRoom: () => set(() => ({ room: null })), })); From efa4b28e42b41d9f9219665a678491a7977e1bd0 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Wed, 8 Feb 2023 20:38:09 +0100 Subject: [PATCH 18/44] added collaborative line creation --- backend/src/Controller/socketController.ts | 18 ++++++++++++ backend/src/index.ts | 9 ++++-- backend/types/socket.types.d.ts | 7 +++++ frontend/src/components/Board/Board.tsx | 4 +-- frontend/src/components/Tiles/Tile.tsx | 26 ++--------------- frontend/src/hooks/useMouse.tsx | 34 +++++++++++++++++++++- frontend/src/types.d.ts | 7 +++++ 7 files changed, 77 insertions(+), 28 deletions(-) diff --git a/backend/src/Controller/socketController.ts b/backend/src/Controller/socketController.ts index 60e1401..0b6bc12 100644 --- a/backend/src/Controller/socketController.ts +++ b/backend/src/Controller/socketController.ts @@ -1,3 +1,5 @@ +import { TileConnection } from "./../../types/socket.types.d"; +import { state } from "./../Model/socketModel"; import { Socket, Server } from "socket.io"; import { DefaultEventsMap } from "socket.io/dist/typed-events"; import { @@ -209,3 +211,19 @@ export const disconnect = ( }); }); }; + +export const tileConnect = ( + data: TileConnection, + state: RoomData[], + io: Server, +) => { + const room = state.find((room) => room.roomId === data.roomId); + if (room) { + if (room.tileConnections?.length > 0) { + room.tileConnections?.push(data); + } else { + room.tileConnections = [data]; + } + } + io.to(data.roomId).emit("room-data", room); +}; diff --git a/backend/src/index.ts b/backend/src/index.ts index f0b7219..f9b9be4 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -4,16 +4,18 @@ import { SocketCursorData, SocketDeleteData, TabFocusData, + TileConnection, } from "./../types/socket.types.d"; import { - createRoom, - cursorMove, joinRoom, tabFocus, tileDrag, tileDrop, + createRoom, deleteTile, disconnect, + cursorMove, + tileConnect, errorHandling, } from "./Controller/socketController"; import cors from "cors"; @@ -96,6 +98,9 @@ io.on("connection", (socket) => { ); socket.on("disconnect", () => disconnect(state, socket)); socket.on("error", (err: Error) => errorHandling(err)); + socket.on("tile-connection", (data: TileConnection) => + tileConnect(data, state, io), + ); }); db.on("error", (error) => console.error(error)); diff --git a/backend/types/socket.types.d.ts b/backend/types/socket.types.d.ts index 30392be..39552ad 100644 --- a/backend/types/socket.types.d.ts +++ b/backend/types/socket.types.d.ts @@ -55,8 +55,15 @@ export type TileData = { tile: NewTile; }; +export type TileConnection = { + to: string; + from: string; + roomId: string; +}; + export type RoomData = { roomId: string; users: UserData[]; tiles?: TileData[]; + tileConnections?: TileConnection[]; }; diff --git a/frontend/src/components/Board/Board.tsx b/frontend/src/components/Board/Board.tsx index 6e12726..56d286f 100644 --- a/frontend/src/components/Board/Board.tsx +++ b/frontend/src/components/Board/Board.tsx @@ -39,7 +39,7 @@ const Board = () => { const connectionPreview = useConnectedTilesState((state) => state.connectionPreview); setStageReference(stageRef); - const connectionLines = connections.map((connection) => { + const connectionLines = room?.tileConnections?.map((connection) => { const [toId, toAnchorType] = connection.to.split('_'); const [fromId, fromAnchorType] = connection.from.split('_'); const toTile = room?.tiles?.find((tile) => tile.tile.id === toId); @@ -50,7 +50,7 @@ const Board = () => { if (fromTile && toTile && fromAnchor && toAnchor) { return ( = ({ @@ -26,31 +25,12 @@ const Tile: React.FC = ({ }) => { const tileRef = React.useRef(null); const { handleContextMenu } = useContextMenu(); - const { updateTilePosition, setActiveDragElement } = useMouse(); - const { fromShapeId, setFromShapeId, connections, setConnections } = useConnectedTilesState( - (state) => state, - ); + const { updateTilePosition, setActiveDragElement, handleClick } = useMouse(); + const { fromShapeId } = useConnectedTilesState((state) => state); const userColor = useWebSocketState( (state) => state.room?.users?.find((user) => user.userId === state.socket?.id)?.color, ); - const handleClick = (event: KonvaEventObject) => { - const { id, 'data-type': type } = event.target.attrs; - console.log('clicked', id); - if (fromShapeId === null) { - setFromShapeId(id); - } else { - if (fromShapeId !== id) { - const newConnection = { - from: `${fromShapeId}_${type}`, - to: `${id}_${type}`, - }; - setConnections([...connections, newConnection]); - setFromShapeId(null); - } - } - }; - return ( <> {userColor && id && ( @@ -63,7 +43,7 @@ const Tile: React.FC = ({ y={y + point.y} type={point.type} onClick={handleClick} - fill={fromShapeId === id ? 'green' : 'black'} + fill={fromShapeId === `${id}_${point.type}` ? 'green' : 'black'} /> ))} diff --git a/frontend/src/hooks/useMouse.tsx b/frontend/src/hooks/useMouse.tsx index f2061b9..5109a8f 100644 --- a/frontend/src/hooks/useMouse.tsx +++ b/frontend/src/hooks/useMouse.tsx @@ -6,6 +6,7 @@ import { KonvaEventObject } from 'konva/lib/Node'; import { useBoardState } from '../state/BoardState'; import { useWebSocketState } from '../state/WebSocketState'; import { useContextMenuState } from '../state/ContextMenuState'; +import { useConnectedTilesState } from '../state/SyntaxTreeState'; export const useMouse = () => { const setTiles = useBoardState((state) => state.addTile); @@ -21,7 +22,9 @@ export const useMouse = () => { (state) => state.room?.users.find((user) => user.userId === socket?.id)?.color, ); const setContextMenuOpen = useContextMenuState((state) => state.setContextMenuOpen); - + const { fromShapeId, setFromShapeId, connections, setConnections } = useConnectedTilesState( + (state) => state, + ); const toggleCategory = () => { setCategoriesOpen(false); }; @@ -264,8 +267,37 @@ export const useMouse = () => { } }; + const handleClick = (event: KonvaEventObject) => { + const { id, 'data-type': type } = event.target.attrs; + console.log('clicked', id, type); + if (fromShapeId === `${id}_${type}`) { + setFromShapeId(null); + } + if (fromShapeId === null) { + setFromShapeId(`${id}_${type}`); + } else { + if (fromShapeId !== `${id}_${type}`) { + const newConnection = { + from: `${fromShapeId}`, + to: `${id}_${type}`, + }; + setConnections([...connections, newConnection]); + setFromShapeId(null); + + if (socket && roomId) { + const tileConnection = { + from: `${fromShapeId}`, + to: `${id}_${type}`, + roomId: roomId, + }; + socket.emit('tile-connection', tileConnection); + } + } + } + }; return { handleDrop, + handleClick, handleWheel, toggleCategory, handleMouseEnL, diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index b55b68d..c8f472a 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -63,10 +63,17 @@ export type TileData = { tile: Tile; }; +export type TileConnection = { + to: string; + from: string; + roomId: string; +}; + export type RoomData = { roomId: string; users: UserData[]; tiles?: TileData[]; + tileConnections?: TileConnection[]; }; export type Coordinates = { From 12cbf4442edd8092f34574374574ae2f3371305a Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Thu, 9 Feb 2023 14:04:18 +0100 Subject: [PATCH 19/44] added disconnect Logic --- backend/src/Controller/socketController.ts | 2 ++ backend/src/index.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/Controller/socketController.ts b/backend/src/Controller/socketController.ts index 0b6bc12..aded19c 100644 --- a/backend/src/Controller/socketController.ts +++ b/backend/src/Controller/socketController.ts @@ -191,6 +191,7 @@ export const deleteTile = ( export const disconnect = ( state: RoomData[], socket: Socket, + io: Server, ) => { /** * before updating the state check if the user is the host @@ -206,6 +207,7 @@ export const disconnect = ( state.splice(state.indexOf(room), 1); } else { room.users.splice(room.users.indexOf(user), 1); + io.emit("room-data", room); } } }); diff --git a/backend/src/index.ts b/backend/src/index.ts index f9b9be4..0a4a111 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -96,7 +96,7 @@ io.on("connection", (socket) => { socket.on("tile-delete", (data: SocketDeleteData) => deleteTile(data, state, io), ); - socket.on("disconnect", () => disconnect(state, socket)); + socket.on("disconnect", () => disconnect(state, socket, io)); socket.on("error", (err: Error) => errorHandling(err)); socket.on("tile-connection", (data: TileConnection) => tileConnect(data, state, io), From aae20bf8a8e311cb6f5cd48523e48c6830c969cb Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Thu, 9 Feb 2023 15:33:19 +0100 Subject: [PATCH 20/44] create function to delete Line --- backend/src/Controller/socketController.ts | 24 ++++++++- backend/src/index.ts | 7 +-- backend/utils/tileConnections.ts | 16 ++++++ frontend/src/components/Board/Board.tsx | 14 ++++-- .../ContextMenus/LineRightClickMenu.tsx | 36 ++++++++++++++ .../ContextMenus/MenuOptions/RemoveLine.tsx | 18 +++++++ .../ContextMenus/MenuOptions/SetLamp.tsx | 1 - ...htClickMenu.tsx => TileRightClickMenu.tsx} | 4 +- frontend/src/components/Tiles/Tile.tsx | 6 ++- frontend/src/hooks/useContextMenu.tsx | 49 +++++++++++++------ frontend/src/pages/CanvasPage.tsx | 12 +++-- frontend/src/state/ContextMenuState.tsx | 4 ++ frontend/src/state/SyntaxTreeState.tsx | 6 +++ frontend/src/utils/tileConnections.ts | 11 +++++ 14 files changed, 173 insertions(+), 35 deletions(-) create mode 100644 backend/utils/tileConnections.ts create mode 100644 frontend/src/components/ContextMenus/LineRightClickMenu.tsx create mode 100644 frontend/src/components/ContextMenus/MenuOptions/RemoveLine.tsx rename frontend/src/components/ContextMenus/{RightClickMenu.tsx => TileRightClickMenu.tsx} (95%) create mode 100644 frontend/src/utils/tileConnections.ts diff --git a/backend/src/Controller/socketController.ts b/backend/src/Controller/socketController.ts index aded19c..105517e 100644 --- a/backend/src/Controller/socketController.ts +++ b/backend/src/Controller/socketController.ts @@ -1,6 +1,6 @@ -import { TileConnection } from "./../../types/socket.types.d"; -import { state } from "./../Model/socketModel"; import { Socket, Server } from "socket.io"; +import { findConnections } from "./../../utils/tileConnections"; +import { TileConnection } from "./../../types/socket.types.d"; import { DefaultEventsMap } from "socket.io/dist/typed-events"; import { RoomData, @@ -168,6 +168,26 @@ export const cursorMove = ( } }; +export const deleteLine = ( + data: SocketDeleteData, + state: RoomData[], + io: Server, +) => { + /** + * check if the room exists + * if it does,then check if the tile exists + * if it does, delete the tile from the room + * update the room state + * emit the room data to the every user in the room + */ + + const room = state.find((room) => room.roomId === data.roomId); + if (room) { + room.tileConnections = findConnections(data.id, room.tileConnections); + io.to(data.roomId).emit("room-data", room); + } +}; + export const deleteTile = ( data: SocketDeleteData, state: RoomData[], diff --git a/backend/src/index.ts b/backend/src/index.ts index 0a4a111..59ecd79 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -13,6 +13,7 @@ import { tileDrop, createRoom, deleteTile, + deleteLine, disconnect, cursorMove, tileConnect, @@ -79,9 +80,6 @@ app.use(cors()); app.use("/uploads", express.static("uploads")); app.use(express.urlencoded({ extended: true })); - - - io.on("connection", (socket) => { socket.on("room-create", (data: UserData) => createRoom(data, state, socket)); socket.on("join-room", (data: UserData) => joinRoom(data, state, socket, io)); @@ -96,6 +94,9 @@ io.on("connection", (socket) => { socket.on("tile-delete", (data: SocketDeleteData) => deleteTile(data, state, io), ); + socket.on("line-delete", (data: SocketDeleteData) => + deleteLine(data, state, io), + ); socket.on("disconnect", () => disconnect(state, socket, io)); socket.on("error", (err: Error) => errorHandling(err)); socket.on("tile-connection", (data: TileConnection) => diff --git a/backend/utils/tileConnections.ts b/backend/utils/tileConnections.ts new file mode 100644 index 0000000..197f733 --- /dev/null +++ b/backend/utils/tileConnections.ts @@ -0,0 +1,16 @@ +import { TileConnection } from "./../types/socket.types"; + +const splitIds = (id: string) => { + const [fromId, toId] = id.split("."); + return { fromId, toId }; +}; + +const findConnections = (value: string, connections: TileConnection[]) => { + const { fromId, toId } = splitIds(value); + console.log(fromId, toId); + return connections.filter( + (connection) => connection.from !== fromId && connection.to !== toId, + ); +}; + +export { splitIds, findConnections }; diff --git a/frontend/src/components/Board/Board.tsx b/frontend/src/components/Board/Board.tsx index 56d286f..237cea0 100644 --- a/frontend/src/components/Board/Board.tsx +++ b/frontend/src/components/Board/Board.tsx @@ -10,6 +10,9 @@ import { useBoardState } from '../../state/BoardState'; import { useWebSocketState } from '../../state/WebSocketState'; import useWindowDimensions from '../../hooks/useWindowDimensions'; import { useConnectedTilesState } from '../../state/SyntaxTreeState'; +import { KonvaEventObject } from 'konva/lib/Node'; +import { useContextMenu } from '../../hooks/useContextMenu'; +import { useContextMenuState } from '../../state/ContextMenuState'; // Main Stage Component that holds the Canvas. Scales based on the window size. @@ -23,20 +26,21 @@ const Board = () => { handleDragOver, handleDrop, handleWheel, - handleMouseMove, toggleCategory, + handleMouseMove, handleBoardDrag, } = useMouse(); + const { handleContextMenu } = useContextMenu(); const room = useWebSocketState((state) => state.room); const addTile = useBoardState((state) => state.addTile); const socket = useWebSocketState((state) => state.socket); const setRoom = useWebSocketState((state) => state.setRoom); const deleteTile = useBoardState((state) => state.removeTile); const updateTile = useBoardState((state) => state.updateTile); - const connections = useConnectedTilesState((state) => state.connections); const setStageReference = useBoardState((state) => state.setStageReference); const setRemoteDragColor = useBoardState((state) => state.setRemoteDragColor); const connectionPreview = useConnectedTilesState((state) => state.connectionPreview); + const setLineContextMenuOpen = useContextMenuState((state) => state.setLineContextMenuOpen); setStageReference(stageRef); const connectionLines = room?.tileConnections?.map((connection) => { @@ -50,7 +54,9 @@ const Board = () => { if (fromTile && toTile && fromAnchor && toAnchor) { return ( handleContextMenu(e, setLineContextMenuOpen)} + id={`${fromId}_${fromAnchorType}.${toId}_${toAnchorType}`} + key={`${fromId}_${fromAnchorType}.${toId}_${toAnchorType}`} points={[ fromTile.tile.x + fromAnchor.x, fromTile.tile.y + fromAnchor.y, @@ -58,7 +64,7 @@ const Board = () => { toTile.tile.y + toAnchor.y, ]} stroke='black' - strokeWidth={2} + strokeWidth={6} /> ); } diff --git a/frontend/src/components/ContextMenus/LineRightClickMenu.tsx b/frontend/src/components/ContextMenus/LineRightClickMenu.tsx new file mode 100644 index 0000000..e8d3488 --- /dev/null +++ b/frontend/src/components/ContextMenus/LineRightClickMenu.tsx @@ -0,0 +1,36 @@ +import React from 'react'; + +import RemoveLine from './MenuOptions/RemoveLine'; +import { faXmark } from '@fortawesome/free-solid-svg-icons'; +import { useContextMenu } from '../../hooks/useContextMenu'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { useContextMenuState } from '../../state/ContextMenuState'; + +const LineRightClickMenu = () => { + const { contextMenuAnchorPoint, handleRemoveLine } = useContextMenu(); + const setLineContextMenuOpen = useContextMenuState((state) => state.setLineContextMenuOpen); + + return ( +
    +
    + setLineContextMenuOpen(false)} + icon={faXmark} + /> +
    +
      + +
    +
    + ); +}; + +export default LineRightClickMenu; diff --git a/frontend/src/components/ContextMenus/MenuOptions/RemoveLine.tsx b/frontend/src/components/ContextMenus/MenuOptions/RemoveLine.tsx new file mode 100644 index 0000000..4d8bbca --- /dev/null +++ b/frontend/src/components/ContextMenus/MenuOptions/RemoveLine.tsx @@ -0,0 +1,18 @@ +import { faTrash } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import React from 'react'; + +type Props = { + onclick: () => void; +}; + +const RemoveTile: React.FC = ({ onclick }) => { + return ( +
  • +

    Verbindung aufheben

    + +
  • + ); +}; + +export default RemoveTile; diff --git a/frontend/src/components/ContextMenus/MenuOptions/SetLamp.tsx b/frontend/src/components/ContextMenus/MenuOptions/SetLamp.tsx index 2e372a8..8bc8c8c 100644 --- a/frontend/src/components/ContextMenus/MenuOptions/SetLamp.tsx +++ b/frontend/src/components/ContextMenus/MenuOptions/SetLamp.tsx @@ -1,7 +1,6 @@ import React from 'react'; import { faLightbulb } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { useContextMenuState } from '../../../state/ContextMenuState'; type Props = { onclick: () => void; diff --git a/frontend/src/components/ContextMenus/RightClickMenu.tsx b/frontend/src/components/ContextMenus/TileRightClickMenu.tsx similarity index 95% rename from frontend/src/components/ContextMenus/RightClickMenu.tsx rename to frontend/src/components/ContextMenus/TileRightClickMenu.tsx index 023e760..ded8cf0 100644 --- a/frontend/src/components/ContextMenus/RightClickMenu.tsx +++ b/frontend/src/components/ContextMenus/TileRightClickMenu.tsx @@ -7,7 +7,7 @@ import { useContextMenu } from '../../hooks/useContextMenu'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useContextMenuState } from '../../state/ContextMenuState'; -const RightClickMenu = () => { +const TileRightClickMenu = () => { const removeTile = useBoardState((state) => state.removeTile); const { contextMenuAnchorPoint, handleClick } = useContextMenu(); const setPanelOpen = useContextMenuState((state) => state.setPanelOpen); @@ -47,4 +47,4 @@ const RightClickMenu = () => { ); }; -export default RightClickMenu; +export default TileRightClickMenu; diff --git a/frontend/src/components/Tiles/Tile.tsx b/frontend/src/components/Tiles/Tile.tsx index f33292f..2f3960b 100644 --- a/frontend/src/components/Tiles/Tile.tsx +++ b/frontend/src/components/Tiles/Tile.tsx @@ -7,6 +7,7 @@ import { Group as GroupType } from 'konva/lib/Group'; import { useContextMenu } from '../../hooks/useContextMenu'; import { useWebSocketState } from '../../state/WebSocketState'; import { useConnectedTilesState } from '../../state/SyntaxTreeState'; +import { useContextMenuState } from '../../state/ContextMenuState'; const Tile: React.FC = ({ x, @@ -25,8 +26,9 @@ const Tile: React.FC = ({ }) => { const tileRef = React.useRef(null); const { handleContextMenu } = useContextMenu(); - const { updateTilePosition, setActiveDragElement, handleClick } = useMouse(); const { fromShapeId } = useConnectedTilesState((state) => state); + const { updateTilePosition, setActiveDragElement, handleClick } = useMouse(); + const setContextMenuOpen = useContextMenuState((state) => state.setContextMenuOpen); const userColor = useWebSocketState( (state) => state.room?.users?.find((user) => user.userId === state.socket?.id)?.color, ); @@ -61,7 +63,7 @@ const Tile: React.FC = ({ data-height={height} data-category={category} onDragEnd={updateTilePosition} - onContextMenu={handleContextMenu} + onContextMenu={(e) => handleContextMenu(e, setContextMenuOpen)} data-points={JSON.stringify(points)} data-anchors={JSON.stringify(anchors)} data-textPosition={JSON.stringify(textPosition)} diff --git a/frontend/src/hooks/useContextMenu.tsx b/frontend/src/hooks/useContextMenu.tsx index 0671280..a41790c 100644 --- a/frontend/src/hooks/useContextMenu.tsx +++ b/frontend/src/hooks/useContextMenu.tsx @@ -2,25 +2,32 @@ import { useCallback } from 'react'; import { KonvaEventObject } from 'konva/lib/Node'; import { useWebSocketState } from '../state/WebSocketState'; import { useContextMenuState } from '../state/ContextMenuState'; +import { useConnectedTilesState } from '../state/SyntaxTreeState'; export const useContextMenu = () => { - const room = useWebSocketState((state) => state.room); - const socket = useWebSocketState((state) => state.socket); + const { room, socket } = useWebSocketState((state) => state); const contextMenu = useContextMenuState((state) => state.contextMenuOpen); - const setContextMenuOpen = useContextMenuState((state) => state.setContextMenuOpen); - const contextMenuAnchorPoint = useContextMenuState((state) => state.contextMenuAnchorPoint); - const setContextMenuAnchorPoint = useContextMenuState((state) => state.setContextMenuAnchorPoint); + const { + contextMenuAnchorPoint, + setContextMenuOpen, + setLineContextMenuOpen, + setContextMenuAnchorPoint, + } = useContextMenuState((state) => state); + const removeConnection = useConnectedTilesState((state) => state.removeConnection); - const handleContextMenu = (event: KonvaEventObject) => { + const handleContextMenu = ( + event: KonvaEventObject, + stateFunc: (value: boolean) => void, + ) => { event.evt.preventDefault(); - setContextMenuOpen(true); - if (event.target.parent?.id()) { - setContextMenuAnchorPoint({ - x: event.evt.pageX, - y: event.evt.pageY, - id: event.target.parent.id(), - }); - } + stateFunc(true); + + setContextMenuAnchorPoint({ + x: event.evt.pageX, + y: event.evt.pageY, + // id of parent is for TIles, id of target is for Lines + id: event.target.parent?.id() ? event.target.parent.id() : event.target.attrs.id, + }); }; const handleClick = useCallback(() => { @@ -28,9 +35,19 @@ export const useContextMenu = () => { id: contextMenuAnchorPoint.id, roomId: room?.roomId, }; + setContextMenuOpen(false); socket && socket.emit('tile-delete', deleteData); - contextMenu && setContextMenuOpen(false); }, []); - return { contextMenu, handleContextMenu, handleClick, contextMenuAnchorPoint }; + const handleRemoveLine = () => { + removeConnection(contextMenuAnchorPoint.id); + const deleteData = { + id: contextMenuAnchorPoint.id, + roomId: room?.roomId, + }; + setLineContextMenuOpen(false); + socket && socket.emit('line-delete', deleteData); + }; + + return { contextMenu, handleContextMenu, handleClick, handleRemoveLine, contextMenuAnchorPoint }; }; diff --git a/frontend/src/pages/CanvasPage.tsx b/frontend/src/pages/CanvasPage.tsx index d8b5dcb..646c122 100644 --- a/frontend/src/pages/CanvasPage.tsx +++ b/frontend/src/pages/CanvasPage.tsx @@ -1,20 +1,22 @@ import React from 'react'; import Board from '../components/Board/Board'; import Sidebar from '../components/Sidebar/Sidebar'; -import RightClickMenu from '../components/ContextMenus/RightClickMenu'; -import { useContextMenuState } from '../state/ContextMenuState'; -import InfoComponent from '../components/Forms/InfoComponent'; import { useWindowFocus } from '../hooks/useWindowFocus'; +import InfoComponent from '../components/Forms/InfoComponent'; import SelectLampForm from '../components/Forms/SelectLampForm'; +import { useContextMenuState } from '../state/ContextMenuState'; +import TileRightClickMenu from '../components/ContextMenus/TileRightClickMenu'; +import LineRightClickMenu from '../components/ContextMenus/LineRightClickMenu'; const CanvasPage = () => { // Add Cursor here. const socket = useWebSocketState((state) => state.socket); - const contextMenuOpen = useContextMenuState((state) => state.contextMenuOpen); + const { contextMenuOpen, lineContextMenuOpen } = useContextMenuState((state) => state); useWindowFocus(); return ( <> - {contextMenuOpen === true && } + {contextMenuOpen === true && } + {lineContextMenuOpen === true && } diff --git a/frontend/src/state/ContextMenuState.tsx b/frontend/src/state/ContextMenuState.tsx index 265863e..220d1e1 100644 --- a/frontend/src/state/ContextMenuState.tsx +++ b/frontend/src/state/ContextMenuState.tsx @@ -4,8 +4,10 @@ import { mountStoreDevtool } from 'simple-zustand-devtools'; export type ContextMenuStateType = { panelOpen: boolean; contextMenuOpen: boolean; + lineContextMenuOpen: boolean; setPanelOpen: (value: boolean) => void; setContextMenuOpen: (value: boolean) => void; + setLineContextMenuOpen: (value: boolean) => void; contextMenuAnchorPoint: { x: number; y: number; id: string }; setContextMenuAnchorPoint: (value: { x: number; y: number; id: string }) => void; }; @@ -13,11 +15,13 @@ export type ContextMenuStateType = { export const useContextMenuState = create((set) => ({ panelOpen: false, contextMenuOpen: false, + lineContextMenuOpen: false, contextMenuAnchorPoint: { x: 0, y: 0, id: '' }, setPanelOpen: (value: boolean) => set(() => ({ panelOpen: value })), setContextMenuAnchorPoint: (value: { x: number; y: number; id: string }) => set(() => ({ contextMenuAnchorPoint: value })), setContextMenuOpen: (value: boolean) => set(() => ({ contextMenuOpen: value })), + setLineContextMenuOpen: (value: boolean) => set(() => ({ lineContextMenuOpen: value })), })); diff --git a/frontend/src/state/SyntaxTreeState.tsx b/frontend/src/state/SyntaxTreeState.tsx index 264d22e..da6e82f 100644 --- a/frontend/src/state/SyntaxTreeState.tsx +++ b/frontend/src/state/SyntaxTreeState.tsx @@ -3,6 +3,7 @@ import create from 'zustand'; import { mountStoreDevtool } from 'simple-zustand-devtools'; +import { findConnections } from '../utils/tileConnections'; export type ConnectedTilesContextType = { fromShapeId: string | null; @@ -10,6 +11,7 @@ export type ConnectedTilesContextType = { connections: { from: string; to: string }[]; setFromShapeId: (value: string | null) => void; setConnectionPreview: (value: JSX.Element | null) => void; + removeConnection: (value: string) => void; setConnections: (value: { from: string; to: string }[]) => void; }; @@ -19,6 +21,10 @@ export const useConnectedTilesState = create((set) => connectedTiles: {}, connectionPreview: null, setFromShapeId: (value: string | null) => set(() => ({ fromShapeId: value })), + removeConnection: (value: string) => + set((state) => ({ + connections: findConnections(value, state.connections), + })), setConnections: (value: { from: string; to: string }[]) => set(() => ({ connections: value })), setConnectionPreview: (value: JSX.Element | null) => set(() => ({ connectionPreview: value })), })); diff --git a/frontend/src/utils/tileConnections.ts b/frontend/src/utils/tileConnections.ts new file mode 100644 index 0000000..705157a --- /dev/null +++ b/frontend/src/utils/tileConnections.ts @@ -0,0 +1,11 @@ +const splitIds = (id: string) => { + const [fromId, toId] = id.split('.'); + return { fromId, toId }; +}; + +const findConnections = (value: string, connections: { from: string; to: string }[]) => { + const { fromId, toId } = splitIds(value); + return connections.filter((connection) => connection.from !== fromId && connection.to !== toId); +}; + +export { splitIds, findConnections }; From b429c8c68e0624c0fd04ae6d43a17af19827cce2 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Sun, 19 Feb 2023 18:34:14 +0100 Subject: [PATCH 21/44] added type to generate AST and added more Tiles --- frontend/src/json/kacheln.json | 112 ++++++++++++++++++++++++++++ frontend/src/utils/codeGenerator.ts | 43 +++++++++++ 2 files changed, 155 insertions(+) create mode 100644 frontend/src/utils/codeGenerator.ts diff --git a/frontend/src/json/kacheln.json b/frontend/src/json/kacheln.json index bad9045..1db83f2 100644 --- a/frontend/src/json/kacheln.json +++ b/frontend/src/json/kacheln.json @@ -73,6 +73,62 @@ }, "__v": 1 }, + { + "_id": "63cdabg635c0dc3e0c5f046e", + "category": "Objekte", + "name": "Knopf", + "src": "http://localhost:9001/uploads/1674425498259.png", + "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], + "color": "#EB555B", + "width": 400, + "height": 300, + "anchors": [ + { + "type": "L", + "x": -70, + "y": 50 + }, + { + "type": "R", + "x": 270, + "y": 50 + } + ], + "textPosition": { + "x": 150, + "y": 150, + "_id": "63cdabf635c0dc3e0c5f046f" + }, + "__v": 1 + }, + { + "_id": "63cd4bg635c0dc3e0c5f046e", + "category": "Objekte", + "name": "Timer", + "src": "http://localhost:9001/uploads/1674425498259.png", + "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], + "color": "#EB555B", + "width": 400, + "height": 300, + "anchors": [ + { + "type": "L", + "x": -70, + "y": 50 + }, + { + "type": "R", + "x": 270, + "y": 50 + } + ], + "textPosition": { + "x": 150, + "y": 150, + "_id": "63cdabf635c0dc3e0c5f046f" + }, + "__v": 1 + }, { "_id": "63cdad5d35c0dc3e0c5f0475", "category": "Zustand", @@ -101,6 +157,62 @@ }, "__v": 2 }, + { + "_id": "83cdad5d35c0dc3e0c5f0475", + "category": "Zustand", + "name": "drücken", + "src": "http://localhost:9001/uploads/1674423645286.png", + "points": [0, 0, 200, 0, 300, 150, 200, 300, 0, 300, 100, 150], + "color": "#F4AECE", + "width": 300, + "height": 300, + "anchors": [ + { + "type": "L", + "x": -10, + "y": 150 + }, + { + "type": "R", + "x": 350, + "y": 150 + } + ], + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cdad5d35c0dc3e0c5f0476" + }, + "__v": 2 + }, + { + "_id": "83cdad5d35c0dc4e0c5f0475", + "category": "Zustand", + "name": "speichern", + "src": "http://localhost:9001/uploads/1674423645286.png", + "points": [0, 0, 200, 0, 300, 150, 200, 300, 0, 300, 100, 150], + "color": "#F4AECE", + "width": 300, + "height": 300, + "anchors": [ + { + "type": "L", + "x": -10, + "y": 150 + }, + { + "type": "R", + "x": 350, + "y": 150 + } + ], + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cdad5d35c0dc3e0c5f0476" + }, + "__v": 2 + }, { "_id": "63cdaff535c0dc3e0c5f0478", "category": "Konditionen", diff --git a/frontend/src/utils/codeGenerator.ts b/frontend/src/utils/codeGenerator.ts new file mode 100644 index 0000000..2292f92 --- /dev/null +++ b/frontend/src/utils/codeGenerator.ts @@ -0,0 +1,43 @@ +export enum Category { + 'Start', + 'Ende', + 'Objekte', + 'Konditionen', + 'Zustand', +} + +export enum Operator { + '===', + '!==', + '<', + '>', + '<=', + '>=', + 'in', + '!in', + 'ist leer', + 'ist nicht leer', + 'true', + 'false', +} + +export interface Token { + id: string; + type: string; + value: string; +} + +export interface AST { + program: { + type: Category.Start; + body: { + left: Token; + right: Token; + operator: Operator; + }[]; + execute: { + target: Token; + change: Token; + }; + }; +} From bfce5654e71d4b5f3b062cfa065393e9814e75f5 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Sun, 19 Feb 2023 18:35:05 +0100 Subject: [PATCH 22/44] made AST contain more programs --- frontend/src/utils/codeGenerator.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/utils/codeGenerator.ts b/frontend/src/utils/codeGenerator.ts index 2292f92..16f3352 100644 --- a/frontend/src/utils/codeGenerator.ts +++ b/frontend/src/utils/codeGenerator.ts @@ -34,10 +34,10 @@ export interface AST { left: Token; right: Token; operator: Operator; - }[]; + }; execute: { target: Token; change: Token; }; - }; + }[]; } From 5be9f609572e11a61ad590e9460ee95e9ab9454b Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Wed, 22 Feb 2023 22:45:34 +0100 Subject: [PATCH 23/44] added AST types for JS --- frontend/src/utils/JScodeGenerator.ts | 90 +++++++++++++++++++++++++++ frontend/src/utils/codeGenerator.ts | 43 ------------- 2 files changed, 90 insertions(+), 43 deletions(-) create mode 100644 frontend/src/utils/JScodeGenerator.ts delete mode 100644 frontend/src/utils/codeGenerator.ts diff --git a/frontend/src/utils/JScodeGenerator.ts b/frontend/src/utils/JScodeGenerator.ts new file mode 100644 index 0000000..a5bf106 --- /dev/null +++ b/frontend/src/utils/JScodeGenerator.ts @@ -0,0 +1,90 @@ +export enum Category { + 'Start', + 'Ende', + 'Objekte', + 'Konditionen', + 'Zustand', +} + +export enum Operator { + '===', + '!==', + '<', + '>', + '<=', + '>=', + 'in', + '!in', + 'ist leer', + 'ist nicht leer', + 'true', + 'false', +} + +export interface MemberExpression { + type: MemberExpression; + object: string; + property: { + type: string | (() => void); + value: string; + }; +} + +export interface Literal { + type: Literal; + value: string; +} + +export interface BinaryExpression { + type: BinaryExpression; + left: MemberExpression; + right: MemberExpression | Literal; + operator: Operator; + consequent: {}; +} +export interface IfStatement { + type: IfStatement; + test: BinaryExpression; +} +export interface Identifier { + type: Identifier; + name: string; +} + +export interface CallExpression { + type: CallExpression; + callee: MemberExpression; + object: Identifier; + property: Identifier; + arguments: + | MemberExpression[] + | Literal + | BinaryExpression + | IfStatement + | CallExpression + | Identifier + | (() => void); +} +export interface ExpressionStatement { + type: ExpressionStatement; + expression: MemberExpression | BinaryExpression | CallExpression | IfStatement; +} + +export interface BlockStatement { + type: BlockStatement; + body: ExpressionStatement[]; +} +export interface AST { + program: { + type: Category.Start; + body: { + left: MemberExpression; + right: MemberExpression; + operator: Operator; + }; + execute: { + target: MemberExpression; + change: MemberExpression; + }; + }[]; +} \ No newline at end of file diff --git a/frontend/src/utils/codeGenerator.ts b/frontend/src/utils/codeGenerator.ts deleted file mode 100644 index 16f3352..0000000 --- a/frontend/src/utils/codeGenerator.ts +++ /dev/null @@ -1,43 +0,0 @@ -export enum Category { - 'Start', - 'Ende', - 'Objekte', - 'Konditionen', - 'Zustand', -} - -export enum Operator { - '===', - '!==', - '<', - '>', - '<=', - '>=', - 'in', - '!in', - 'ist leer', - 'ist nicht leer', - 'true', - 'false', -} - -export interface Token { - id: string; - type: string; - value: string; -} - -export interface AST { - program: { - type: Category.Start; - body: { - left: Token; - right: Token; - operator: Operator; - }; - execute: { - target: Token; - change: Token; - }; - }[]; -} From 099118831672bc2df4c68bdcf00536ce4e5b1ac6 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Thu, 23 Feb 2023 16:11:47 +0100 Subject: [PATCH 24/44] documented all types needed for Generation --- frontend/src/astTypes.d.ts | 129 ++++++++++++++++++++++++++ frontend/src/utils/JScodeGenerator.ts | 90 ------------------ 2 files changed, 129 insertions(+), 90 deletions(-) create mode 100644 frontend/src/astTypes.d.ts delete mode 100644 frontend/src/utils/JScodeGenerator.ts diff --git a/frontend/src/astTypes.d.ts b/frontend/src/astTypes.d.ts new file mode 100644 index 0000000..0605767 --- /dev/null +++ b/frontend/src/astTypes.d.ts @@ -0,0 +1,129 @@ +/** + * This file contains the AST for the JS code generator + * The AST is based on the ESTree standard + * Basic building blocks are: + * Program + * Literals + * Identifiers + * Expression Statements + * Statements + * + * These should be used to create an AST for the generator like Babel can + * use to create a JS file + */ + +/** + * Operators used for BinaryExpressions + */ +export enum Operator { + '===', + '!==', + '<', + '>', + '<=', + '>=', + '&&', + '||', + 'true', + 'false', +} +/** + * Program + * The Startnode of the AST + * */ + +export interface Program { + type: Program; + body: IfStatement[]; + sourceType: 'module'; +} + +/** + * Literals + * all types of strings, numbers, booleans + */ +export interface Literal { + type: Literal; + value: string; +} + +/** + * Identifiers + * all types of variables, functions, classes + */ +export interface Identifier { + type: Identifier; + name: string; +} + +/** + * Expression Statements + * basic expressions + */ + +// MemeberExpression can be used for objects and its properties +export interface MemberExpression { + type: MemberExpression; + object: string; + property: { + type: string | (() => void); + value: string; + }; +} + +// BinaryExpression can be used for if statements +export interface BinaryExpression { + type: BinaryExpression; + left: MemberExpression; + right: MemberExpression | Literal; + operator: Operator; + consequent: { + type: BlockStatement; + body: ExpressionStatement[]; + }; +} +/** + * CallExpression can be used for functions. + * They also can take in other functions or Expressions as arguments + **/ +export interface CallExpression { + type: CallExpression; + callee: MemberExpression; + object: Identifier; + property: Identifier; + arguments: + | MemberExpression[] + | Literal + | BinaryExpression + | IfStatement + | CallExpression + | Identifier + | (() => void); +} + +/** + * Statements + */ + +/** + * IfStatement can be used for if statements + * Since most of the proccesses can be convered by if statements, + * we wont be needing other types of statements at first + * */ +export interface IfStatement { + type: IfStatement; + test: BinaryExpression; +} + +export interface ExpressionStatement { + type: ExpressionStatement; + expression: MemberExpression | BinaryExpression | CallExpression | IfStatement; +} +/** + * Blockstatement are the body of a program + * or the body of a function + */ +export interface BlockStatement { + type: BlockStatement; + body: ExpressionStatement[]; +} diff --git a/frontend/src/utils/JScodeGenerator.ts b/frontend/src/utils/JScodeGenerator.ts deleted file mode 100644 index a5bf106..0000000 --- a/frontend/src/utils/JScodeGenerator.ts +++ /dev/null @@ -1,90 +0,0 @@ -export enum Category { - 'Start', - 'Ende', - 'Objekte', - 'Konditionen', - 'Zustand', -} - -export enum Operator { - '===', - '!==', - '<', - '>', - '<=', - '>=', - 'in', - '!in', - 'ist leer', - 'ist nicht leer', - 'true', - 'false', -} - -export interface MemberExpression { - type: MemberExpression; - object: string; - property: { - type: string | (() => void); - value: string; - }; -} - -export interface Literal { - type: Literal; - value: string; -} - -export interface BinaryExpression { - type: BinaryExpression; - left: MemberExpression; - right: MemberExpression | Literal; - operator: Operator; - consequent: {}; -} -export interface IfStatement { - type: IfStatement; - test: BinaryExpression; -} -export interface Identifier { - type: Identifier; - name: string; -} - -export interface CallExpression { - type: CallExpression; - callee: MemberExpression; - object: Identifier; - property: Identifier; - arguments: - | MemberExpression[] - | Literal - | BinaryExpression - | IfStatement - | CallExpression - | Identifier - | (() => void); -} -export interface ExpressionStatement { - type: ExpressionStatement; - expression: MemberExpression | BinaryExpression | CallExpression | IfStatement; -} - -export interface BlockStatement { - type: BlockStatement; - body: ExpressionStatement[]; -} -export interface AST { - program: { - type: Category.Start; - body: { - left: MemberExpression; - right: MemberExpression; - operator: Operator; - }; - execute: { - target: MemberExpression; - change: MemberExpression; - }; - }[]; -} \ No newline at end of file From d8aa30a05950dd087355f3390883147b24f2be3c Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Fri, 24 Feb 2023 02:02:30 +0100 Subject: [PATCH 25/44] added code generation for js --- backend/babel.config.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 backend/babel.config.json diff --git a/backend/babel.config.json b/backend/babel.config.json new file mode 100644 index 0000000..bfef096 --- /dev/null +++ b/backend/babel.config.json @@ -0,0 +1,17 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "targets": { + "edge": "17", + "firefox": "60", + "chrome": "67", + "safari": "11.1" + }, + "useBuiltIns": "usage", + "corejs": "3.6.5" + } + ] + ] +} From 2283752471f947182ac379862368a2026c88fbce Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Fri, 24 Feb 2023 02:02:43 +0100 Subject: [PATCH 26/44] added code generation for js --- backend/package-lock.json | 305 ++++++++++++++++-- backend/package.json | 3 + backend/src/Controller/astController.ts | 16 + backend/src/Routes/ApiRoutes.ts | 10 +- frontend/src/astTypes.d.ts | 99 +++--- .../src/components/Forms/InfoComponent.tsx | 7 +- frontend/src/hooks/useCodeGeneration.tsx | 58 ++++ frontend/src/hooks/useMouse.tsx | 2 +- frontend/src/json/kaceln.old.json | 282 ++++++++++++++++ frontend/src/json/kacheln.json | 133 ++------ frontend/src/state/SyntaxTreeState.tsx | 14 +- frontend/src/utils/generateAst.ts | 185 +++++++++++ 12 files changed, 917 insertions(+), 197 deletions(-) create mode 100644 backend/src/Controller/astController.ts create mode 100644 frontend/src/hooks/useCodeGeneration.tsx create mode 100644 frontend/src/json/kaceln.old.json create mode 100644 frontend/src/utils/generateAst.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index 480f2a4..e14bbcd 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,9 +9,11 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@babel/generator": "^7.21.1", "@types/multer": "^1.4.7", "@types/node": "^18.11.9", "@types/socket.io": "^3.0.2", + "body-parser": "^1.20.2", "cors": "^2.8.5", "dotenv": "^16.0.3", "express": "^4.18.1", @@ -21,6 +23,7 @@ "socket.io": "^4.5.1" }, "devDependencies": { + "@types/babel__generator": "^7.6.4", "@types/express": "^4.17.13", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.26.0", @@ -1095,6 +1098,58 @@ "node": ">=14.0.0" } }, + "node_modules/@babel/generator": { + "version": "7.21.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.1.tgz", + "integrity": "sha512-1lT45bAYlQhFn/BHivJs43AiW2rg3/UbLyShGfF3C0KmHvO5fSghWd5kBJy30kpRRucGzXStvnnCFniCR2kXAA==", + "dependencies": { + "@babel/types": "^7.21.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.21.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.2.tgz", + "integrity": "sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw==", + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1217,11 +1272,31 @@ "dev": true, "peer": true }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", "engines": { "node": ">=6.0.0" } @@ -1229,14 +1304,12 @@ "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -1326,6 +1399,15 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "node_modules/@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, "node_modules/@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -1663,12 +1745,12 @@ } }, "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -1676,7 +1758,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -1945,9 +2027,9 @@ } }, "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "engines": { "node": ">= 0.6" } @@ -2703,6 +2785,43 @@ "node": ">= 0.10.0" } }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3600,6 +3719,17 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4322,9 +4452,9 @@ } }, "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -4904,6 +5034,14 @@ "globrex": "^0.1.2" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6199,6 +6337,48 @@ "tslib": "^2.3.1" } }, + "@babel/generator": { + "version": "7.21.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.21.1.tgz", + "integrity": "sha512-1lT45bAYlQhFn/BHivJs43AiW2rg3/UbLyShGfF3C0KmHvO5fSghWd5kBJy30kpRRucGzXStvnnCFniCR2kXAA==", + "requires": { + "@babel/types": "^7.21.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + } + } + }, + "@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==" + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==" + }, + "@babel/types": { + "version": "7.21.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.21.2.tgz", + "integrity": "sha512-3wRZSs7jiFaB8AjxiiD+VqN5DTG2iRvJGQ+qYFrs/654lg6kGTQWIOFjlBo5RaXuAZjBmP3+OQH4dmhqiiyYxw==", + "requires": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + } + }, "@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -6290,23 +6470,35 @@ "dev": true, "peer": true }, + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==" }, "@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==" }, "@jridgewell/trace-mapping": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "dev": true, "requires": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -6381,6 +6573,15 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, "@types/body-parser": { "version": "1.19.2", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", @@ -6649,12 +6850,12 @@ "dev": true }, "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "requires": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", @@ -6662,7 +6863,7 @@ "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.11.0", - "raw-body": "2.5.1", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } @@ -6867,9 +7068,9 @@ } }, "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" }, "cookie": { "version": "0.5.0", @@ -7453,6 +7654,38 @@ "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" + }, + "dependencies": { + "body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + } + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + } } }, "fast-deep-equal": { @@ -8089,6 +8322,11 @@ "argparse": "^2.0.1" } }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -8611,9 +8849,9 @@ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" }, "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "requires": { "bytes": "3.1.2", "http-errors": "2.0.0", @@ -9028,6 +9266,11 @@ "globrex": "^0.1.2" } }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==" + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index 8f0e742..6d2f76a 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,9 +12,11 @@ "author": "", "license": "ISC", "dependencies": { + "@babel/generator": "^7.21.1", "@types/multer": "^1.4.7", "@types/node": "^18.11.9", "@types/socket.io": "^3.0.2", + "body-parser": "^1.20.2", "cors": "^2.8.5", "dotenv": "^16.0.3", "express": "^4.18.1", @@ -24,6 +26,7 @@ "socket.io": "^4.5.1" }, "devDependencies": { + "@types/babel__generator": "^7.6.4", "@types/express": "^4.17.13", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.26.0", diff --git a/backend/src/Controller/astController.ts b/backend/src/Controller/astController.ts new file mode 100644 index 0000000..eb6dc77 --- /dev/null +++ b/backend/src/Controller/astController.ts @@ -0,0 +1,16 @@ +import generate from "@babel/generator"; +import { Request, Response } from "express"; +export const generateCode = async (req: Request, res: Response) => { + /** + * generate code from the ast + * if no error, send response with the generated code + * else send error + */ + try { + console.log(req.body); + const generated = await generate(req.body); + res.status(200).json({ code: generated.code }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; diff --git a/backend/src/Routes/ApiRoutes.ts b/backend/src/Routes/ApiRoutes.ts index 64b03c4..1424d8e 100644 --- a/backend/src/Routes/ApiRoutes.ts +++ b/backend/src/Routes/ApiRoutes.ts @@ -1,5 +1,7 @@ import { Router } from "express"; import { uploadFiles } from "../middleware/upload"; +import { generateCode } from "../Controller/astController"; +import bodyParser from "body-parser"; import { findTile, getAllTiles, @@ -9,15 +11,17 @@ import { } from "../Controller/tileController"; /* -* Api routes for the tiles -* uses the upload middleware to upload files -*/ + * Api routes for the tiles + * uses the upload middleware to upload files + */ const router = Router(); +const jsonParser = bodyParser.json(); router.get("/", getAllTiles); router.get("/:id", findTile); router.post("/", uploadFiles, createTile); +router.post("/ast", jsonParser, generateCode); router.put("/:id", uploadFiles, updateTile); router.delete("/:id", deleteTile); diff --git a/frontend/src/astTypes.d.ts b/frontend/src/astTypes.d.ts index 0605767..6e2bb2d 100644 --- a/frontend/src/astTypes.d.ts +++ b/frontend/src/astTypes.d.ts @@ -3,7 +3,7 @@ * The AST is based on the ESTree standard * Basic building blocks are: * Program - * Literals + * StringLiterals * Identifiers * Expression Statements * Statements @@ -13,19 +13,19 @@ */ /** - * Operators used for BinaryExpressions + * Operators used for LogicalExpressions */ export enum Operator { - '===', - '!==', - '<', - '>', - '<=', - '>=', - '&&', - '||', - 'true', - 'false', + equals = '===', + unequals = '!==', + lessThan = '<', + greaterThan = '>', + lessOrEquals = '<=', + greaterOrEquals = '>=', + and = '&&', + or = '||', + true = 'true', + false = 'false', } /** * Program @@ -33,17 +33,17 @@ export enum Operator { * */ export interface Program { - type: Program; + type: 'Program'; body: IfStatement[]; sourceType: 'module'; } /** - * Literals + * StringLiterals * all types of strings, numbers, booleans */ -export interface Literal { - type: Literal; +export interface StringLiteral { + type: 'StringLiteral'; value: string; } @@ -52,7 +52,7 @@ export interface Literal { * all types of variables, functions, classes */ export interface Identifier { - type: Identifier; + type: 'Identifier'; name: string; } @@ -63,42 +63,39 @@ export interface Identifier { // MemeberExpression can be used for objects and its properties export interface MemberExpression { - type: MemberExpression; - object: string; - property: { - type: string | (() => void); - value: string; - }; + type: 'MemberExpression'; + object: Identifier; + property: Identifier; } - -// BinaryExpression can be used for if statements export interface BinaryExpression { - type: BinaryExpression; - left: MemberExpression; - right: MemberExpression | Literal; + type: 'BinaryExpression'; + left: MemberExpression | Identifier | StringLiteral | null; + right: MemberExpression | Identifier | StringLiteral | null; + operator: Operator | null; +} + +// LogicalExpression can be used for if statements +export interface LogicalExpression { + type: 'LogicalExpression'; + left: MemberExpression | BinaryExpression | StringLiteral; + right: MemberExpression | BinaryExpression | StringLiteral; operator: Operator; - consequent: { - type: BlockStatement; - body: ExpressionStatement[]; - }; } /** * CallExpression can be used for functions. * They also can take in other functions or Expressions as arguments **/ export interface CallExpression { - type: CallExpression; + type: 'CallExpression'; callee: MemberExpression; object: Identifier; property: Identifier; arguments: | MemberExpression[] - | Literal - | BinaryExpression - | IfStatement - | CallExpression - | Identifier - | (() => void); + | StringLiteral[] + | LogicalExpression[] + | IfStatement[] + | Identifier[]; } /** @@ -110,20 +107,30 @@ export interface CallExpression { * Since most of the proccesses can be convered by if statements, * we wont be needing other types of statements at first * */ -export interface IfStatement { - type: IfStatement; - test: BinaryExpression; +export interface ExpressionStatement { + type: 'ExpressionStatement'; + expression: + | MemberExpression + | BinaryExpression + | LogicalExpression + | CallExpression + | IfStatement; } -export interface ExpressionStatement { - type: ExpressionStatement; - expression: MemberExpression | BinaryExpression | CallExpression | IfStatement; +export interface IfStatement { + type: 'IfStatement'; + test: BinaryExpression | null; + consequent: { + type: 'BlockStatement'; + body: ExpressionStatement[] | null; + }; } + /** * Blockstatement are the body of a program * or the body of a function */ export interface BlockStatement { - type: BlockStatement; + type: 'BlockStatement'; body: ExpressionStatement[]; } diff --git a/frontend/src/components/Forms/InfoComponent.tsx b/frontend/src/components/Forms/InfoComponent.tsx index a069fe8..d066e5c 100644 --- a/frontend/src/components/Forms/InfoComponent.tsx +++ b/frontend/src/components/Forms/InfoComponent.tsx @@ -4,10 +4,12 @@ import { useWebSocketState } from '../../state/WebSocketState'; import Default from '../Buttons/Default'; import RoomCodeInput from './Inputs/RoomCodeInput'; import UserDisplay from '../UserDisplay/UserDisplay'; +import { useCodeGeneration } from '../../hooks/useCodeGeneration'; const InfoComponent = () => { - const socket = useWebSocketState((state) => state.socket); const navigate = useNavigate(); + const { generateCode } = useCodeGeneration(); + const socket = useWebSocketState((state) => state.socket); const roomId = useWebSocketState((state) => state.room?.roomId); const users = useWebSocketState((state) => state.room?.users); @@ -18,13 +20,14 @@ const InfoComponent = () => { }; // Component that displays the connected users and Disconnect Button return ( -
    +
    {users?.map((user) => ( ))}
    {roomId && } +
    ); diff --git a/frontend/src/hooks/useCodeGeneration.tsx b/frontend/src/hooks/useCodeGeneration.tsx new file mode 100644 index 0000000..9245df7 --- /dev/null +++ b/frontend/src/hooks/useCodeGeneration.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import { useConnectedTilesState } from '../state/SyntaxTreeState'; +import { useBoardState } from '../state/BoardState'; +import { generateAst } from '../utils/generateAst'; + +const allowedConnections = { + Start: ['Objekte'], + Objekte: ['Zustand'], + Zustand: ['Konditionen', 'Ende'], + Konditionen: ['Objekte'], + End: [], +}; + +export const useCodeGeneration = () => { + const { connections, ast, setAst } = useConnectedTilesState((state) => state); + const { tilesOnBoard } = useBoardState((state) => state); + const generateCode = () => { + connections.forEach((connection) => { + const { from, to } = connection; + const fromSplit = from.split('_'); + const toSplit = to.split('_'); + const fromTileObject = tilesOnBoard.find((tile) => tile.id === fromSplit[0]); + const toTileObject = tilesOnBoard.find((tile) => tile.id === toSplit[0]); + const fromTile = { + id: fromSplit[0], + anchorPosition: fromSplit[1], + tileName: fromTileObject?.name, + tileCategory: fromTileObject?.category, + }; + const toTile = { + id: toSplit[0], + anchorPosition: toSplit[1], + tileName: toTileObject?.name, + tileCategory: toTileObject?.category, + }; + if (!fromTile.tileName || !toTile.tileName) return; + if ( + !allowedConnections[fromTile.tileCategory as keyof typeof allowedConnections].includes( + toTile.tileCategory as never, + ) + ) { + alert( + `ungültige Verbindung: ${fromTile.tileCategory} -> ${toTile.tileCategory}. ${ + fromTile.tileCategory + } dürfen nur mit ${ + allowedConnections[fromTile.tileCategory as keyof typeof allowedConnections] + } verbunden werden.`, + ); + } else { + generateAst(fromTile, toTile, ast, setAst); + } + }); + }; + + return { + generateCode, + }; +}; diff --git a/frontend/src/hooks/useMouse.tsx b/frontend/src/hooks/useMouse.tsx index 5109a8f..1f43325 100644 --- a/frontend/src/hooks/useMouse.tsx +++ b/frontend/src/hooks/useMouse.tsx @@ -269,7 +269,7 @@ export const useMouse = () => { const handleClick = (event: KonvaEventObject) => { const { id, 'data-type': type } = event.target.attrs; - console.log('clicked', id, type); + if (fromShapeId === `${id}_${type}`) { setFromShapeId(null); } diff --git a/frontend/src/json/kaceln.old.json b/frontend/src/json/kaceln.old.json new file mode 100644 index 0000000..1db83f2 --- /dev/null +++ b/frontend/src/json/kaceln.old.json @@ -0,0 +1,282 @@ +[ + { + "_id": "63cda4a235c0dc3e0c5f0455", + "category": "Start", + "name": "Wenn", + "src": "http://localhost:9001/uploads/1674421410153.png", + "points": [0, 0, 200, 0, 100, 150, 200, 300, 0, 300], + "color": "#f9b43d", + "width": 200, + "height": 300, + "anchors": [ + { + "type": "L", + "x": 150, + "y": 150 + } + ], + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cda4a235c0dc3e0c5f0456" + }, + "__v": 0 + }, + { + "_id": "63cda72e35c0dc3e0c5f0460", + "category": "Ende", + "name": " ", + "src": "http://localhost:9001/uploads/1674422062486.png", + "points": [0, 0, 200, 0, 200, 300, 0, 300, 100, 150], + "color": "#f9b43d", + "width": 200, + "height": 300, + "anchors": [ + { + "type": "L", + "x": 50, + "y": 150 + } + ], + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cda72e35c0dc3e0c5f0461" + }, + "__v": 0 + }, + { + "_id": "63cdabf635c0dc3e0c5f046e", + "category": "Objekte", + "name": "Smarte Lampe", + "src": "http://localhost:9001/uploads/1674425498259.png", + "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], + "color": "#EB555B", + "width": 400, + "height": 300, + "anchors": [ + { + "type": "L", + "x": -70, + "y": 50 + }, + { + "type": "R", + "x": 270, + "y": 50 + } + ], + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cdabf635c0dc3e0c5f046f" + }, + "__v": 1 + }, + { + "_id": "63cdabg635c0dc3e0c5f046e", + "category": "Objekte", + "name": "Knopf", + "src": "http://localhost:9001/uploads/1674425498259.png", + "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], + "color": "#EB555B", + "width": 400, + "height": 300, + "anchors": [ + { + "type": "L", + "x": -70, + "y": 50 + }, + { + "type": "R", + "x": 270, + "y": 50 + } + ], + "textPosition": { + "x": 150, + "y": 150, + "_id": "63cdabf635c0dc3e0c5f046f" + }, + "__v": 1 + }, + { + "_id": "63cd4bg635c0dc3e0c5f046e", + "category": "Objekte", + "name": "Timer", + "src": "http://localhost:9001/uploads/1674425498259.png", + "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], + "color": "#EB555B", + "width": 400, + "height": 300, + "anchors": [ + { + "type": "L", + "x": -70, + "y": 50 + }, + { + "type": "R", + "x": 270, + "y": 50 + } + ], + "textPosition": { + "x": 150, + "y": 150, + "_id": "63cdabf635c0dc3e0c5f046f" + }, + "__v": 1 + }, + { + "_id": "63cdad5d35c0dc3e0c5f0475", + "category": "Zustand", + "name": "an", + "src": "http://localhost:9001/uploads/1674423645286.png", + "points": [0, 0, 200, 0, 300, 150, 200, 300, 0, 300, 100, 150], + "color": "#F4AECE", + "width": 300, + "height": 300, + "anchors": [ + { + "type": "L", + "x": -10, + "y": 150 + }, + { + "type": "R", + "x": 350, + "y": 150 + } + ], + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cdad5d35c0dc3e0c5f0476" + }, + "__v": 2 + }, + { + "_id": "83cdad5d35c0dc3e0c5f0475", + "category": "Zustand", + "name": "drücken", + "src": "http://localhost:9001/uploads/1674423645286.png", + "points": [0, 0, 200, 0, 300, 150, 200, 300, 0, 300, 100, 150], + "color": "#F4AECE", + "width": 300, + "height": 300, + "anchors": [ + { + "type": "L", + "x": -10, + "y": 150 + }, + { + "type": "R", + "x": 350, + "y": 150 + } + ], + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cdad5d35c0dc3e0c5f0476" + }, + "__v": 2 + }, + { + "_id": "83cdad5d35c0dc4e0c5f0475", + "category": "Zustand", + "name": "speichern", + "src": "http://localhost:9001/uploads/1674423645286.png", + "points": [0, 0, 200, 0, 300, 150, 200, 300, 0, 300, 100, 150], + "color": "#F4AECE", + "width": 300, + "height": 300, + "anchors": [ + { + "type": "L", + "x": -10, + "y": 150 + }, + { + "type": "R", + "x": 350, + "y": 150 + } + ], + "textPosition": { + "x": 50, + "y": 50, + "_id": "63cdad5d35c0dc3e0c5f0476" + }, + "__v": 2 + }, + { + "_id": "63cdaff535c0dc3e0c5f0478", + "category": "Konditionen", + "name": "Dann", + "src": "http://localhost:9001/uploads/1674424309409.png", + "points": [0, 0, 200, 0, 300, 150, -100, 150], + "color": "#F9B43D", + "width": 400, + "height": 200, + "anchors": [ + { + "type": "L", + "x": -100, + "y": 50 + }, + { + "type": "R", + "x": 300, + "y": 50 + } + ], + "textPosition": { + "x": -50, + "y": -50, + "_id": "63cdaff535c0dc3e0c5f0479" + }, + "__v": 1 + }, + { + "_id": "63cdb2bc35c0dc3e0c5f047d", + "category": "Konditionen", + "name": "Und", + "src": "http://localhost:9001/uploads/1674425020844.png", + "points": [0, 0, 150, 0, 250, 150, 250, 250, 150, 400, 0, 400, -100, 250, -100, 150], + "color": "#E2E7DF", + "width": 350, + "height": 450, + "anchors": [ + { + "type": "TL", + "x": -90, + "y": 50 + }, + { + "type": "TR", + "x": 225, + "y": 50 + }, + { + "type": "BL", + "x": 215, + "y": 350 + }, + { + "type": "BR", + "x": -90, + "y": 350 + } + ], + "textPosition": { + "x": -50, + "y": -50, + "_id": "63cdb2bc35c0dc3e0c5f047e" + }, + "__v": 0 + } +] diff --git a/frontend/src/json/kacheln.json b/frontend/src/json/kacheln.json index 1db83f2..5d23bcf 100644 --- a/frontend/src/json/kacheln.json +++ b/frontend/src/json/kacheln.json @@ -22,33 +22,10 @@ }, "__v": 0 }, - { - "_id": "63cda72e35c0dc3e0c5f0460", - "category": "Ende", - "name": " ", - "src": "http://localhost:9001/uploads/1674422062486.png", - "points": [0, 0, 200, 0, 200, 300, 0, 300, 100, 150], - "color": "#f9b43d", - "width": 200, - "height": 300, - "anchors": [ - { - "type": "L", - "x": 50, - "y": 150 - } - ], - "textPosition": { - "x": 50, - "y": 50, - "_id": "63cda72e35c0dc3e0c5f0461" - }, - "__v": 0 - }, { "_id": "63cdabf635c0dc3e0c5f046e", "category": "Objekte", - "name": "Smarte Lampe", + "name": "Kontakt Sensor", "src": "http://localhost:9001/uploads/1674425498259.png", "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], "color": "#EB555B", @@ -74,37 +51,9 @@ "__v": 1 }, { - "_id": "63cdabg635c0dc3e0c5f046e", - "category": "Objekte", - "name": "Knopf", - "src": "http://localhost:9001/uploads/1674425498259.png", - "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], - "color": "#EB555B", - "width": 400, - "height": 300, - "anchors": [ - { - "type": "L", - "x": -70, - "y": 50 - }, - { - "type": "R", - "x": 270, - "y": 50 - } - ], - "textPosition": { - "x": 150, - "y": 150, - "_id": "63cdabf635c0dc3e0c5f046f" - }, - "__v": 1 - }, - { - "_id": "63cd4bg635c0dc3e0c5f046e", + "_id": "63cdabf635c0dc3e0c5f046e", "category": "Objekte", - "name": "Timer", + "name": "Lautsprecher", "src": "http://localhost:9001/uploads/1674425498259.png", "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], "color": "#EB555B", @@ -123,44 +72,39 @@ } ], "textPosition": { - "x": 150, - "y": 150, + "x": 50, + "y": 50, "_id": "63cdabf635c0dc3e0c5f046f" }, "__v": 1 }, { - "_id": "63cdad5d35c0dc3e0c5f0475", - "category": "Zustand", - "name": "an", - "src": "http://localhost:9001/uploads/1674423645286.png", - "points": [0, 0, 200, 0, 300, 150, 200, 300, 0, 300, 100, 150], - "color": "#F4AECE", - "width": 300, + "_id": "63cda72e35c0dc3e0c5f0460", + "category": "Ende", + "name": " ", + "src": "http://localhost:9001/uploads/1674422062486.png", + "points": [0, 0, 200, 0, 200, 300, 0, 300, 100, 150], + "color": "#f9b43d", + "width": 200, "height": 300, "anchors": [ { "type": "L", - "x": -10, - "y": 150 - }, - { - "type": "R", - "x": 350, + "x": 50, "y": 150 } ], "textPosition": { "x": 50, "y": 50, - "_id": "63cdad5d35c0dc3e0c5f0476" + "_id": "63cda72e35c0dc3e0c5f0461" }, - "__v": 2 + "__v": 0 }, { - "_id": "83cdad5d35c0dc3e0c5f0475", + "_id": "63cdad5d35c0dc3e0c5f0475", "category": "Zustand", - "name": "drücken", + "name": "auf", "src": "http://localhost:9001/uploads/1674423645286.png", "points": [0, 0, 200, 0, 300, 150, 200, 300, 0, 300, 100, 150], "color": "#F4AECE", @@ -186,9 +130,9 @@ "__v": 2 }, { - "_id": "83cdad5d35c0dc4e0c5f0475", + "_id": "63cdad5d35c0dc3e0c5f0475", "category": "Zustand", - "name": "speichern", + "name": "gib Bescheid", "src": "http://localhost:9001/uploads/1674423645286.png", "points": [0, 0, 200, 0, 300, 150, 200, 300, 0, 300, 100, 150], "color": "#F4AECE", @@ -240,43 +184,6 @@ "_id": "63cdaff535c0dc3e0c5f0479" }, "__v": 1 - }, - { - "_id": "63cdb2bc35c0dc3e0c5f047d", - "category": "Konditionen", - "name": "Und", - "src": "http://localhost:9001/uploads/1674425020844.png", - "points": [0, 0, 150, 0, 250, 150, 250, 250, 150, 400, 0, 400, -100, 250, -100, 150], - "color": "#E2E7DF", - "width": 350, - "height": 450, - "anchors": [ - { - "type": "TL", - "x": -90, - "y": 50 - }, - { - "type": "TR", - "x": 225, - "y": 50 - }, - { - "type": "BL", - "x": 215, - "y": 350 - }, - { - "type": "BR", - "x": -90, - "y": 350 - } - ], - "textPosition": { - "x": -50, - "y": -50, - "_id": "63cdb2bc35c0dc3e0c5f047e" - }, - "__v": 0 } ] + diff --git a/frontend/src/state/SyntaxTreeState.tsx b/frontend/src/state/SyntaxTreeState.tsx index da6e82f..675a0b2 100644 --- a/frontend/src/state/SyntaxTreeState.tsx +++ b/frontend/src/state/SyntaxTreeState.tsx @@ -2,13 +2,23 @@ // also saves the connections between them import create from 'zustand'; +import { IfStatement } from '../AstTypes'; import { mountStoreDevtool } from 'simple-zustand-devtools'; import { findConnections } from '../utils/tileConnections'; - +interface ASTType { + type: 'File'; + errors: []; + program: { + type: 'Program'; + body: IfStatement[]; + } | null; +} export type ConnectedTilesContextType = { fromShapeId: string | null; + ast: ASTType | null; connectionPreview: JSX.Element | null; connections: { from: string; to: string }[]; + setAst: (value: ASTType | null) => void; setFromShapeId: (value: string | null) => void; setConnectionPreview: (value: JSX.Element | null) => void; removeConnection: (value: string) => void; @@ -16,10 +26,12 @@ export type ConnectedTilesContextType = { }; export const useConnectedTilesState = create((set) => ({ + ast: null, connections: [], fromShapeId: null, connectedTiles: {}, connectionPreview: null, + setAst: (value: ASTType | null) => set(() => ({ ast: value })), setFromShapeId: (value: string | null) => set(() => ({ fromShapeId: value })), removeConnection: (value: string) => set((state) => ({ diff --git a/frontend/src/utils/generateAst.ts b/frontend/src/utils/generateAst.ts new file mode 100644 index 0000000..b552832 --- /dev/null +++ b/frontend/src/utils/generateAst.ts @@ -0,0 +1,185 @@ +import { IfStatement, CallExpression, StringLiteral } from '../AstTypes'; +enum Operator { + equals = '===', + unequals = '!==', + lessThan = '<', + greaterThan = '>', + lessOrEquals = '<=', + greaterOrEquals = '>=', + and = '&&', + or = '||', + true = 'true', + false = 'false', +} + +interface NodeType { + id: string; + anchorPosition: string; + tileName: string | undefined; + tileCategory: string | undefined; +} + +interface ASTType { + type: 'File'; + errors: []; + program: { + type: 'Program'; + body: IfStatement[]; + } | null; +} + +export const generateAst = ( + fromNode: NodeType | undefined, + toNode: NodeType | undefined, + ast: ASTType | null, + setAst: (ast: ASTType | null) => void, +) => { + if ( + fromNode?.tileCategory === undefined || + fromNode.tileName === undefined || + toNode?.tileName === undefined || + toNode?.tileCategory === undefined + ) + return; + if (fromNode.tileCategory === 'Start' && fromNode.tileName === 'Wenn') { + ast = { + type: 'File', + errors: [], + program: { + type: 'Program', + body: [ + { + type: 'IfStatement', + test: null, + consequent: { + type: 'BlockStatement', + body: null, + }, + }, + ], + }, + }; + setAst(ast); + } + if ( + toNode.tileCategory === 'Objekte' && + ast !== null && + ast.program?.body[0].type === 'IfStatement' + ) { + ast.program.body[0].test = { + type: 'BinaryExpression', + left: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'kontaktSensor', + }, + property: { + type: 'Identifier', + name: 'state', + }, + }, + right: null, + operator: null, + }; + setAst(ast); + } + if ( + toNode.tileCategory === 'Zustand' && + fromNode.tileCategory === 'Objekte' && + ast !== null && + ast.program?.body[0].type === 'IfStatement' && + ast.program?.body[0].test?.type === 'BinaryExpression' + ) { + ast.program.body[0].test.right = { + type: 'StringLiteral', + value: 'open', + }; + ast.program.body[0].test.operator = Operator.equals; + setAst(ast); + } + + if ( + fromNode.tileCategory === 'Zustand' && + toNode.tileCategory === 'Konditionen' && + toNode.tileName === 'Dann' && + ast !== null && + ast.program?.body[0].type === 'IfStatement' + ) { + ast.program.body[0].consequent = { + type: 'BlockStatement', + body: [], + }; + setAst(ast); + } + if ( + fromNode.tileCategory === 'Konditionen' && + fromNode.tileName === 'Dann' && + toNode.tileCategory === 'Objekte' && + ast !== null && + ast.program?.body[0].consequent.type === 'BlockStatement' + ) { + ast.program.body[0].consequent.body = [ + { + type: 'ExpressionStatement', + expression: { + type: 'CallExpression', + callee: { + type: 'MemberExpression', + object: { + type: 'Identifier', + name: 'client', + }, + property: { + type: 'Identifier', + name: 'publish', + }, + }, + arguments: [ + { + type: 'StringLiteral', + value: 'speaker/speaker1/set', + } as StringLiteral, + ] as StringLiteral[], + } as CallExpression, + }, + ]; + setAst(ast); + } + + if ( + fromNode.tileCategory === 'Objekte' && + toNode.tileCategory === 'Zustand' && + ast !== null && + ast.program?.body[0].consequent.body !== null && + ast.program?.body[0].consequent.type === 'BlockStatement' && + ast.program?.body[0].consequent.body.length !== 0 && + ast?.program?.body[0].consequent.body[0].expression.type === 'CallExpression' && + ast?.program?.body[0].consequent.body[0].expression.arguments.length !== 0 + ) { + ast?.program?.body[0].consequent.body[0].expression.arguments.push({ + type: 'StringLiteral', + value: 'on', + } as any); + + setAst(ast); + } + + if (fromNode.tileCategory === 'Zustand' && toNode.tileCategory === 'End') { + // send ast to backend + const backendUrl = process.env.REACT_APP_BACKEND_URL; + (async () => { + try { + const response = await fetch(`${backendUrl}/ast`, { + method: 'POST', + body: JSON.stringify(ast, null, 2), + }); + const data = await response.json(); + console.log(data); + } catch (error) { + alert(error); + } + })(); + } + console.log(JSON.stringify(ast, null, 2)); +}; From 66586bc1c6d0f38a131f308175a8838b24b6acf7 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Thu, 23 Mar 2023 19:46:50 +0100 Subject: [PATCH 27/44] added ASTNodes to JSON --- frontend/src/astTypes.d.ts | 13 +- frontend/src/hooks/useCodeGeneration.tsx | 2 + frontend/src/hooks/useMouse.tsx | 18 +- frontend/src/json/kaceln.old.json | 120 +++++++++++++ frontend/src/json/kacheln.json | 207 +++++++++++++++++++++++ frontend/src/state/SyntaxTreeState.tsx | 11 +- frontend/src/types.d.ts | 1 + frontend/src/utils/generateAst.ts | 77 ++------- 8 files changed, 375 insertions(+), 74 deletions(-) diff --git a/frontend/src/astTypes.d.ts b/frontend/src/astTypes.d.ts index 6e2bb2d..6c0d00e 100644 --- a/frontend/src/astTypes.d.ts +++ b/frontend/src/astTypes.d.ts @@ -15,6 +15,15 @@ /** * Operators used for LogicalExpressions */ +export interface ASTType { + type: 'File'; + errors: []; + program: { + type: 'Program'; + body: IfStatement[]; + }; +} + export enum Operator { equals = '===', unequals = '!==', @@ -119,10 +128,10 @@ export interface ExpressionStatement { export interface IfStatement { type: 'IfStatement'; - test: BinaryExpression | null; + test: BinaryExpression; consequent: { type: 'BlockStatement'; - body: ExpressionStatement[] | null; + body: ExpressionStatement[]; }; } diff --git a/frontend/src/hooks/useCodeGeneration.tsx b/frontend/src/hooks/useCodeGeneration.tsx index 9245df7..60caf67 100644 --- a/frontend/src/hooks/useCodeGeneration.tsx +++ b/frontend/src/hooks/useCodeGeneration.tsx @@ -26,12 +26,14 @@ export const useCodeGeneration = () => { anchorPosition: fromSplit[1], tileName: fromTileObject?.name, tileCategory: fromTileObject?.category, + astNode: fromTileObject?.astNode, }; const toTile = { id: toSplit[0], anchorPosition: toSplit[1], tileName: toTileObject?.name, tileCategory: toTileObject?.category, + astNode: toTileObject?.astNode, }; if (!fromTile.tileName || !toTile.tileName) return; if ( diff --git a/frontend/src/hooks/useMouse.tsx b/frontend/src/hooks/useMouse.tsx index 1f43325..0465a81 100644 --- a/frontend/src/hooks/useMouse.tsx +++ b/frontend/src/hooks/useMouse.tsx @@ -71,8 +71,19 @@ export const useMouse = () => { ); if (tile) { - const { _id, name, src, color, points, textPosition, anchors, width, height, category } = - tile; + const { + _id, + src, + name, + color, + width, + height, + points, + anchors, + category, + astNode, + textPosition, + } = tile; const dragPayload = JSON.stringify({ id: uuidv4(), _id: _id, @@ -83,6 +94,7 @@ export const useMouse = () => { height: height, points: points, anchors: anchors, + astNode: astNode, category: category, textPosition: textPosition, offsetX: event.nativeEvent.offsetX, @@ -112,6 +124,7 @@ export const useMouse = () => { offsetX, offsetY, category, + astNode, textPosition, } = JSON.parse(draggedData); if (x && y) { @@ -125,6 +138,7 @@ export const useMouse = () => { height: height, points: points, anchors: anchors, + astNode: astNode, category: category, textPosition: textPosition, x: x - (offsetX - width / 2), diff --git a/frontend/src/json/kaceln.old.json b/frontend/src/json/kaceln.old.json index 1db83f2..1767814 100644 --- a/frontend/src/json/kaceln.old.json +++ b/frontend/src/json/kaceln.old.json @@ -8,6 +8,64 @@ "color": "#f9b43d", "width": 200, "height": 300, + "astNode": { + "javaScript": { + "type": "IfStatement", + "test": { + "type": "BinaryExpression", + "left": { + "type": "MemberExpression", + "object": null, + "property": { + "type": "Identifier", + "name": "state" + } + }, + "right": null, + "operator": "===" + }, + "consequent": { + "type": "BlockStatement", + "body": null + } + }, + "python": { + "type": "IfStatement", + "test": null, + "consequent": { + "type": "BlockStatement", + "body": [ + { + "type": "ExpressionStatement", + "expression": { + "type": "CallExpression", + "callee": { + "type": "MemberExpression", + "object": { + "type": "Identifier", + "name": "client" + }, + "property": { + "type": "Identifier", + "name": "publish" + } + }, + "arguments": [ + { + "type": "StringLiteral", + "value": "" + }, + { + "type": "StringLiteral", + "value": "" + } + ] + } + } + ] + } + } + }, "anchors": [ { "type": "L", @@ -31,6 +89,16 @@ "color": "#f9b43d", "width": 200, "height": 300, + "astNode": { + "javaScript": { + "type": "BlockStatement", + "body": null + }, + "python": { + "type": "BlockStatement", + "body": null + } + }, "anchors": [ { "type": "L", @@ -54,6 +122,13 @@ "color": "#EB555B", "width": 400, "height": 300, + "astNode": { + "javaScript": { + "type": "Identifier", + "name": "lampe" + }, + "MQTTtopic": "lampe/lampe1/set" + }, "anchors": [ { "type": "L", @@ -82,6 +157,12 @@ "color": "#EB555B", "width": 400, "height": 300, + "astNode": { + "javaScript": { + "type": "Identifier", + "name": "knopf" + } + }, "anchors": [ { "type": "L", @@ -110,6 +191,12 @@ "color": "#EB555B", "width": 400, "height": 300, + "astNode": { + "javaScript": { + "type": "Identifier", + "name": "timer" + } + }, "anchors": [ { "type": "L", @@ -138,6 +225,12 @@ "color": "#F4AECE", "width": 300, "height": 300, + "astNode": { + "javaScript": { + "type": "StringLiteral", + "value": "on" + } + }, "anchors": [ { "type": "L", @@ -166,6 +259,12 @@ "color": "#F4AECE", "width": 300, "height": 300, + "astNode": { + "javaScript": { + "type": "StringLiteral", + "value": "pressed" + } + }, "anchors": [ { "type": "L", @@ -194,6 +293,12 @@ "color": "#F4AECE", "width": 300, "height": 300, + "astNode": { + "javaScript": { + "type": "StringLiteral", + "value": "save" + } + }, "anchors": [ { "type": "L", @@ -250,6 +355,21 @@ "color": "#E2E7DF", "width": 350, "height": 450, + "astNode": { + "javaScript": { + "type": "LogicalExpression", + "left": null, + "right": { + "right": { + "type": "BinaryExpression", + "left": null, + "right": null, + "operator": "===" + } + }, + "operator": "&&" + } + }, "anchors": [ { "type": "TL", diff --git a/frontend/src/json/kacheln.json b/frontend/src/json/kacheln.json index 5d23bcf..6c45cf2 100644 --- a/frontend/src/json/kacheln.json +++ b/frontend/src/json/kacheln.json @@ -8,6 +8,64 @@ "color": "#f9b43d", "width": 200, "height": 300, + "astNode": { + "javaScript": { + "type": "IfStatement", + "test": { + "type": "BinaryExpression", + "left": { + "type": "MemberExpression", + "object": null, + "property": { + "type": "Identifier", + "name": "state" + } + }, + "right": null, + "operator": "===" + }, + "consequent": { + "type": "BlockStatement", + "body": null + } + }, + "python": { + "type": "IfStatement", + "test": null, + "consequent": { + "type": "BlockStatement", + "body": [ + { + "type": "ExpressionStatement", + "expression": { + "type": "CallExpression", + "callee": { + "type": "MemberExpression", + "object": { + "type": "Identifier", + "name": "client" + }, + "property": { + "type": "Identifier", + "name": "publish" + } + }, + "arguments": [ + { + "type": "StringLiteral", + "value": "" + }, + { + "type": "StringLiteral", + "value": "" + } + ] + } + } + ] + } + } + }, "anchors": [ { "type": "L", @@ -31,6 +89,13 @@ "color": "#EB555B", "width": 400, "height": 300, + "astNode": { + "javaScript": { + "type": "Identifier", + "name": "sensor" + }, + "MQTTtopic": "sensor/sensor1/set" + }, "anchors": [ { "type": "L", @@ -59,6 +124,13 @@ "color": "#EB555B", "width": 400, "height": 300, + "astNode": { + "javaScript": { + "type": "Identifier", + "name": "lautsprecher" + }, + "MQTTtopic": "lautsprecher/lautsprecher1/set" + }, "anchors": [ { "type": "L", @@ -101,6 +173,76 @@ }, "__v": 0 }, + { + "_id": "63cd4bg635c0dc3e0c5f046e", + "category": "Objekte", + "name": "Timer", + "src": "http://localhost:9001/uploads/1674425498259.png", + "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], + "color": "#EB555B", + "width": 400, + "height": 300, + "astNode": { + "javaScript": { + "type": "Identifier", + "name": "timer" + }, + "MQTTtopic": "timer/set" + }, + "anchors": [ + { + "type": "L", + "x": -70, + "y": 50 + }, + { + "type": "R", + "x": 270, + "y": 50 + } + ], + "textPosition": { + "x": 150, + "y": 150, + "_id": "63cdabf635c0dc3e0c5f046f" + }, + "__v": 1 + }, + { + "_id": "63cdabg635c0dc3e0c5f046e", + "category": "Objekte", + "name": "Knopf", + "src": "http://localhost:9001/uploads/1674425498259.png", + "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], + "color": "#EB555B", + "width": 400, + "height": 300, + "astNode": { + "javaScript": { + "type": "Identifier", + "name": "knopf" + }, + "MQTTtopic": "button/button1/set" + }, + "anchors": [ + { + "type": "L", + "x": -70, + "y": 50 + }, + { + "type": "R", + "x": 270, + "y": 50 + } + ], + "textPosition": { + "x": 150, + "y": 150, + "_id": "63cdabf635c0dc3e0c5f046f" + }, + "__v": 1 + }, { "_id": "63cdad5d35c0dc3e0c5f0475", "category": "Zustand", @@ -110,6 +252,12 @@ "color": "#F4AECE", "width": 300, "height": 300, + "astNode": { + "javaScript": { + "type": "StringLiteral", + "value": "open" + } + }, "anchors": [ { "type": "L", @@ -138,6 +286,12 @@ "color": "#F4AECE", "width": 300, "height": 300, + "astNode": { + "javaScript": { + "type": "StringLiteral", + "value": "on" + } + }, "anchors": [ { "type": "L", @@ -184,6 +338,59 @@ "_id": "63cdaff535c0dc3e0c5f0479" }, "__v": 1 + }, + { + "_id": "63cdb2bc35c0dc3e0c5f047d", + "category": "Konditionen", + "name": "Und", + "src": "http://localhost:9001/uploads/1674425020844.png", + "points": [0, 0, 150, 0, 250, 150, 250, 250, 150, 400, 0, 400, -100, 250, -100, 150], + "color": "#E2E7DF", + "width": 350, + "height": 450, + "astNode": { + "javaScript": { + "type": "LogicalExpression", + "left": null, + "right": { + "right": { + "type": "BinaryExpression", + "left": null, + "right": null, + "operator": "===" + } + }, + "operator": "&&" + } + }, + "anchors": [ + { + "type": "TL", + "x": -90, + "y": 50 + }, + { + "type": "TR", + "x": 225, + "y": 50 + }, + { + "type": "BL", + "x": 215, + "y": 350 + }, + { + "type": "BR", + "x": -90, + "y": 350 + } + ], + "textPosition": { + "x": -50, + "y": -50, + "_id": "63cdb2bc35c0dc3e0c5f047e" + }, + "__v": 0 } ] diff --git a/frontend/src/state/SyntaxTreeState.tsx b/frontend/src/state/SyntaxTreeState.tsx index 675a0b2..2b2ce6f 100644 --- a/frontend/src/state/SyntaxTreeState.tsx +++ b/frontend/src/state/SyntaxTreeState.tsx @@ -2,17 +2,10 @@ // also saves the connections between them import create from 'zustand'; -import { IfStatement } from '../AstTypes'; +import { ASTType } from '../AstTypes'; import { mountStoreDevtool } from 'simple-zustand-devtools'; import { findConnections } from '../utils/tileConnections'; -interface ASTType { - type: 'File'; - errors: []; - program: { - type: 'Program'; - body: IfStatement[]; - } | null; -} + export type ConnectedTilesContextType = { fromShapeId: string | null; ast: ASTType | null; diff --git a/frontend/src/types.d.ts b/frontend/src/types.d.ts index c8f472a..268c92e 100644 --- a/frontend/src/types.d.ts +++ b/frontend/src/types.d.ts @@ -35,6 +35,7 @@ export type Tile = { src: string; name: string; color: string; + astNode?: { javaScript: any; python: any; MQTTtopic?: string }; width: number; height: number; points: number[]; diff --git a/frontend/src/utils/generateAst.ts b/frontend/src/utils/generateAst.ts index b552832..9c978e1 100644 --- a/frontend/src/utils/generateAst.ts +++ b/frontend/src/utils/generateAst.ts @@ -1,4 +1,6 @@ -import { IfStatement, CallExpression, StringLiteral } from '../AstTypes'; +import { CallExpression, Identifier, IfStatement, StringLiteral } from './../AstTypes.d'; +import { ASTType } from '../AstTypes'; + enum Operator { equals = '===', unequals = '!==', @@ -17,15 +19,7 @@ interface NodeType { anchorPosition: string; tileName: string | undefined; tileCategory: string | undefined; -} - -interface ASTType { - type: 'File'; - errors: []; - program: { - type: 'Program'; - body: IfStatement[]; - } | null; + astNode: any; } export const generateAst = ( @@ -41,48 +35,17 @@ export const generateAst = ( toNode?.tileCategory === undefined ) return; - if (fromNode.tileCategory === 'Start' && fromNode.tileName === 'Wenn') { - ast = { + if (fromNode.tileCategory === 'Start' && toNode.tileCategory === 'Objekte') { + const startNode = fromNode.astNode.javaScript as IfStatement; + startNode.test.left = toNode.astNode.javaScript as Identifier; + setAst({ type: 'File', errors: [], program: { type: 'Program', - body: [ - { - type: 'IfStatement', - test: null, - consequent: { - type: 'BlockStatement', - body: null, - }, - }, - ], - }, - }; - setAst(ast); - } - if ( - toNode.tileCategory === 'Objekte' && - ast !== null && - ast.program?.body[0].type === 'IfStatement' - ) { - ast.program.body[0].test = { - type: 'BinaryExpression', - left: { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: 'kontaktSensor', - }, - property: { - type: 'Identifier', - name: 'state', - }, + body: [startNode], }, - right: null, - operator: null, - }; - setAst(ast); + }); } if ( toNode.tileCategory === 'Zustand' && @@ -91,11 +54,7 @@ export const generateAst = ( ast.program?.body[0].type === 'IfStatement' && ast.program?.body[0].test?.type === 'BinaryExpression' ) { - ast.program.body[0].test.right = { - type: 'StringLiteral', - value: 'open', - }; - ast.program.body[0].test.operator = Operator.equals; + ast.program.body[0].test.right = toNode.astNode.javaScript as Identifier; setAst(ast); } @@ -106,10 +65,7 @@ export const generateAst = ( ast !== null && ast.program?.body[0].type === 'IfStatement' ) { - ast.program.body[0].consequent = { - type: 'BlockStatement', - body: [], - }; + ast.program.body[0].consequent.body = []; setAst(ast); } if ( @@ -138,7 +94,7 @@ export const generateAst = ( arguments: [ { type: 'StringLiteral', - value: 'speaker/speaker1/set', + value: toNode.astNode.MQTTTopic, } as StringLiteral, ] as StringLiteral[], } as CallExpression, @@ -157,10 +113,9 @@ export const generateAst = ( ast?.program?.body[0].consequent.body[0].expression.type === 'CallExpression' && ast?.program?.body[0].consequent.body[0].expression.arguments.length !== 0 ) { - ast?.program?.body[0].consequent.body[0].expression.arguments.push({ - type: 'StringLiteral', - value: 'on', - } as any); + ast?.program?.body[0].consequent.body[0].expression.arguments.push( + toNode.astNode.javaScript as any, + ); setAst(ast); } From 9c6fcd8f4e27638e8cbdf8c56104f6c6149a6242 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Thu, 23 Mar 2023 22:04:00 +0100 Subject: [PATCH 28/44] get code back from backend and add editor package --- backend/src/Controller/socketController.ts | 2 + backend/types/socket.types.d.ts | 2 + frontend/package-lock.json | 16 ++++++ frontend/package.json | 1 + frontend/src/components/Board/Board.tsx | 7 +-- frontend/src/components/Tiles/Tile.tsx | 2 + frontend/src/hooks/useCodeGeneration.tsx | 6 ++- frontend/src/hooks/useMouse.tsx | 4 ++ frontend/src/json/kacheln.json | 35 +++++++++++++ frontend/src/state/SyntaxTreeState.tsx | 4 ++ frontend/src/utils/generateAst.ts | 57 ++++++---------------- 11 files changed, 90 insertions(+), 46 deletions(-) diff --git a/backend/src/Controller/socketController.ts b/backend/src/Controller/socketController.ts index 105517e..0d6ca10 100644 --- a/backend/src/Controller/socketController.ts +++ b/backend/src/Controller/socketController.ts @@ -77,12 +77,14 @@ export const tileDrop = ( x: data.tile.x, y: data.tile.y, id: data.tile.id, + _id: data.tile._id, src: data.tile.src, name: data.tile.name, width: data.tile.width, color: data.tile.color, height: data.tile.height, points: data.tile.points, + astNode: data.tile.astNode, anchors: data.tile.anchors, category: data.tile.category, textPosition: data.tile.textPosition, diff --git a/backend/types/socket.types.d.ts b/backend/types/socket.types.d.ts index 39552ad..902c959 100644 --- a/backend/types/socket.types.d.ts +++ b/backend/types/socket.types.d.ts @@ -19,6 +19,7 @@ export type NewTile = { x: number; y: number; id: string; + _id: string; src: string; name: string; width: number; @@ -26,6 +27,7 @@ export type NewTile = { height: number; category: string; points: number[]; + astNode?: { javaScript: any; python: any; MQTTtopic?: string }; textPosition: { x: number; y: number }; anchors: { type: string; x: number; y: number }[]; }; diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bf2069b..f182d96 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -25,6 +25,7 @@ "react-konva": "^18.2.1", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", + "react-simple-code-editor": "^0.13.1", "react-toastify": "^9.1.1", "simple-zustand-devtools": "^1.1.0", "socket.io-client": "^4.5.1", @@ -12696,6 +12697,15 @@ } } }, + "node_modules/react-simple-code-editor": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.13.1.tgz", + "integrity": "sha512-XYeVwRZwgyKtjNIYcAEgg2FaQcCZwhbarnkJIV20U2wkCU9q/CPFBo8nRXrK4GXUz3AvbqZFsZRrpUTkqqEYyQ==", + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, "node_modules/react-toastify": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz", @@ -24459,6 +24469,12 @@ "workbox-webpack-plugin": "^6.4.1" } }, + "react-simple-code-editor": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/react-simple-code-editor/-/react-simple-code-editor-0.13.1.tgz", + "integrity": "sha512-XYeVwRZwgyKtjNIYcAEgg2FaQcCZwhbarnkJIV20U2wkCU9q/CPFBo8nRXrK4GXUz3AvbqZFsZRrpUTkqqEYyQ==", + "requires": {} + }, "react-toastify": { "version": "9.1.1", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-9.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9cd8be6..27062a3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "react-konva": "^18.2.1", "react-router-dom": "^6.3.0", "react-scripts": "5.0.1", + "react-simple-code-editor": "^0.13.1", "react-toastify": "^9.1.1", "simple-zustand-devtools": "^1.1.0", "socket.io-client": "^4.5.1", diff --git a/frontend/src/components/Board/Board.tsx b/frontend/src/components/Board/Board.tsx index 237cea0..0327299 100644 --- a/frontend/src/components/Board/Board.tsx +++ b/frontend/src/components/Board/Board.tsx @@ -115,15 +115,16 @@ const Board = () => { key={tileObject.tile.id} x={tileObject.tile.x} y={tileObject.tile.y} - _id={tileObject.tile._id} id={tileObject.tile.id} + _id={tileObject.tile._id} src={tileObject.tile.src} name={tileObject.tile.name} - anchors={tileObject.tile.anchors} color={tileObject.tile.color} width={tileObject.tile.width} - height={tileObject.tile.height} points={tileObject.tile.points} + height={tileObject.tile.height} + anchors={tileObject.tile.anchors} + astNode={tileObject.tile.astNode} category={tileObject.tile.category} textPosition={tileObject.tile.textPosition} /> diff --git a/frontend/src/components/Tiles/Tile.tsx b/frontend/src/components/Tiles/Tile.tsx index 2f3960b..0fdbdfb 100644 --- a/frontend/src/components/Tiles/Tile.tsx +++ b/frontend/src/components/Tiles/Tile.tsx @@ -21,6 +21,7 @@ const Tile: React.FC = ({ height, points, anchors, + astNode, category, textPosition, }) => { @@ -62,6 +63,7 @@ const Tile: React.FC = ({ data-width={width} data-height={height} data-category={category} + data-astNode={JSON.stringify(astNode)} onDragEnd={updateTilePosition} onContextMenu={(e) => handleContextMenu(e, setContextMenuOpen)} data-points={JSON.stringify(points)} diff --git a/frontend/src/hooks/useCodeGeneration.tsx b/frontend/src/hooks/useCodeGeneration.tsx index 60caf67..71efe0a 100644 --- a/frontend/src/hooks/useCodeGeneration.tsx +++ b/frontend/src/hooks/useCodeGeneration.tsx @@ -12,7 +12,9 @@ const allowedConnections = { }; export const useCodeGeneration = () => { - const { connections, ast, setAst } = useConnectedTilesState((state) => state); + const { connections, ast, setAst, generatedCode, setGeneratedCode } = useConnectedTilesState( + (state) => state, + ); const { tilesOnBoard } = useBoardState((state) => state); const generateCode = () => { connections.forEach((connection) => { @@ -49,7 +51,7 @@ export const useCodeGeneration = () => { } verbunden werden.`, ); } else { - generateAst(fromTile, toTile, ast, setAst); + generateAst(fromTile, toTile, ast, setAst, generatedCode, setGeneratedCode); } }); }; diff --git a/frontend/src/hooks/useMouse.tsx b/frontend/src/hooks/useMouse.tsx index 0465a81..985bee7 100644 --- a/frontend/src/hooks/useMouse.tsx +++ b/frontend/src/hooks/useMouse.tsx @@ -172,6 +172,7 @@ export const useMouse = () => { 'data-points': points, 'data-height': height, 'data-anchors': anchors, + 'data-astNode': astNode, 'data-category': tileName, 'data-textPosition': textPosition, } = event.target.attrs; @@ -188,6 +189,7 @@ export const useMouse = () => { id: event.target.attrs.id, points: JSON.parse(points), textPosition: textPosition, + astNode: JSON.parse(astNode), anchors: JSON.parse(anchors), name: event.target.attrs.name, }; @@ -237,6 +239,7 @@ export const useMouse = () => { 'data-height': height, 'data-points': points, 'data-anchors': anchors, + 'data-astNode': astNode, 'data-textPosition': textPosition, } = event.target.attrs; const stage = stageRef.current; @@ -252,6 +255,7 @@ export const useMouse = () => { y: event.target.y(), id: event.target.attrs.id, points: Array.from(points), + astNode: JSON.parse(astNode), name: event.target.attrs.name, category: event.target.attrs.name, textPosition: JSON.parse(textPosition), diff --git a/frontend/src/json/kacheln.json b/frontend/src/json/kacheln.json index 6c45cf2..9ade2e3 100644 --- a/frontend/src/json/kacheln.json +++ b/frontend/src/json/kacheln.json @@ -159,6 +159,16 @@ "color": "#f9b43d", "width": 200, "height": 300, + "astNode": { + "javaScript": { + "type": "ReturnStatement", + "argument": null + }, + "python": { + "type": "ReturnStatement", + "argument": null + } + }, "anchors": [ { "type": "L", @@ -320,6 +330,31 @@ "color": "#F9B43D", "width": 400, "height": 200, + "astNode": { + "javaScript": { + "type": "ExpressionStatement", + "expression": { + "type": "CallExpression", + "callee": { + "type": "MemberExpression", + "object": { + "type": "Identifier", + "name": "client" + }, + "property": { + "type": "Identifier", + "name": "publish" + } + }, + "arguments": [ + { + "type": "StringLiteral", + "value": null + } + ] + } + } + }, "anchors": [ { "type": "L", diff --git a/frontend/src/state/SyntaxTreeState.tsx b/frontend/src/state/SyntaxTreeState.tsx index 2b2ce6f..648202f 100644 --- a/frontend/src/state/SyntaxTreeState.tsx +++ b/frontend/src/state/SyntaxTreeState.tsx @@ -9,9 +9,11 @@ import { findConnections } from '../utils/tileConnections'; export type ConnectedTilesContextType = { fromShapeId: string | null; ast: ASTType | null; + generatedCode: string; connectionPreview: JSX.Element | null; connections: { from: string; to: string }[]; setAst: (value: ASTType | null) => void; + setGeneratedCode: (value: string) => void; setFromShapeId: (value: string | null) => void; setConnectionPreview: (value: JSX.Element | null) => void; removeConnection: (value: string) => void; @@ -20,10 +22,12 @@ export type ConnectedTilesContextType = { export const useConnectedTilesState = create((set) => ({ ast: null, + generatedCode: '', connections: [], fromShapeId: null, connectedTiles: {}, connectionPreview: null, + setGeneratedCode: (value: string) => set(() => ({ generatedCode: value })), setAst: (value: ASTType | null) => set(() => ({ ast: value })), setFromShapeId: (value: string | null) => set(() => ({ fromShapeId: value })), removeConnection: (value: string) => diff --git a/frontend/src/utils/generateAst.ts b/frontend/src/utils/generateAst.ts index 9c978e1..f003b95 100644 --- a/frontend/src/utils/generateAst.ts +++ b/frontend/src/utils/generateAst.ts @@ -1,19 +1,6 @@ -import { CallExpression, Identifier, IfStatement, StringLiteral } from './../AstTypes.d'; +import { CallExpression, Identifier, IfStatement, ExpressionStatement } from './../AstTypes.d'; import { ASTType } from '../AstTypes'; -enum Operator { - equals = '===', - unequals = '!==', - lessThan = '<', - greaterThan = '>', - lessOrEquals = '<=', - greaterOrEquals = '>=', - and = '&&', - or = '||', - true = 'true', - false = 'false', -} - interface NodeType { id: string; anchorPosition: string; @@ -27,6 +14,8 @@ export const generateAst = ( toNode: NodeType | undefined, ast: ASTType | null, setAst: (ast: ASTType | null) => void, + generatedCode?: string, + setGeneratedCode?: (value: string) => void, ) => { if ( fromNode?.tileCategory === undefined || @@ -35,7 +24,7 @@ export const generateAst = ( toNode?.tileCategory === undefined ) return; - if (fromNode.tileCategory === 'Start' && toNode.tileCategory === 'Objekte') { + if (fromNode.tileCategory === 'Start' && toNode.tileCategory === 'Objekte' && ast === null) { const startNode = fromNode.astNode.javaScript as IfStatement; startNode.test.left = toNode.astNode.javaScript as Identifier; setAst({ @@ -75,29 +64,12 @@ export const generateAst = ( ast !== null && ast.program?.body[0].consequent.type === 'BlockStatement' ) { - ast.program.body[0].consequent.body = [ + ast.program.body[0].consequent.body = [fromNode.astNode.javaScript as ExpressionStatement]; + const callExpression = ast.program.body[0].consequent.body[0].expression as CallExpression; + callExpression.arguments = [ { - type: 'ExpressionStatement', - expression: { - type: 'CallExpression', - callee: { - type: 'MemberExpression', - object: { - type: 'Identifier', - name: 'client', - }, - property: { - type: 'Identifier', - name: 'publish', - }, - }, - arguments: [ - { - type: 'StringLiteral', - value: toNode.astNode.MQTTTopic, - } as StringLiteral, - ] as StringLiteral[], - } as CallExpression, + type: 'StringLiteral', + value: toNode.astNode.MQTTtopic, }, ]; setAst(ast); @@ -120,21 +92,24 @@ export const generateAst = ( setAst(ast); } - if (fromNode.tileCategory === 'Zustand' && toNode.tileCategory === 'End') { + if (fromNode.tileCategory === 'Zustand' && toNode.tileCategory === 'Ende' && ast !== null) { // send ast to backend const backendUrl = process.env.REACT_APP_BACKEND_URL; + console.log('fetch ast to backend: ', JSON.stringify(ast)); (async () => { try { const response = await fetch(`${backendUrl}/ast`, { method: 'POST', - body: JSON.stringify(ast, null, 2), + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(ast), }); const data = await response.json(); - console.log(data); + setGeneratedCode && setGeneratedCode(data.code); } catch (error) { alert(error); } })(); } - console.log(JSON.stringify(ast, null, 2)); }; From d9b989d86cb62678d57139f83e0991624c09a4fc Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Thu, 23 Mar 2023 22:53:22 +0100 Subject: [PATCH 29/44] added prism code visualizer --- frontend/package-lock.json | 27 +++++++++++++ frontend/package.json | 2 + frontend/src/components/Board/Board.tsx | 6 +-- .../src/components/Forms/InfoComponent.tsx | 2 +- frontend/src/pages/CanvasPage.tsx | 38 ++++++++++++++++++- 5 files changed, 70 insertions(+), 5 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f182d96..e8c83f8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,7 @@ "@types/react": "^18.0.12", "@types/react-dom": "^18.0.5", "konva": "^8.3.11", + "prismjs": "^1.29.0", "react": "^18.1.0", "react-dom": "^18.1.0", "react-draggable": "^4.4.5", @@ -35,6 +36,7 @@ "zustand": "^4.1.1" }, "devDependencies": { + "@types/prismjs": "^1.26.0", "@types/socket.io-client": "^3.0.0", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.28.0", @@ -3499,6 +3501,12 @@ "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==" }, + "node_modules/@types/prismjs": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.0.tgz", + "integrity": "sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==", + "dev": true + }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -12251,6 +12259,14 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -17970,6 +17986,12 @@ "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.2.tgz", "integrity": "sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==" }, + "@types/prismjs": { + "version": "1.26.0", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.0.tgz", + "integrity": "sha512-ZTaqn/qSqUuAq1YwvOFQfVW1AR/oQJlLSZVustdjwI+GZ8kr0MSHBj0tsXPW1EqHubx50gtBEjbPGsdZwQwCjQ==", + "dev": true + }, "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -24146,6 +24168,11 @@ } } }, + "prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==" + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 27062a3..47fbad5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,6 +14,7 @@ "@types/react": "^18.0.12", "@types/react-dom": "^18.0.5", "konva": "^8.3.11", + "prismjs": "^1.29.0", "react": "^18.1.0", "react-dom": "^18.1.0", "react-draggable": "^4.4.5", @@ -56,6 +57,7 @@ ] }, "devDependencies": { + "@types/prismjs": "^1.26.0", "@types/socket.io-client": "^3.0.0", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.28.0", diff --git a/frontend/src/components/Board/Board.tsx b/frontend/src/components/Board/Board.tsx index 0327299..81508cf 100644 --- a/frontend/src/components/Board/Board.tsx +++ b/frontend/src/components/Board/Board.tsx @@ -7,12 +7,11 @@ import { Stage as StageType } from 'konva/lib/Stage'; import { Layer as LayerType } from 'konva/lib/Layer'; import { SocketDragTile, RoomData } from '../../types'; import { useBoardState } from '../../state/BoardState'; +import { useContextMenu } from '../../hooks/useContextMenu'; import { useWebSocketState } from '../../state/WebSocketState'; import useWindowDimensions from '../../hooks/useWindowDimensions'; -import { useConnectedTilesState } from '../../state/SyntaxTreeState'; -import { KonvaEventObject } from 'konva/lib/Node'; -import { useContextMenu } from '../../hooks/useContextMenu'; import { useContextMenuState } from '../../state/ContextMenuState'; +import { useConnectedTilesState } from '../../state/SyntaxTreeState'; // Main Stage Component that holds the Canvas. Scales based on the window size. @@ -40,6 +39,7 @@ const Board = () => { const setStageReference = useBoardState((state) => state.setStageReference); const setRemoteDragColor = useBoardState((state) => state.setRemoteDragColor); const connectionPreview = useConnectedTilesState((state) => state.connectionPreview); + const setLineContextMenuOpen = useContextMenuState((state) => state.setLineContextMenuOpen); setStageReference(stageRef); diff --git a/frontend/src/components/Forms/InfoComponent.tsx b/frontend/src/components/Forms/InfoComponent.tsx index d066e5c..a3377b4 100644 --- a/frontend/src/components/Forms/InfoComponent.tsx +++ b/frontend/src/components/Forms/InfoComponent.tsx @@ -20,7 +20,7 @@ const InfoComponent = () => { }; // Component that displays the connected users and Disconnect Button return ( -
    +
    {users?.map((user) => ( diff --git a/frontend/src/pages/CanvasPage.tsx b/frontend/src/pages/CanvasPage.tsx index 646c122..0ed15f3 100644 --- a/frontend/src/pages/CanvasPage.tsx +++ b/frontend/src/pages/CanvasPage.tsx @@ -1,26 +1,62 @@ import React from 'react'; import Board from '../components/Board/Board'; +import Editor from 'react-simple-code-editor'; import Sidebar from '../components/Sidebar/Sidebar'; import { useWindowFocus } from '../hooks/useWindowFocus'; +import { highlight, languages } from 'prismjs'; import InfoComponent from '../components/Forms/InfoComponent'; import SelectLampForm from '../components/Forms/SelectLampForm'; import { useContextMenuState } from '../state/ContextMenuState'; import TileRightClickMenu from '../components/ContextMenus/TileRightClickMenu'; import LineRightClickMenu from '../components/ContextMenus/LineRightClickMenu'; +import { useConnectedTilesState } from '../state/SyntaxTreeState'; +import 'prismjs/components/prism-clike'; +import 'prismjs/components/prism-javascript'; +import 'prismjs/components/prism-json'; +import 'prismjs/themes/prism.css'; +import { ASTType } from '../AstTypes'; const CanvasPage = () => { // Add Cursor here. const socket = useWebSocketState((state) => state.socket); const { contextMenuOpen, lineContextMenuOpen } = useContextMenuState((state) => state); + const { generatedCode, setGeneratedCode, ast, setAst } = useConnectedTilesState((state) => state); useWindowFocus(); return ( <> {contextMenuOpen === true && } {lineContextMenuOpen === true && } - + +
    + + setAst(code as unknown as ASTType)} + highlight={(code) => highlight(code, languages.json, 'json')} + padding={20} + style={{ + fontFamily: '"Fira code", "Fira Mono", monospace', + fontSize: 12, + }} + /> + {generatedCode.length > 0 && ( + setGeneratedCode(code)} + highlight={(code) => highlight(code, languages.js, 'js')} + padding={20} + style={{ + fontFamily: '"Fira code", "Fira Mono", monospace', + fontSize: 16, + }} + /> + )} +
    ); }; From 70cf3700d1c27a649b5891423b94ef581c0b7819 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Thu, 23 Mar 2023 23:11:25 +0100 Subject: [PATCH 30/44] added generation sidebar --- frontend/src/pages/CanvasPage.tsx | 57 +++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/frontend/src/pages/CanvasPage.tsx b/frontend/src/pages/CanvasPage.tsx index 0ed15f3..33bdc38 100644 --- a/frontend/src/pages/CanvasPage.tsx +++ b/frontend/src/pages/CanvasPage.tsx @@ -30,31 +30,52 @@ const CanvasPage = () => { -
    +
    - setAst(code as unknown as ASTType)} - highlight={(code) => highlight(code, languages.json, 'json')} - padding={20} - style={{ - fontFamily: '"Fira code", "Fira Mono", monospace', - fontSize: 12, - }} - /> - {generatedCode.length > 0 && ( +
    + AST setGeneratedCode(code)} - highlight={(code) => highlight(code, languages.js, 'js')} + className='w-full border border-gray-500 h-full bg-gray-100 text-gray-500 p-4' + value={JSON.stringify(ast)} + onValueChange={(code) => setAst(code as unknown as ASTType)} + highlight={(code) => highlight(code, languages.json, 'json')} padding={20} style={{ fontFamily: '"Fira code", "Fira Mono", monospace', - fontSize: 16, + fontSize: 12, }} /> +
    + + {generatedCode.length > 0 && ( +
    + JavaScript + setGeneratedCode(code)} + highlight={(code) => highlight(code, languages.js, 'js')} + padding={20} + style={{ + fontFamily: '"Fira code", "Fira Mono", monospace', + fontSize: 16, + }} + /> +
    + )} + {generatedCode.length > 0 && ( +
    + Python + setGeneratedCode(code)} + highlight={(code) => highlight(code, languages.js, 'js')} + padding={20} + style={{ + fontFamily: '"Fira code", "Fira Mono", monospace', + fontSize: 16, + }} + /> +
    )}
    From 52e02d3f16c3006f74ce7664a5d5f48f1f4de4f0 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Thu, 23 Mar 2023 23:23:06 +0100 Subject: [PATCH 31/44] added styling changes --- frontend/src/components/Board/Board.tsx | 3 ++- frontend/src/components/Forms/InfoComponent.tsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Board/Board.tsx b/frontend/src/components/Board/Board.tsx index 81508cf..285df66 100644 --- a/frontend/src/components/Board/Board.tsx +++ b/frontend/src/components/Board/Board.tsx @@ -99,10 +99,11 @@ const Board = () => { handleWheel(e)} > diff --git a/frontend/src/components/Forms/InfoComponent.tsx b/frontend/src/components/Forms/InfoComponent.tsx index a3377b4..4e9702c 100644 --- a/frontend/src/components/Forms/InfoComponent.tsx +++ b/frontend/src/components/Forms/InfoComponent.tsx @@ -20,14 +20,14 @@ const InfoComponent = () => { }; // Component that displays the connected users and Disconnect Button return ( -
    +
    {users?.map((user) => ( ))}
    {roomId && } - +
    ); From c9fd8f68e285bf525bde410a06f57f336ad94fc8 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Fri, 24 Mar 2023 03:23:13 +0100 Subject: [PATCH 32/44] added py implementation --- backend/generatePythonCode.py | 47 +++++++++++++++++++++++++ backend/src/Controller/astController.ts | 37 ++++++++++++++++++- backend/src/Routes/ApiRoutes.ts | 6 ++-- frontend/src/pages/CanvasPage.tsx | 12 +++---- frontend/src/state/SyntaxTreeState.tsx | 11 +++--- frontend/src/utils/generateAst.ts | 6 ++-- 6 files changed, 103 insertions(+), 16 deletions(-) create mode 100644 backend/generatePythonCode.py diff --git a/backend/generatePythonCode.py b/backend/generatePythonCode.py new file mode 100644 index 0000000..c1c0be2 --- /dev/null +++ b/backend/generatePythonCode.py @@ -0,0 +1,47 @@ +import ast +import json + +def generate_code(ast_json): + node_type = ast_json.get('type') + + if node_type == 'Module': + body = ast_json.get('body') + return ast.Module(body=[generate_code(node) for node in body]) + elif node_type == 'If': + test = generate_code(ast_json.get('test')) + body = ast_json.get('body') + orelse = ast_json.get('orelse') + return ast.If(test=test, body=[generate_code(node) for node in body], orelse=[generate_code(node) for node in orelse]) + elif node_type == 'Compare': + left = generate_code(ast_json.get('left')) + ops = [generate_code(op) for op in ast_json.get('ops')] + comparators = [generate_code(comparator) for comparator in ast_json.get('comparators')] + return ast.Compare(left=left, ops=ops, comparators=comparators) + elif node_type == 'Name': + return ast.Name(id=ast_json.get('id'), ctx=generate_code(ast_json.get('ctx'))) + elif node_type == 'Load': + return ast.Load() + elif node_type == 'Eq': + return ast.Eq() + elif node_type == 'Str': + return ast.Str(s=ast_json.get('s')) + elif node_type == 'Expr': + value = generate_code(ast_json.get('value')) + return ast.Expr(value=value) + elif node_type == 'Call': + func = generate_code(ast_json.get('func')) + args = [generate_code(arg) for arg in ast_json.get('args')] + keywords = [generate_code(keyword) for keyword in ast_json.get('keywords')] + return ast.Call(func=func, args=args, keywords=keywords) + elif node_type == 'Attribute': + value = generate_code(ast_json.get('value')) + attr = ast_json.get('attr') + ctx = generate_code(ast_json.get('ctx')) + return ast.Attribute(value=value, attr=attr, ctx=ctx) + else: + raise ValueError(f'Unknown AST node type: {node_type}') + +#print(ast.dump(generate_code(sys.argv[1]))) + +#ast_node = generate_code(json.loads(sys.argv[1])) +sys.stdout.write(sys.argv[1]) diff --git a/backend/src/Controller/astController.ts b/backend/src/Controller/astController.ts index eb6dc77..f95625b 100644 --- a/backend/src/Controller/astController.ts +++ b/backend/src/Controller/astController.ts @@ -1,6 +1,8 @@ import generate from "@babel/generator"; import { Request, Response } from "express"; -export const generateCode = async (req: Request, res: Response) => { +import { spawn } from "child_process"; + +export const generateJsCode = async (req: Request, res: Response) => { /** * generate code from the ast * if no error, send response with the generated code @@ -14,3 +16,36 @@ export const generateCode = async (req: Request, res: Response) => { res.status(500).json({ message: error.message }); } }; + +export const generatePyCode = async (req: Request, res: Response) => { + /** + * generate code from the ast + * if no error, send response with the generated code + * else send error + */ + + try { + let dataToSend = ""; + // spawn new child process to call the python script + const python = spawn("python3", ["generatePythonCode.py", req.body]); + // collect data from script + python.stdin.write(req.body); + python.stdin.end(); + + python.stdout.on("data", function (data) { + console.log("Pipe data from python script ..."); + dataToSend = data.toString(); + }); + // in close event we are sure that stream from child process is closed + python.on("close", (code) => { + console.log(`child process close all stdio with code ${code}`); + // send data to browser + res.send(dataToSend); + }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + + + diff --git a/backend/src/Routes/ApiRoutes.ts b/backend/src/Routes/ApiRoutes.ts index 1424d8e..1dbc8a0 100644 --- a/backend/src/Routes/ApiRoutes.ts +++ b/backend/src/Routes/ApiRoutes.ts @@ -1,6 +1,6 @@ import { Router } from "express"; import { uploadFiles } from "../middleware/upload"; -import { generateCode } from "../Controller/astController"; +import { generateJsCode, generatePyCode } from "../Controller/astController"; import bodyParser from "body-parser"; import { findTile, @@ -17,11 +17,13 @@ import { const router = Router(); const jsonParser = bodyParser.json(); +const textParser = bodyParser.text(); router.get("/", getAllTiles); router.get("/:id", findTile); router.post("/", uploadFiles, createTile); -router.post("/ast", jsonParser, generateCode); +router.post("/ast/js", jsonParser, generateJsCode); +router.post("/ast/py", textParser, generatePyCode); router.put("/:id", uploadFiles, updateTile); router.delete("/:id", deleteTile); diff --git a/frontend/src/pages/CanvasPage.tsx b/frontend/src/pages/CanvasPage.tsx index 33bdc38..e6e8a48 100644 --- a/frontend/src/pages/CanvasPage.tsx +++ b/frontend/src/pages/CanvasPage.tsx @@ -47,12 +47,12 @@ const CanvasPage = () => { />
    - {generatedCode.length > 0 && ( + {generatedCode.js.length > 0 && (
    JavaScript setGeneratedCode(code)} + value={generatedCode.js} + onValueChange={(code) => setGeneratedCode({ ...generatedCode, js: code })} highlight={(code) => highlight(code, languages.js, 'js')} padding={20} style={{ @@ -62,12 +62,12 @@ const CanvasPage = () => { />
    )} - {generatedCode.length > 0 && ( + {generatedCode.py.length > 0 && (
    Python setGeneratedCode(code)} + value={generatedCode.py} + onValueChange={(code) => setGeneratedCode({ ...generatedCode, py: code })} highlight={(code) => highlight(code, languages.js, 'js')} padding={20} style={{ diff --git a/frontend/src/state/SyntaxTreeState.tsx b/frontend/src/state/SyntaxTreeState.tsx index 648202f..025c696 100644 --- a/frontend/src/state/SyntaxTreeState.tsx +++ b/frontend/src/state/SyntaxTreeState.tsx @@ -9,11 +9,14 @@ import { findConnections } from '../utils/tileConnections'; export type ConnectedTilesContextType = { fromShapeId: string | null; ast: ASTType | null; - generatedCode: string; + generatedCode: { + js: string; + py: string; + }; connectionPreview: JSX.Element | null; connections: { from: string; to: string }[]; setAst: (value: ASTType | null) => void; - setGeneratedCode: (value: string) => void; + setGeneratedCode: (value: { js: string; py: string }) => void; setFromShapeId: (value: string | null) => void; setConnectionPreview: (value: JSX.Element | null) => void; removeConnection: (value: string) => void; @@ -22,12 +25,12 @@ export type ConnectedTilesContextType = { export const useConnectedTilesState = create((set) => ({ ast: null, - generatedCode: '', + generatedCode: { js: '', py: '' }, connections: [], fromShapeId: null, connectedTiles: {}, connectionPreview: null, - setGeneratedCode: (value: string) => set(() => ({ generatedCode: value })), + setGeneratedCode: (value: { js: string; py: string }) => set(() => ({ generatedCode: value })), setAst: (value: ASTType | null) => set(() => ({ ast: value })), setFromShapeId: (value: string | null) => set(() => ({ fromShapeId: value })), removeConnection: (value: string) => diff --git a/frontend/src/utils/generateAst.ts b/frontend/src/utils/generateAst.ts index f003b95..8ac6638 100644 --- a/frontend/src/utils/generateAst.ts +++ b/frontend/src/utils/generateAst.ts @@ -14,8 +14,8 @@ export const generateAst = ( toNode: NodeType | undefined, ast: ASTType | null, setAst: (ast: ASTType | null) => void, - generatedCode?: string, - setGeneratedCode?: (value: string) => void, + generatedCode?: { js: string; py: string }, + setGeneratedCode?: (value: { js: string; py: string }) => void, ) => { if ( fromNode?.tileCategory === undefined || @@ -106,7 +106,7 @@ export const generateAst = ( body: JSON.stringify(ast), }); const data = await response.json(); - setGeneratedCode && setGeneratedCode(data.code); + setGeneratedCode && setGeneratedCode(data); } catch (error) { alert(error); } From 4c7311526399ec83af6f08a4aa4d7b5b0dca94cb Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Sat, 25 Mar 2023 21:54:01 +0100 Subject: [PATCH 33/44] generate python code from js ast and display in frontend --- backend/generatePythonCode.py | 47 ----------------- backend/src/Controller/astController.ts | 26 ++------- backend/src/Routes/ApiRoutes.ts | 8 +-- backend/types/ast.types.d.ts | 70 +++++++++++++++++++++++++ backend/utils/generatePyCode.ts | 67 +++++++++++++++++++++++ 5 files changed, 147 insertions(+), 71 deletions(-) delete mode 100644 backend/generatePythonCode.py create mode 100644 backend/types/ast.types.d.ts create mode 100644 backend/utils/generatePyCode.ts diff --git a/backend/generatePythonCode.py b/backend/generatePythonCode.py deleted file mode 100644 index c1c0be2..0000000 --- a/backend/generatePythonCode.py +++ /dev/null @@ -1,47 +0,0 @@ -import ast -import json - -def generate_code(ast_json): - node_type = ast_json.get('type') - - if node_type == 'Module': - body = ast_json.get('body') - return ast.Module(body=[generate_code(node) for node in body]) - elif node_type == 'If': - test = generate_code(ast_json.get('test')) - body = ast_json.get('body') - orelse = ast_json.get('orelse') - return ast.If(test=test, body=[generate_code(node) for node in body], orelse=[generate_code(node) for node in orelse]) - elif node_type == 'Compare': - left = generate_code(ast_json.get('left')) - ops = [generate_code(op) for op in ast_json.get('ops')] - comparators = [generate_code(comparator) for comparator in ast_json.get('comparators')] - return ast.Compare(left=left, ops=ops, comparators=comparators) - elif node_type == 'Name': - return ast.Name(id=ast_json.get('id'), ctx=generate_code(ast_json.get('ctx'))) - elif node_type == 'Load': - return ast.Load() - elif node_type == 'Eq': - return ast.Eq() - elif node_type == 'Str': - return ast.Str(s=ast_json.get('s')) - elif node_type == 'Expr': - value = generate_code(ast_json.get('value')) - return ast.Expr(value=value) - elif node_type == 'Call': - func = generate_code(ast_json.get('func')) - args = [generate_code(arg) for arg in ast_json.get('args')] - keywords = [generate_code(keyword) for keyword in ast_json.get('keywords')] - return ast.Call(func=func, args=args, keywords=keywords) - elif node_type == 'Attribute': - value = generate_code(ast_json.get('value')) - attr = ast_json.get('attr') - ctx = generate_code(ast_json.get('ctx')) - return ast.Attribute(value=value, attr=attr, ctx=ctx) - else: - raise ValueError(f'Unknown AST node type: {node_type}') - -#print(ast.dump(generate_code(sys.argv[1]))) - -#ast_node = generate_code(json.loads(sys.argv[1])) -sys.stdout.write(sys.argv[1]) diff --git a/backend/src/Controller/astController.ts b/backend/src/Controller/astController.ts index f95625b..5e92a63 100644 --- a/backend/src/Controller/astController.ts +++ b/backend/src/Controller/astController.ts @@ -1,6 +1,6 @@ import generate from "@babel/generator"; +import generatePyCode from "../../utils/generatePyCode"; import { Request, Response } from "express"; -import { spawn } from "child_process"; export const generateJsCode = async (req: Request, res: Response) => { /** @@ -9,7 +9,6 @@ export const generateJsCode = async (req: Request, res: Response) => { * else send error */ try { - console.log(req.body); const generated = await generate(req.body); res.status(200).json({ code: generated.code }); } catch (error) { @@ -17,31 +16,16 @@ export const generateJsCode = async (req: Request, res: Response) => { } }; -export const generatePyCode = async (req: Request, res: Response) => { +export const generatePythonCode = async (req: Request, res: Response) => { /** * generate code from the ast * if no error, send response with the generated code * else send error */ - + const generated = generatePyCode(req.body); try { - let dataToSend = ""; - // spawn new child process to call the python script - const python = spawn("python3", ["generatePythonCode.py", req.body]); - // collect data from script - python.stdin.write(req.body); - python.stdin.end(); - - python.stdout.on("data", function (data) { - console.log("Pipe data from python script ..."); - dataToSend = data.toString(); - }); - // in close event we are sure that stream from child process is closed - python.on("close", (code) => { - console.log(`child process close all stdio with code ${code}`); - // send data to browser - res.send(dataToSend); - }); + console.log(req.body); + res.status(500).json({ code: generated }); } catch (error) { res.status(500).json({ message: error.message }); } diff --git a/backend/src/Routes/ApiRoutes.ts b/backend/src/Routes/ApiRoutes.ts index 1dbc8a0..c426fa2 100644 --- a/backend/src/Routes/ApiRoutes.ts +++ b/backend/src/Routes/ApiRoutes.ts @@ -1,6 +1,9 @@ import { Router } from "express"; import { uploadFiles } from "../middleware/upload"; -import { generateJsCode, generatePyCode } from "../Controller/astController"; +import { + generateJsCode, + generatePythonCode, +} from "../Controller/astController"; import bodyParser from "body-parser"; import { findTile, @@ -17,13 +20,12 @@ import { const router = Router(); const jsonParser = bodyParser.json(); -const textParser = bodyParser.text(); router.get("/", getAllTiles); router.get("/:id", findTile); router.post("/", uploadFiles, createTile); router.post("/ast/js", jsonParser, generateJsCode); -router.post("/ast/py", textParser, generatePyCode); +router.post("/ast/py", jsonParser, generatePythonCode); router.put("/:id", uploadFiles, updateTile); router.delete("/:id", deleteTile); diff --git a/backend/types/ast.types.d.ts b/backend/types/ast.types.d.ts new file mode 100644 index 0000000..1ea02ff --- /dev/null +++ b/backend/types/ast.types.d.ts @@ -0,0 +1,70 @@ +export type ASTType = { + type: string; + errors: any[]; + program: Program; +}; + +export type Program = { + type: string; + body: Body[]; +}; + +export type Body = { + type: string; + test: Test; + consequent: Consequent; +}; + +export type Test = { + type: string; + left: Left; + right: Right; + operator: string; +}; + +export type Left = { + type: string; + name: string; +}; + +export type Right = { + type: string; + value: string; +}; + +export type Consequent = { + type: string; + body: ConsequentBody[]; +}; + +export type ConsequentBody = { + type: string; + expression: Expression; +}; + +export type Expression = { + type: string; + callee: Callee; + arguments: Argument[]; +}; + +export type Callee = { + type: string; + object: Object; + property: Property; +}; + +export type Object = { + type: string; + name: string; +}; + +export type Property = { + type: string; + name: string; +}; + +export type Argument = { + type: string; + value: string; +}; diff --git a/backend/utils/generatePyCode.ts b/backend/utils/generatePyCode.ts new file mode 100644 index 0000000..cea1824 --- /dev/null +++ b/backend/utils/generatePyCode.ts @@ -0,0 +1,67 @@ +import { ASTType } from "./../types/ast.types.d"; + +const getPythonLogicalOperator = (operatorString: string) => { + //get python equivalent of logical Operator + switch (operatorString) { + case "||": + return "or"; + case "&&": + return "and"; + case "!==": + return "not"; + default: + return " "; + } +}; + +const getPythonIdentityOperator = (operatorString: string) => { + //get python equivalent of Identity Operator + switch (operatorString) { + case "===": + return "is"; + case "!==": + return "is not"; + default: + return " "; + } +}; + +const generatePyCode = (ast: ASTType) => { + const body = ast.program.body; + let pyCode = ""; + + body.forEach((body) => { + const left = body.test.left; + const right = body.test.right; + const operator = getPythonIdentityOperator(body.test.operator); + const consequent = body.consequent; + if (body.type === "IfStatement" && body.test.type === "BinaryExpression") { + pyCode += `if ${left.name} ${operator} ${right.value}:`; + } + + if (body.type === "IfStatement" && body.test.type === "LogicalExpression") { + const logicalOperator = getPythonLogicalOperator(body.test.operator); + pyCode += `if ${left.name} ${operator} ${right.value} ${logicalOperator}:`; + } + + if (consequent.type === "BlockStatement") { + consequent.body.forEach((expression) => { + if (expression.type === "ExpressionStatement") { + const callee = expression.expression.callee; + const args = expression.expression.arguments; + const argList = args.map((arg) => `"${arg.value}"`).join(", "); + if ( + expression.type === "ExpressionStatement" && + expression.expression.type === "CallExpression" + ) { + pyCode += `\n\t${callee.object.name}.${callee.property.name}(${argList})`; + } + } + }); + } + }); + + return pyCode; +}; + +export default generatePyCode; From 2e7f4bb12eacbbf925453185c16a0d0310f306bd Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Sat, 25 Mar 2023 21:55:09 +0100 Subject: [PATCH 34/44] parallel fetch data --- frontend/src/pages/CanvasPage.tsx | 2 +- frontend/src/utils/generateAst.ts | 31 +++++++++++++++++++++---------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/frontend/src/pages/CanvasPage.tsx b/frontend/src/pages/CanvasPage.tsx index e6e8a48..880f171 100644 --- a/frontend/src/pages/CanvasPage.tsx +++ b/frontend/src/pages/CanvasPage.tsx @@ -37,7 +37,7 @@ const CanvasPage = () => { setAst(code as unknown as ASTType)} + onValueChange={(code) => console.log(code)} highlight={(code) => highlight(code, languages.json, 'json')} padding={20} style={{ diff --git a/frontend/src/utils/generateAst.ts b/frontend/src/utils/generateAst.ts index 8ac6638..afb7be1 100644 --- a/frontend/src/utils/generateAst.ts +++ b/frontend/src/utils/generateAst.ts @@ -96,20 +96,31 @@ export const generateAst = ( // send ast to backend const backendUrl = process.env.REACT_APP_BACKEND_URL; console.log('fetch ast to backend: ', JSON.stringify(ast)); - (async () => { - try { - const response = await fetch(`${backendUrl}/ast`, { + const data = Promise.all([ + fetch(`${backendUrl}/ast/js`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(ast), - }); - const data = await response.json(); - setGeneratedCode && setGeneratedCode(data); - } catch (error) { - alert(error); - } - })(); + }).then((value) => value.json()), + fetch(`${backendUrl}/ast/py`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(ast), + }).then((value) => value.json()), + ]); + + (async () => { + try { + const resolvedData = await data; + setGeneratedCode && + setGeneratedCode({ js: resolvedData[0].code, py: resolvedData[1].code }); + } catch (error) { + alert(error); + } + })(); } }; From cfb5c52b9e7d05f9457c397aa98cf94fcdc26bba Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Mon, 27 Mar 2023 09:33:19 +0200 Subject: [PATCH 35/44] use square anchorpoints --- frontend/src/components/Tiles/Tile.tsx | 2 +- frontend/src/components/Tiles/TileBorderAnchor.tsx | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/Tiles/Tile.tsx b/frontend/src/components/Tiles/Tile.tsx index 0fdbdfb..78182bd 100644 --- a/frontend/src/components/Tiles/Tile.tsx +++ b/frontend/src/components/Tiles/Tile.tsx @@ -46,7 +46,7 @@ const Tile: React.FC = ({ y={y + point.y} type={point.type} onClick={handleClick} - fill={fromShapeId === `${id}_${point.type}` ? 'green' : 'black'} + fill={fromShapeId === `${id}_${point.type}` ? 'green' : color} /> ))} diff --git a/frontend/src/components/Tiles/TileBorderAnchor.tsx b/frontend/src/components/Tiles/TileBorderAnchor.tsx index 8d062a5..29c32b1 100644 --- a/frontend/src/components/Tiles/TileBorderAnchor.tsx +++ b/frontend/src/components/Tiles/TileBorderAnchor.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Circle } from 'react-konva'; +import { Rect } from 'react-konva'; import { Circle as CircleObject } from 'konva/lib/shapes/Circle'; import { KonvaEventObject } from 'konva/lib/Node'; @@ -26,13 +26,14 @@ const TileBorderAnchors: React.FC = ({ x, y, id, onClick, fill, type }) = const anchor = React.useRef(null); return ( <> - Date: Mon, 27 Mar 2023 09:33:45 +0200 Subject: [PATCH 36/44] change position of anchor --- frontend/src/json/kacheln.json | 136 +++++++++++---------------------- 1 file changed, 46 insertions(+), 90 deletions(-) diff --git a/frontend/src/json/kacheln.json b/frontend/src/json/kacheln.json index 9ade2e3..7b7301f 100644 --- a/frontend/src/json/kacheln.json +++ b/frontend/src/json/kacheln.json @@ -10,67 +10,27 @@ "height": 300, "astNode": { "javaScript": { - "type": "IfStatement", - "test": { + "type": "LogicalExpression", + "left": { "type": "BinaryExpression", - "left": { - "type": "MemberExpression", - "object": null, - "property": { - "type": "Identifier", - "name": "state" - } - }, + "left": null, "right": null, "operator": "===" }, - "consequent": { - "type": "BlockStatement", - "body": null - } - }, - "python": { - "type": "IfStatement", - "test": null, - "consequent": { - "type": "BlockStatement", - "body": [ - { - "type": "ExpressionStatement", - "expression": { - "type": "CallExpression", - "callee": { - "type": "MemberExpression", - "object": { - "type": "Identifier", - "name": "client" - }, - "property": { - "type": "Identifier", - "name": "publish" - } - }, - "arguments": [ - { - "type": "StringLiteral", - "value": "" - }, - { - "type": "StringLiteral", - "value": "" - } - ] - } - } - ] - } + "right": { + "type": "BinaryExpression", + "left": null, + "right": null, + "operator": "===" + }, + "operator": "&&" } }, "anchors": [ { "type": "L", - "x": 150, - "y": 150 + "x": 100, + "y": 145 } ], "textPosition": { @@ -99,13 +59,13 @@ "anchors": [ { "type": "L", - "x": -70, - "y": 50 + "x": -115, + "y": 140 }, { "type": "R", - "x": 270, - "y": 50 + "x": 290, + "y": 140 } ], "textPosition": { @@ -134,13 +94,13 @@ "anchors": [ { "type": "L", - "x": -70, - "y": 50 + "x": -115, + "y": 140 }, { "type": "R", - "x": 270, - "y": 50 + "x": 290, + "y": 140 } ], "textPosition": { @@ -163,16 +123,12 @@ "javaScript": { "type": "ReturnStatement", "argument": null - }, - "python": { - "type": "ReturnStatement", - "argument": null } }, "anchors": [ { "type": "L", - "x": 50, + "x": 100, "y": 150 } ], @@ -202,13 +158,13 @@ "anchors": [ { "type": "L", - "x": -70, - "y": 50 + "x": -115, + "y": 140 }, { "type": "R", - "x": 270, - "y": 50 + "x": 290, + "y": 140 } ], "textPosition": { @@ -237,13 +193,13 @@ "anchors": [ { "type": "L", - "x": -70, - "y": 50 + "x": -115, + "y": 140 }, { "type": "R", - "x": 270, - "y": 50 + "x": 290, + "y": 140 } ], "textPosition": { @@ -264,20 +220,20 @@ "height": 300, "astNode": { "javaScript": { - "type": "StringLiteral", - "value": "open" + "type": "NumericLiteral", + "value": 1 } }, "anchors": [ { "type": "L", - "x": -10, - "y": 150 + "x": 73, + "y": 140 }, { "type": "R", - "x": 350, - "y": 150 + "x": 290, + "y": 140 } ], "textPosition": { @@ -298,20 +254,20 @@ "height": 300, "astNode": { "javaScript": { - "type": "StringLiteral", - "value": "on" + "type": "NumericLiteral", + "value": 1 } }, "anchors": [ { "type": "L", - "x": -10, - "y": 150 + "x": 73, + "y": 140 }, { "type": "R", - "x": 350, - "y": 150 + "x": 290, + "y": 140 } ], "textPosition": { @@ -348,7 +304,7 @@ }, "arguments": [ { - "type": "StringLiteral", + "type": "NumericLiteral", "value": null } ] @@ -358,13 +314,13 @@ "anchors": [ { "type": "L", - "x": -100, - "y": 50 + "x": -27, + "y": 0 }, { "type": "R", - "x": 300, - "y": 50 + "x": 200, + "y": 0 } ], "textPosition": { From 8561d5e935b545b9135a72f7a5d8ab6090efeeff Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Sat, 1 Apr 2023 17:31:04 +0200 Subject: [PATCH 37/44] renamed json file --- frontend/src/json/{kacheln.json => blocks.json} | 0 frontend/src/json/{kaceln.old.json => blocks.old.json} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename frontend/src/json/{kacheln.json => blocks.json} (100%) rename frontend/src/json/{kaceln.old.json => blocks.old.json} (100%) diff --git a/frontend/src/json/kacheln.json b/frontend/src/json/blocks.json similarity index 100% rename from frontend/src/json/kacheln.json rename to frontend/src/json/blocks.json diff --git a/frontend/src/json/kaceln.old.json b/frontend/src/json/blocks.old.json similarity index 100% rename from frontend/src/json/kaceln.old.json rename to frontend/src/json/blocks.old.json From 3545c96907406516caf1cff6c5c1aa5040f4ca12 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Sat, 8 Apr 2023 00:51:28 +0200 Subject: [PATCH 38/44] added formatting changes and new images --- backend/.gitignore | 1 - backend/uploads/1674420527774.png | Bin 0 -> 2987 bytes backend/uploads/1674422039501.png | Bin 0 -> 1837 bytes backend/uploads/1674423645286.png | Bin 0 -> 3384 bytes backend/uploads/1674424309409.png | Bin 0 -> 2183 bytes backend/uploads/1674425020844.png | Bin 0 -> 3179 bytes backend/uploads/1674425372221.png | Bin 0 -> 4590 bytes backend/uploads/1674425372223.png | Bin 0 -> 3562 bytes backend/uploads/1674425372224.png | Bin 0 -> 4057 bytes backend/uploads/1674425372225.png | Bin 0 -> 5853 bytes backend/uploads/1674425372226.png | Bin 0 -> 8089 bytes backend/uploads/1674425372227.png | Bin 0 -> 6100 bytes backend/uploads/1674425372228.png | Bin 0 -> 5288 bytes backend/uploads/1674425372229.png | Bin 0 -> 6191 bytes backend/uploads/1674425372230.png | Bin 0 -> 5751 bytes backend/uploads/1674425372231.png | Bin 0 -> 6704 bytes backend/uploads/1674425372232.png | Bin 0 -> 4801 bytes backend/uploads/Readme.md | 3 ++ frontend/src/components/Sidebar/Sidebar.tsx | 2 +- frontend/src/json/blocks.json | 16 +++--- frontend/src/pages/CanvasPage.tsx | 2 +- frontend/src/state/SyntaxTreeState.tsx | 2 +- frontend/src/utils/generateAst.ts | 53 ++++++++++---------- 23 files changed, 40 insertions(+), 39 deletions(-) create mode 100644 backend/uploads/1674420527774.png create mode 100644 backend/uploads/1674422039501.png create mode 100644 backend/uploads/1674423645286.png create mode 100644 backend/uploads/1674424309409.png create mode 100644 backend/uploads/1674425020844.png create mode 100644 backend/uploads/1674425372221.png create mode 100644 backend/uploads/1674425372223.png create mode 100644 backend/uploads/1674425372224.png create mode 100644 backend/uploads/1674425372225.png create mode 100644 backend/uploads/1674425372226.png create mode 100644 backend/uploads/1674425372227.png create mode 100644 backend/uploads/1674425372228.png create mode 100644 backend/uploads/1674425372229.png create mode 100644 backend/uploads/1674425372230.png create mode 100644 backend/uploads/1674425372231.png create mode 100644 backend/uploads/1674425372232.png create mode 100644 backend/uploads/Readme.md diff --git a/backend/.gitignore b/backend/.gitignore index 16472a6..c9003c6 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -12,7 +12,6 @@ /build #ignore uploads folder -/uploads # misc .env .DS_Store diff --git a/backend/uploads/1674420527774.png b/backend/uploads/1674420527774.png new file mode 100644 index 0000000000000000000000000000000000000000..92856a537ef2457f24587e06e217cadf4cab5902 GIT binary patch literal 2987 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%0DVEb=rX} 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 literal 0 HcmV?d00001 diff --git a/backend/uploads/1674424309409.png b/backend/uploads/1674424309409.png new file mode 100644 index 0000000000000000000000000000000000000000..6a7c7f0911d7a17cf64b9d2d3027899a205479e6 GIT binary patch literal 2183 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 literal 0 HcmV?d00001 diff --git a/backend/uploads/1674425372221.png b/backend/uploads/1674425372221.png new file mode 100644 index 0000000000000000000000000000000000000000..8885a13264900d9611fd26a3c393daecb9db3451 GIT binary patch literal 4590 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 literal 0 HcmV?d00001 diff --git a/backend/uploads/1674425372223.png b/backend/uploads/1674425372223.png new file mode 100644 index 0000000000000000000000000000000000000000..7f4d5e3f4fddf3aa5c773903e65c8345b7fd1ebf GIT binary patch literal 3562 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 literal 0 HcmV?d00001 diff --git a/backend/uploads/1674425372226.png b/backend/uploads/1674425372226.png new file mode 100644 index 0000000000000000000000000000000000000000..5bd2e75c13b2fa323a31da0557a0e40d56e3ad21 GIT binary patch literal 8089 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 H#hd>F6?y1O literal 0 HcmV?d00001 diff --git a/backend/uploads/1674425372228.png b/backend/uploads/1674425372228.png new file mode 100644 index 0000000000000000000000000000000000000000..d6f86f2cd05b898a9af6c79d4a25a7f0d9b2712e GIT binary patch literal 5288 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 C1fItL literal 0 HcmV?d00001 diff --git a/backend/uploads/1674425372229.png b/backend/uploads/1674425372229.png new file mode 100644 index 0000000000000000000000000000000000000000..25c8c305ca2e8d6c0d7206f751082485d0bd438d GIT binary patch literal 6191 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 literal 0 HcmV?d00001 diff --git a/backend/uploads/1674425372230.png b/backend/uploads/1674425372230.png new file mode 100644 index 0000000000000000000000000000000000000000..0ae41e42a5106357dff8e9330fe9be4e79667189 GIT binary patch literal 5751 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 JaxGZw{{UOKTV?0xLQ5Ey#soKfUM<&O9M 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 E4~Cui@c;k- literal 0 HcmV?d00001 diff --git a/backend/uploads/Readme.md b/backend/uploads/Readme.md new file mode 100644 index 0000000..62652df --- /dev/null +++ b/backend/uploads/Readme.md @@ -0,0 +1,3 @@ +# Upload Ordner + +Dieser Ordner enthält alle hochgeladenen Images. Für Testzwecke wurde der Ordner commited. Sollte aber in einer echten Nutzung nicht commited werden. \ No newline at end of file diff --git a/frontend/src/components/Sidebar/Sidebar.tsx b/frontend/src/components/Sidebar/Sidebar.tsx index 7da2978..2a8f6ac 100644 --- a/frontend/src/components/Sidebar/Sidebar.tsx +++ b/frontend/src/components/Sidebar/Sidebar.tsx @@ -1,6 +1,6 @@ import Tab from './Tab'; import Category from './Category'; -import kacheln from '../../json/kacheln.json'; +import kacheln from '../../json/blocks.json'; import { useToast } from '../../hooks/useToast'; import React, { useState, useEffect } from 'react'; import { useBoardState } from '../../state/BoardState'; diff --git a/frontend/src/json/blocks.json b/frontend/src/json/blocks.json index 7b7301f..9e6e080 100644 --- a/frontend/src/json/blocks.json +++ b/frontend/src/json/blocks.json @@ -3,7 +3,7 @@ "_id": "63cda4a235c0dc3e0c5f0455", "category": "Start", "name": "Wenn", - "src": "http://localhost:9001/uploads/1674421410153.png", + "src": "http://localhost:9001/uploads/1674420527774.png", "points": [0, 0, 200, 0, 100, 150, 200, 300, 0, 300], "color": "#f9b43d", "width": 200, @@ -44,7 +44,7 @@ "_id": "63cdabf635c0dc3e0c5f046e", "category": "Objekte", "name": "Kontakt Sensor", - "src": "http://localhost:9001/uploads/1674425498259.png", + "src": "http://localhost:9001/uploads/1674425372226.png", "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], "color": "#EB555B", "width": 400, @@ -79,7 +79,7 @@ "_id": "63cdabf635c0dc3e0c5f046e", "category": "Objekte", "name": "Lautsprecher", - "src": "http://localhost:9001/uploads/1674425498259.png", + "src": "http://localhost:9001/uploads/1674425372227.png", "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], "color": "#EB555B", "width": 400, @@ -114,7 +114,7 @@ "_id": "63cda72e35c0dc3e0c5f0460", "category": "Ende", "name": " ", - "src": "http://localhost:9001/uploads/1674422062486.png", + "src": "http://localhost:9001/uploads/1674422039501.png", "points": [0, 0, 200, 0, 200, 300, 0, 300, 100, 150], "color": "#f9b43d", "width": 200, @@ -143,7 +143,7 @@ "_id": "63cd4bg635c0dc3e0c5f046e", "category": "Objekte", "name": "Timer", - "src": "http://localhost:9001/uploads/1674425498259.png", + "src": "http://localhost:9001/uploads/1674425372230.png", "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], "color": "#EB555B", "width": 400, @@ -178,7 +178,7 @@ "_id": "63cdabg635c0dc3e0c5f046e", "category": "Objekte", "name": "Knopf", - "src": "http://localhost:9001/uploads/1674425498259.png", + "src": "http://localhost:9001/uploads/1674425372225.png", "points": [200, 300, 0, 300, -100, 150, 0, 0, 200, 0, 300, 150], "color": "#EB555B", "width": 400, @@ -213,7 +213,7 @@ "_id": "63cdad5d35c0dc3e0c5f0475", "category": "Zustand", "name": "auf", - "src": "http://localhost:9001/uploads/1674423645286.png", + "src": "http://localhost:9001/uploads/1674425372223.png", "points": [0, 0, 200, 0, 300, 150, 200, 300, 0, 300, 100, 150], "color": "#F4AECE", "width": 300, @@ -247,7 +247,7 @@ "_id": "63cdad5d35c0dc3e0c5f0475", "category": "Zustand", "name": "gib Bescheid", - "src": "http://localhost:9001/uploads/1674423645286.png", + "src": "http://localhost:9001/uploads/1674425372232.png", "points": [0, 0, 200, 0, 300, 150, 200, 300, 0, 300, 100, 150], "color": "#F4AECE", "width": 300, diff --git a/frontend/src/pages/CanvasPage.tsx b/frontend/src/pages/CanvasPage.tsx index 880f171..5370691 100644 --- a/frontend/src/pages/CanvasPage.tsx +++ b/frontend/src/pages/CanvasPage.tsx @@ -14,7 +14,7 @@ import 'prismjs/components/prism-clike'; import 'prismjs/components/prism-javascript'; import 'prismjs/components/prism-json'; import 'prismjs/themes/prism.css'; -import { ASTType } from '../AstTypes'; +import { ASTType } from '../astTypes'; const CanvasPage = () => { // Add Cursor here. const socket = useWebSocketState((state) => state.socket); diff --git a/frontend/src/state/SyntaxTreeState.tsx b/frontend/src/state/SyntaxTreeState.tsx index 025c696..5baa31d 100644 --- a/frontend/src/state/SyntaxTreeState.tsx +++ b/frontend/src/state/SyntaxTreeState.tsx @@ -2,7 +2,7 @@ // also saves the connections between them import create from 'zustand'; -import { ASTType } from '../AstTypes'; +import { ASTType } from '../astTypes'; import { mountStoreDevtool } from 'simple-zustand-devtools'; import { findConnections } from '../utils/tileConnections'; diff --git a/frontend/src/utils/generateAst.ts b/frontend/src/utils/generateAst.ts index afb7be1..de651ff 100644 --- a/frontend/src/utils/generateAst.ts +++ b/frontend/src/utils/generateAst.ts @@ -1,5 +1,4 @@ -import { CallExpression, Identifier, IfStatement, ExpressionStatement } from './../AstTypes.d'; -import { ASTType } from '../AstTypes'; +import { CallExpression, Identifier, IfStatement, ExpressionStatement, ASTType } from '../astTypes'; interface NodeType { id: string; @@ -96,31 +95,31 @@ export const generateAst = ( // send ast to backend const backendUrl = process.env.REACT_APP_BACKEND_URL; console.log('fetch ast to backend: ', JSON.stringify(ast)); - const data = Promise.all([ - fetch(`${backendUrl}/ast/js`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(ast), - }).then((value) => value.json()), - fetch(`${backendUrl}/ast/py`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(ast), - }).then((value) => value.json()), - ]); + const data = Promise.all([ + fetch(`${backendUrl}/ast/js`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(ast), + }).then((value) => value.json()), + fetch(`${backendUrl}/ast/py`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(ast), + }).then((value) => value.json()), + ]); - (async () => { - try { - const resolvedData = await data; - setGeneratedCode && - setGeneratedCode({ js: resolvedData[0].code, py: resolvedData[1].code }); - } catch (error) { - alert(error); - } - })(); + (async () => { + try { + const resolvedData = await data; + setGeneratedCode && + setGeneratedCode({ js: resolvedData[0].code, py: resolvedData[1].code }); + } catch (error) { + alert(error); + } + })(); } }; From 5b7b077401b5f9918963d2ae4c9919f74df778bf Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Sat, 8 Apr 2023 02:06:25 +0200 Subject: [PATCH 39/44] added directional shapes --- .../src/components/Tiles/TileBorderAnchor.tsx | 60 ++++++++++++++----- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/Tiles/TileBorderAnchor.tsx b/frontend/src/components/Tiles/TileBorderAnchor.tsx index 29c32b1..46b02fc 100644 --- a/frontend/src/components/Tiles/TileBorderAnchor.tsx +++ b/frontend/src/components/Tiles/TileBorderAnchor.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Rect } from 'react-konva'; +import { Line } from 'react-konva'; import { Circle as CircleObject } from 'konva/lib/shapes/Circle'; import { KonvaEventObject } from 'konva/lib/Node'; @@ -23,26 +23,54 @@ const TileBorderAnchors: React.FC = ({ x, y, id, onClick, fill, type }) = }; }; + const directionalArrows = [ + [0, 0, 25, 0, 50, 15, 25, 30, 0, 30, 0, 0], + [0, 0, -25, 0, -15, 15, -25, 25, 0, 25, 0, 0], + ]; + const anchor = React.useRef(null); return ( <> - dragBounds(anchor)} - /> + {type === 'L' || type === 'BL' || type === 'TL' ? ( + dragBounds(anchor)} + stroke={'black'} + /> + ) : ( + dragBounds(anchor)} + stroke={'black'} + /> + )} ); }; export default TileBorderAnchors; + From 4eae99769e6095f35f865c50868dabe70079d8e2 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Sat, 8 Apr 2023 08:45:47 +0200 Subject: [PATCH 40/44] added directional Arrows --- frontend/src/components/Tiles/Tile.tsx | 4 +- .../src/components/Tiles/TileBorderAnchor.tsx | 8 +- frontend/src/json/blocks.json | 74 +++++++++---------- 3 files changed, 45 insertions(+), 41 deletions(-) diff --git a/frontend/src/components/Tiles/Tile.tsx b/frontend/src/components/Tiles/Tile.tsx index 78182bd..8c144e0 100644 --- a/frontend/src/components/Tiles/Tile.tsx +++ b/frontend/src/components/Tiles/Tile.tsx @@ -45,6 +45,7 @@ const Tile: React.FC = ({ x={x + point.x} y={y + point.y} type={point.type} + category={category} onClick={handleClick} fill={fromShapeId === `${id}_${point.type}` ? 'green' : color} /> @@ -81,8 +82,9 @@ const Tile: React.FC = ({ /> diff --git a/frontend/src/components/Tiles/TileBorderAnchor.tsx b/frontend/src/components/Tiles/TileBorderAnchor.tsx index 46b02fc..231a84c 100644 --- a/frontend/src/components/Tiles/TileBorderAnchor.tsx +++ b/frontend/src/components/Tiles/TileBorderAnchor.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Line } from 'react-konva'; import { Circle as CircleObject } from 'konva/lib/shapes/Circle'; import { KonvaEventObject } from 'konva/lib/Node'; +import Category from '../Sidebar/Category'; type Props = { x: number; @@ -9,10 +10,11 @@ type Props = { id: string; fill: string; type: string; + category?: string; onClick: (event: KonvaEventObject) => void; }; -const TileBorderAnchors: React.FC = ({ x, y, id, onClick, fill, type }) => { +const TileBorderAnchors: React.FC = ({ x, y, id, onClick, fill, type, category }) => { const dragBounds = (ref: React.RefObject) => { if (ref.current !== null) { return ref.current.getAbsolutePosition(); @@ -24,8 +26,8 @@ const TileBorderAnchors: React.FC = ({ x, y, id, onClick, fill, type }) = }; const directionalArrows = [ - [0, 0, 25, 0, 50, 15, 25, 30, 0, 30, 0, 0], - [0, 0, -25, 0, -15, 15, -25, 25, 0, 25, 0, 0], + [-10, 0, 25, 0, 50, 15, 25, 30, 0, 30, -10, 0], + [20, 0, -50, 0, -25, 15, -50, 30, 35, 30, 30, 0], ]; const anchor = React.useRef(null); diff --git a/frontend/src/json/blocks.json b/frontend/src/json/blocks.json index 9e6e080..b68303d 100644 --- a/frontend/src/json/blocks.json +++ b/frontend/src/json/blocks.json @@ -28,14 +28,14 @@ }, "anchors": [ { - "type": "L", + "type": "LS", "x": 100, - "y": 145 + "y": 140 } ], "textPosition": { "x": 50, - "y": 50, + "y": 250, "_id": "63cda4a235c0dc3e0c5f0456" }, "__v": 0 @@ -59,18 +59,18 @@ "anchors": [ { "type": "L", - "x": -115, + "x": -85, "y": 140 }, { "type": "R", - "x": 290, + "x": 280, "y": 140 } ], "textPosition": { "x": 50, - "y": 50, + "y": 250, "_id": "63cdabf635c0dc3e0c5f046f" }, "__v": 1 @@ -94,18 +94,18 @@ "anchors": [ { "type": "L", - "x": -115, + "x": -90, "y": 140 }, { "type": "R", - "x": 290, + "x": 280, "y": 140 } ], "textPosition": { "x": 50, - "y": 50, + "y": 250, "_id": "63cdabf635c0dc3e0c5f046f" }, "__v": 1 @@ -129,7 +129,7 @@ { "type": "L", "x": 100, - "y": 150 + "y": 140 } ], "textPosition": { @@ -158,18 +158,18 @@ "anchors": [ { "type": "L", - "x": -115, + "x": -90, "y": 140 }, { "type": "R", - "x": 290, + "x": 280, "y": 140 } ], "textPosition": { - "x": 150, - "y": 150, + "x": 50, + "y": 250, "_id": "63cdabf635c0dc3e0c5f046f" }, "__v": 1 @@ -193,18 +193,18 @@ "anchors": [ { "type": "L", - "x": -115, + "x": -90, "y": 140 }, { "type": "R", - "x": 290, + "x": 280, "y": 140 } ], "textPosition": { - "x": 150, - "y": 150, + "x": 50, + "y": 250, "_id": "63cdabf635c0dc3e0c5f046f" }, "__v": 1 @@ -227,18 +227,18 @@ "anchors": [ { "type": "L", - "x": 73, + "x": 100, "y": 140 }, { "type": "R", - "x": 290, + "x": 275, "y": 140 } ], "textPosition": { - "x": 50, - "y": 50, + "x": 125, + "y": 250, "_id": "63cdad5d35c0dc3e0c5f0476" }, "__v": 2 @@ -261,18 +261,18 @@ "anchors": [ { "type": "L", - "x": 73, + "x": 100, "y": 140 }, { "type": "R", - "x": 290, + "x": 275, "y": 140 } ], "textPosition": { - "x": 50, - "y": 50, + "x": 90, + "y": 250, "_id": "63cdad5d35c0dc3e0c5f0476" }, "__v": 2 @@ -314,7 +314,7 @@ "anchors": [ { "type": "L", - "x": -27, + "x": -15, "y": 0 }, { @@ -324,8 +324,8 @@ } ], "textPosition": { - "x": -50, - "y": -50, + "x": 75, + "y": 100, "_id": "63cdaff535c0dc3e0c5f0479" }, "__v": 1 @@ -357,28 +357,28 @@ "anchors": [ { "type": "TL", - "x": -90, + "x": -50, "y": 50 }, { "type": "TR", - "x": 225, + "x": 190, "y": 50 }, { "type": "BL", - "x": 215, - "y": 350 + "x": -50, + "y": 300 }, { "type": "BR", - "x": -90, - "y": 350 + "x": 200, + "y": 300 } ], "textPosition": { - "x": -50, - "y": -50, + "x": 50, + "y": 350, "_id": "63cdb2bc35c0dc3e0c5f047e" }, "__v": 0 From 3149d0c67f89a68ff22cf7638a4965ecece87bc9 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Sat, 8 Apr 2023 09:52:20 +0200 Subject: [PATCH 41/44] removed stroke --- frontend/src/components/Tiles/Tile.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/frontend/src/components/Tiles/Tile.tsx b/frontend/src/components/Tiles/Tile.tsx index 8c144e0..aff32d9 100644 --- a/frontend/src/components/Tiles/Tile.tsx +++ b/frontend/src/components/Tiles/Tile.tsx @@ -80,14 +80,7 @@ const Tile: React.FC = ({ points={points} stroke={'black'} /> - + )} From 0e75920e66e533eb672212a125c6676cb3276d43 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Sat, 8 Apr 2023 12:57:49 +0200 Subject: [PATCH 42/44] added and operator --- frontend/src/astTypes.d.ts | 13 +- frontend/src/hooks/useCodeGeneration.tsx | 32 +++-- frontend/src/json/blocks.json | 24 ++-- frontend/src/pages/CanvasPage.tsx | 5 +- frontend/src/state/SyntaxTreeState.tsx | 4 +- frontend/src/utils/generateAst.ts | 168 ++++++++++++++++++++++- 6 files changed, 211 insertions(+), 35 deletions(-) diff --git a/frontend/src/astTypes.d.ts b/frontend/src/astTypes.d.ts index 6c0d00e..8b4d6c4 100644 --- a/frontend/src/astTypes.d.ts +++ b/frontend/src/astTypes.d.ts @@ -64,7 +64,10 @@ export interface Identifier { type: 'Identifier'; name: string; } - +export interface NumericLiteral { + type: 'NumericLiteral'; + name: number; +} /** * Expression Statements * basic expressions @@ -79,15 +82,15 @@ export interface MemberExpression { export interface BinaryExpression { type: 'BinaryExpression'; left: MemberExpression | Identifier | StringLiteral | null; - right: MemberExpression | Identifier | StringLiteral | null; + right: MemberExpression | Identifier | StringLiteral | NumericLiteral | null; operator: Operator | null; } // LogicalExpression can be used for if statements export interface LogicalExpression { type: 'LogicalExpression'; - left: MemberExpression | BinaryExpression | StringLiteral; - right: MemberExpression | BinaryExpression | StringLiteral; + left: BinaryExpression | MemberExpression | StringLiteral | null; + right: BinaryExpression | MemberExpression | StringLiteral | null; operator: Operator; } /** @@ -128,7 +131,7 @@ export interface ExpressionStatement { export interface IfStatement { type: 'IfStatement'; - test: BinaryExpression; + test: BinaryExpression | LogicalExpression; consequent: { type: 'BlockStatement'; body: ExpressionStatement[]; diff --git a/frontend/src/hooks/useCodeGeneration.tsx b/frontend/src/hooks/useCodeGeneration.tsx index 71efe0a..c7f3b74 100644 --- a/frontend/src/hooks/useCodeGeneration.tsx +++ b/frontend/src/hooks/useCodeGeneration.tsx @@ -38,20 +38,30 @@ export const useCodeGeneration = () => { astNode: toTileObject?.astNode, }; if (!fromTile.tileName || !toTile.tileName) return; + // special Condition for Connection 2 Conditions if ( - !allowedConnections[fromTile.tileCategory as keyof typeof allowedConnections].includes( - toTile.tileCategory as never, - ) + fromTile.tileCategory === 'Konditionen' && + fromTile.tileName === 'Und' && + toTile.tileCategory === 'Konditionen' && + toTile.tileName === 'Dann' ) { - alert( - `ungültige Verbindung: ${fromTile.tileCategory} -> ${toTile.tileCategory}. ${ - fromTile.tileCategory - } dürfen nur mit ${ - allowedConnections[fromTile.tileCategory as keyof typeof allowedConnections] - } verbunden werden.`, - ); + generateAst(fromTile, toTile, ast, setAst, connections, generatedCode, setGeneratedCode); } else { - generateAst(fromTile, toTile, ast, setAst, generatedCode, setGeneratedCode); + if ( + !allowedConnections[fromTile.tileCategory as keyof typeof allowedConnections].includes( + toTile.tileCategory as never, + ) + ) { + alert( + `ungültige Verbindung: ${fromTile.tileCategory} -> ${toTile.tileCategory}. ${ + fromTile.tileCategory + } dürfen nur mit ${ + allowedConnections[fromTile.tileCategory as keyof typeof allowedConnections] + } verbunden werden.`, + ); + } else { + generateAst(fromTile, toTile, ast, setAst, connections, generatedCode, setGeneratedCode); + } } }); }; diff --git a/frontend/src/json/blocks.json b/frontend/src/json/blocks.json index b68303d..8728a0d 100644 --- a/frontend/src/json/blocks.json +++ b/frontend/src/json/blocks.json @@ -10,20 +10,24 @@ "height": 300, "astNode": { "javaScript": { - "type": "LogicalExpression", - "left": { + "type": "IfStatement", + "test": { "type": "BinaryExpression", - "left": null, - "right": null, - "operator": "===" - }, - "right": { - "type": "BinaryExpression", - "left": null, + "left": { + "type": "MemberExpression", + "object": null, + "property": { + "type": "Identifier", + "name": "state" + } + }, "right": null, "operator": "===" }, - "operator": "&&" + "consequent": { + "type": "BlockStatement", + "body": null + } } }, "anchors": [ diff --git a/frontend/src/pages/CanvasPage.tsx b/frontend/src/pages/CanvasPage.tsx index 5370691..d9f4224 100644 --- a/frontend/src/pages/CanvasPage.tsx +++ b/frontend/src/pages/CanvasPage.tsx @@ -14,7 +14,7 @@ import 'prismjs/components/prism-clike'; import 'prismjs/components/prism-javascript'; import 'prismjs/components/prism-json'; import 'prismjs/themes/prism.css'; -import { ASTType } from '../astTypes'; +import Default from '../components/Buttons/Default'; const CanvasPage = () => { // Add Cursor here. const socket = useWebSocketState((state) => state.socket); @@ -45,6 +45,9 @@ const CanvasPage = () => { fontSize: 12, }} /> +
    + setAst(null)} text={'AST löschen'} /> +
    {generatedCode.js.length > 0 && ( diff --git a/frontend/src/state/SyntaxTreeState.tsx b/frontend/src/state/SyntaxTreeState.tsx index 5baa31d..8839148 100644 --- a/frontend/src/state/SyntaxTreeState.tsx +++ b/frontend/src/state/SyntaxTreeState.tsx @@ -6,6 +6,7 @@ import { ASTType } from '../astTypes'; import { mountStoreDevtool } from 'simple-zustand-devtools'; import { findConnections } from '../utils/tileConnections'; +export type connectionsType = { from: string; to: string }[]; export type ConnectedTilesContextType = { fromShapeId: string | null; ast: ASTType | null; @@ -13,8 +14,9 @@ export type ConnectedTilesContextType = { js: string; py: string; }; + + connections: connectionsType; connectionPreview: JSX.Element | null; - connections: { from: string; to: string }[]; setAst: (value: ASTType | null) => void; setGeneratedCode: (value: { js: string; py: string }) => void; setFromShapeId: (value: string | null) => void; diff --git a/frontend/src/utils/generateAst.ts b/frontend/src/utils/generateAst.ts index de651ff..cbdcb71 100644 --- a/frontend/src/utils/generateAst.ts +++ b/frontend/src/utils/generateAst.ts @@ -1,4 +1,14 @@ -import { CallExpression, Identifier, IfStatement, ExpressionStatement, ASTType } from '../astTypes'; +import { + CallExpression, + BinaryExpression, + LogicalExpression, + Identifier, + IfStatement, + ExpressionStatement, + ASTType, + Operator, +} from '../astTypes.d'; +import { connectionsType } from '../state/SyntaxTreeState'; interface NodeType { id: string; @@ -13,9 +23,11 @@ export const generateAst = ( toNode: NodeType | undefined, ast: ASTType | null, setAst: (ast: ASTType | null) => void, + connections: connectionsType, generatedCode?: { js: string; py: string }, setGeneratedCode?: (value: { js: string; py: string }) => void, ) => { + console.log(fromNode?.tileCategory + '->' + toNode?.tileCategory); if ( fromNode?.tileCategory === undefined || fromNode.tileName === undefined || @@ -23,9 +35,12 @@ export const generateAst = ( toNode?.tileCategory === undefined ) return; + if (fromNode.tileCategory === 'Start' && toNode.tileCategory === 'Objekte' && ast === null) { + const binary = fromNode.astNode.javaScript.test as BinaryExpression; const startNode = fromNode.astNode.javaScript as IfStatement; - startNode.test.left = toNode.astNode.javaScript as Identifier; + binary.left = toNode.astNode.javaScript as Identifier; + startNode.test = binary; setAst({ type: 'File', errors: [], @@ -36,14 +51,44 @@ export const generateAst = ( }); } if ( - toNode.tileCategory === 'Zustand' && fromNode.tileCategory === 'Objekte' && + toNode.tileCategory === 'Zustand' && ast !== null && - ast.program?.body[0].type === 'IfStatement' && - ast.program?.body[0].test?.type === 'BinaryExpression' + ast.program?.body[0].type === 'IfStatement' ) { - ast.program.body[0].test.right = toNode.astNode.javaScript as Identifier; - setAst(ast); + if (ast.program?.body[0].test?.type === 'BinaryExpression') { + ast.program.body[0].test.right = toNode.astNode.javaScript as Identifier; + setAst(ast); + } + console.log('from Obj->Zustand:', ast.program.body[0].test?.type === 'LogicalExpression'); + if (ast.program.body[0].test?.type === 'LogicalExpression') { + console.log(toNode.id); + const findOtherConnection = connections.filter((a) => a.from === `${toNode.id}_R`); + console.log(findOtherConnection); + if (findOtherConnection.length === 1) { + const direction = findOtherConnection[0].to.split('_')[1]; + console.log('direction:', direction); + if (direction === 'TL') { + ast.program.body[0].test.left = { + type: 'BinaryExpression', + left: null, + right: null, + operator: Operator.equals, + }; + ast.program.body[0].test.left.left = fromNode.astNode.javaScript; + ast.program.body[0].test.left.right = toNode.astNode.javaScript; + } else if (direction === 'BL') { + ast.program.body[0].test.right = { + type: 'BinaryExpression', + left: null, + right: null, + operator: Operator.equals, + }; + ast.program.body[0].test.right.left = fromNode.astNode.javaScript; + ast.program.body[0].test.right.right = toNode.astNode.javaScript; + } + } + } } if ( @@ -90,6 +135,114 @@ export const generateAst = ( setAst(ast); } + if ( + fromNode.tileCategory === 'Zustand' && + toNode.tileCategory === 'Konditionen' && + toNode.tileName === 'Und' && + toNode.anchorPosition === 'TL' && + ast !== null + ) { + // save the current elements in the if Condition + console.log(ast.program.body[0].test.type); + if (ast.program.body[0].test.type !== 'LogicalExpression') { + const leftBinaryExpression = ast.program.body[0].test; + const logicNode = { + type: 'IfStatement', + test: { + type: 'LogicalExpression', + left: null, + right: null, + operator: '&&', + } as LogicalExpression, + consequent: { + type: 'BlockStatement', + body: null, + }, + } as unknown as IfStatement; + + const newAst = { + type: 'File', + errors: [], + program: { + type: 'Program', + body: [logicNode], + }, + } as ASTType; + logicNode.test.left = leftBinaryExpression as BinaryExpression; + console.log('added logical Op with TL'); + setAst(newAst); + } + } + if ( + fromNode.tileCategory === 'Zustand' && + toNode.tileCategory === 'Konditionen' && + toNode.tileName === 'Und' && + toNode.anchorPosition === 'BL' && + ast !== null + ) { + // save the current elements in the if Condition + console.log(ast.program.body[0].test.type); + if (ast.program.body[0].test.type !== 'LogicalExpression') { + const leftBinaryExpression = ast.program.body[0].test; + const logicNode = { + type: 'IfStatement', + test: { + type: 'LogicalExpression', + left: null, + right: null, + operator: '&&', + } as LogicalExpression, + consequent: { + type: 'BlockStatement', + body: null, + }, + } as unknown as IfStatement; + + const newAst = { + type: 'File', + errors: [], + program: { + type: 'Program', + body: [logicNode], + }, + } as ASTType; + logicNode.test.right = leftBinaryExpression as BinaryExpression; + console.log('added logical Op with BL'); + setAst(newAst); + } + } + // if ( + // fromNode.tileCategory === 'Zustand' && + // toNode.tileCategory === 'Konditionen' && + // toNode.tileName === 'Und' && + // toNode.anchorPosition === 'BL' && + // ast !== null && + // ast.program.body[0].test.type === 'LogicalExpression' + // ) { + // ast.program.body[0].test.right = { + // type: 'BinaryExpression', + // left: null, + // right: null, + // operator: '===', + // } as BinaryExpression; + // ast.program.body[0].test.right.right = toNode.astNode.javaScript as Identifier; + // } + // if ( + // fromNode.tileCategory === 'Zustand' && + // toNode.tileCategory === 'Konditionen' && + // toNode.tileName === 'Und' && + // toNode.anchorPosition === 'TL' && + // ast !== null && + // ast.program.body[0].test.type === 'LogicalExpression' + // ) { + // ast.program.body[0].test.left = { + // type: 'BinaryExpression', + // left: null, + // right: null, + // operator: '===', + // } as BinaryExpression; + // ast.program.body[0].test.left.right = toNode.astNode.javaScript as Identifier; + // } if (fromNode.tileCategory === 'Zustand' && toNode.tileCategory === 'Ende' && ast !== null) { // send ast to backend @@ -117,6 +270,7 @@ export const generateAst = ( const resolvedData = await data; setGeneratedCode && setGeneratedCode({ js: resolvedData[0].code, py: resolvedData[1].code }); + setAst(null); } catch (error) { alert(error); } From 1a5541879bde39e9dc71a8d14f552675f42c2597 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Sat, 8 Apr 2023 13:21:45 +0200 Subject: [PATCH 43/44] added backend for py generation with and --- backend/utils/generatePyCode.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/backend/utils/generatePyCode.ts b/backend/utils/generatePyCode.ts index cea1824..383bbf9 100644 --- a/backend/utils/generatePyCode.ts +++ b/backend/utils/generatePyCode.ts @@ -1,4 +1,4 @@ -import { ASTType } from "./../types/ast.types.d"; +import { ASTType, Test } from "./../types/ast.types.d"; const getPythonLogicalOperator = (operatorString: string) => { //get python equivalent of logical Operator @@ -40,8 +40,16 @@ const generatePyCode = (ast: ASTType) => { } if (body.type === "IfStatement" && body.test.type === "LogicalExpression") { + const logicalLeft = body.test.left as any; + const logicalRight = body.test.right as any; const logicalOperator = getPythonLogicalOperator(body.test.operator); - pyCode += `if ${left.name} ${operator} ${right.value} ${logicalOperator}:`; + pyCode += `if ${logicalLeft.left.name} ${getPythonLogicalOperator( + logicalLeft.operator, + )} ${logicalLeft.right.value} ${getPythonLogicalOperator( + body.test.operator, + )} ${logicalRight.left.name} ${getPythonLogicalOperator( + logicalRight.operator, + )} ${logicalRight.right.value} :`; } if (consequent.type === "BlockStatement") { From 240050414f05fa362427224cdaa9cc73901a20c9 Mon Sep 17 00:00:00 2001 From: George Iyawe Date: Sat, 8 Apr 2023 15:37:07 +0200 Subject: [PATCH 44/44] removed unused variable --- backend/utils/generatePyCode.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/utils/generatePyCode.ts b/backend/utils/generatePyCode.ts index 383bbf9..bf34925 100644 --- a/backend/utils/generatePyCode.ts +++ b/backend/utils/generatePyCode.ts @@ -42,7 +42,6 @@ const generatePyCode = (ast: ASTType) => { if (body.type === "IfStatement" && body.test.type === "LogicalExpression") { const logicalLeft = body.test.left as any; const logicalRight = body.test.right as any; - const logicalOperator = getPythonLogicalOperator(body.test.operator); pyCode += `if ${logicalLeft.left.name} ${getPythonLogicalOperator( logicalLeft.operator, )} ${logicalLeft.right.value} ${getPythonLogicalOperator(