diff --git a/apps/client/mocks.ts b/apps/client/mocks.ts index f38cf6a9..e7815429 100644 --- a/apps/client/mocks.ts +++ b/apps/client/mocks.ts @@ -1,5 +1,5 @@ -// import { Group, Node } from '@types'; -// import { nanoid } from 'nanoid'; +import { Group, Node } from '@types'; +import { nanoid } from 'nanoid'; const CloudFunctionNode: Node = { id: `node-${nanoid()}`, diff --git a/apps/client/src/CloudGraph.tsx b/apps/client/src/CloudGraph.tsx index 2bf94889..8391b6ad 100644 --- a/apps/client/src/CloudGraph.tsx +++ b/apps/client/src/CloudGraph.tsx @@ -7,12 +7,13 @@ import GridBackground from '@components/GridBackground'; import Group from '@components/Group'; import Node from '@components/Node'; import { useEdgeContext } from '@contexts/EdgeContext'; +import { useGraphContext } from '@contexts/GraphConetxt'; import { useGroupContext } from '@contexts/GroupContext'; import { useNodeContext } from '@contexts/NodeContext'; import useConnection from '@hooks/useConnection'; import useGraph from '@hooks/useGraph'; import useSelection from '@hooks/useSelection'; -import { useEffect, useLayoutEffect } from 'react'; +import { useEffect, useLayoutEffect, useRef } from 'react'; export default () => { const { @@ -24,6 +25,11 @@ export default () => { const { state: { groups }, } = useGroupContext(); + const { + state: { viewBox }, + dispatch: graphDispatch, + } = useGraphContext(); + const { selectedNodeId, selectedEdge, @@ -36,14 +42,13 @@ export default () => { const { svgRef, - prevDimension, dimension, moveNode, addEdge, splitEdge, - updatedPointForDimension, moveBendingPointer, getGroupBounds, + updateNodePointForDimension, moveGroup, removeNode, removeEdge, @@ -59,6 +64,9 @@ export default () => { updateEdgeFn: addEdge, }); + const nodesRef = useRef(nodes); + const prevDimensionRef = useRef(dimension); + useEffect(() => { const handleContextMenu = (e: MouseEvent) => e.preventDefault(); const handleMouseDown = (e: MouseEvent) => { @@ -75,11 +83,20 @@ export default () => { }; }, []); - useLayoutEffect(() => { - if (dimension === prevDimension) return; + useEffect(() => { + prevDimensionRef.current = dimension; + }, [dimension]); + []; + + useEffect(() => { + nodesRef.current = nodes; + }, [nodes]); - updatedPointForDimension(); + useLayoutEffect(() => { + if (prevDimensionRef.current === dimension) return; + updateNodePointForDimension(dimension); }, [dimension]); + return ( diff --git a/apps/client/src/components/Graph/index.tsx b/apps/client/src/components/Graph/index.tsx index 8518b7c7..75b49b84 100644 --- a/apps/client/src/components/Graph/index.tsx +++ b/apps/client/src/components/Graph/index.tsx @@ -15,7 +15,15 @@ export default ({ children }: PropsWithChildren) => { const isPanning = useRef(false); const startPoint = useRef({ x: 0, y: 0 }); const spaceActiveKey = useKey('space'); + const initialViewBox = { + x: 0, + y: 0, + width: svgRef.current?.clientWidth || 0, + height: svgRef.current?.clientHeight || 0, + }; + const MIN_ZOOM = 0.2; + const MAX_ZOOM = 1; const zoom = (wheelY: number, point: Point) => { if (!svgRef.current) return; @@ -23,6 +31,12 @@ export default ({ children }: PropsWithChildren) => { const cursorSvgPoint = getSvgPoint(svgRef.current, point); if (!cursorSvgPoint) return; + const currentZoom = initialViewBox.width / viewBox.width; + const newZoom = currentZoom * (1 / zoomFactor); + + if (newZoom < MIN_ZOOM || newZoom > MAX_ZOOM) { + return; + } dispatch({ type: 'SET_VIEWBOX', payload: { diff --git a/apps/client/src/components/Group/ncloud/Rect3D.tsx b/apps/client/src/components/Group/ncloud/Rect3D.tsx new file mode 100644 index 00000000..bb248811 --- /dev/null +++ b/apps/client/src/components/Group/ncloud/Rect3D.tsx @@ -0,0 +1,55 @@ +import { Bounds } from '@types'; +import { screenToGrid2d, gridToScreen3d } from '@utils'; +import { ReactNode } from 'react'; + +type Props = { + children: ReactNode; + bounds: Bounds; + color: string; + [key: string]: any; +}; +export default ({ bounds, properties, color, children }: Props) => { + const topLeftGrid = screenToGrid2d({ x: 0, y: 0 }); + const topRightGrid = screenToGrid2d({ x: bounds.width, y: 0 }); + const bottomRightGrid = screenToGrid2d({ + x: bounds.width, + y: bounds.height, + }); + const bottomLeftGrid = screenToGrid2d({ x: 0, y: bounds.height }); + + const point1 = gridToScreen3d({ + col: topLeftGrid.col, + row: topLeftGrid.row, + }); + const point2 = gridToScreen3d({ + col: topRightGrid.col, + row: topRightGrid.row, + }); + const point3 = gridToScreen3d({ + col: bottomRightGrid.col, + row: bottomRightGrid.row, + }); + const point4 = gridToScreen3d({ + col: bottomLeftGrid.col, + row: bottomLeftGrid.row, + }); + + const points = ` + ${point1.x} ${point1.y}, + ${point2.x} ${point2.y}, + ${point3.x} ${point3.y}, + ${point4.x} ${point4.y} + `; + + return ( + <> + + {children} + + ); +}; diff --git a/apps/client/src/components/Group/ncloud/RegionGroup.tsx b/apps/client/src/components/Group/ncloud/RegionGroup.tsx index 2d222447..f583e427 100644 --- a/apps/client/src/components/Group/ncloud/RegionGroup.tsx +++ b/apps/client/src/components/Group/ncloud/RegionGroup.tsx @@ -1,8 +1,9 @@ import Text from '@components/Group/ncloud/Title'; import { useDimensionContext } from '@contexts/DimensionContext'; import { Bounds, Group } from '@types'; -import { generateRandomRGB, gridToScreen3d, screenToGrid2d } from '@utils'; +import { generateRandomRGB } from '@utils'; import { useMemo } from 'react'; +import Rect3D from './Rect3D'; interface Props extends Partial { color: string; @@ -10,52 +11,15 @@ interface Props extends Partial { } const Region3D = ({ bounds, properties, color }: Props) => { - const topLeftGrid = screenToGrid2d({ x: 0, y: 0 }); - const topRightGrid = screenToGrid2d({ x: bounds.width, y: 0 }); - const bottomRightGrid = screenToGrid2d({ - x: bounds.width, - y: bounds.height, - }); - const bottomLeftGrid = screenToGrid2d({ x: 0, y: bounds.height }); - - const point1 = gridToScreen3d({ - col: topLeftGrid.col + 1, - row: topLeftGrid.row, - }); - const point2 = gridToScreen3d({ - col: topRightGrid.col + 1, - row: topRightGrid.row, - }); - const point3 = gridToScreen3d({ - col: bottomRightGrid.col + 1, - row: bottomRightGrid.row, - }); - const point4 = gridToScreen3d({ - col: bottomLeftGrid.col + 1, - row: bottomLeftGrid.row, - }); - - const points = ` - ${point1.x} ${point1.y}, - ${point2.x} ${point2.y}, - ${point3.x} ${point3.y}, - ${point4.x} ${point4.y} - `; - return ( - <> - + - + ); }; const Region2D = ({ bounds, color, properties }: Props) => { + console.log(bounds); const points = `0 0, 0 ${bounds.height}, ${bounds.width} ${bounds.height}, ${bounds.width} 0`; return ( diff --git a/apps/client/src/components/Group/ncloud/SubnetGroup.tsx b/apps/client/src/components/Group/ncloud/SubnetGroup.tsx index 2d222447..8f43ff75 100644 --- a/apps/client/src/components/Group/ncloud/SubnetGroup.tsx +++ b/apps/client/src/components/Group/ncloud/SubnetGroup.tsx @@ -1,61 +1,24 @@ import Text from '@components/Group/ncloud/Title'; import { useDimensionContext } from '@contexts/DimensionContext'; import { Bounds, Group } from '@types'; -import { generateRandomRGB, gridToScreen3d, screenToGrid2d } from '@utils'; +import { calcIsoMatrixPoint, generateRandomRGB } from '@utils'; import { useMemo } from 'react'; +import Rect3D from './Rect3D'; interface Props extends Partial { color: string; bounds: Bounds; } -const Region3D = ({ bounds, properties, color }: Props) => { - const topLeftGrid = screenToGrid2d({ x: 0, y: 0 }); - const topRightGrid = screenToGrid2d({ x: bounds.width, y: 0 }); - const bottomRightGrid = screenToGrid2d({ - x: bounds.width, - y: bounds.height, - }); - const bottomLeftGrid = screenToGrid2d({ x: 0, y: bounds.height }); - - const point1 = gridToScreen3d({ - col: topLeftGrid.col + 1, - row: topLeftGrid.row, - }); - const point2 = gridToScreen3d({ - col: topRightGrid.col + 1, - row: topRightGrid.row, - }); - const point3 = gridToScreen3d({ - col: bottomRightGrid.col + 1, - row: bottomRightGrid.row, - }); - const point4 = gridToScreen3d({ - col: bottomLeftGrid.col + 1, - row: bottomLeftGrid.row, - }); - - const points = ` - ${point1.x} ${point1.y}, - ${point2.x} ${point2.y}, - ${point3.x} ${point3.y}, - ${point4.x} ${point4.y} - `; - +const Subnet3D = ({ bounds, properties, color }: Props) => { return ( - <> - + - + ); }; -const Region2D = ({ bounds, color, properties }: Props) => { +const Subnet2D = ({ bounds, color, properties }: Props) => { const points = `0 0, 0 ${bounds.height}, ${bounds.width} ${bounds.height}, ${bounds.width} 0`; return ( @@ -76,8 +39,8 @@ export default ({ bounds, properties }: Omit) => { const color = useMemo(() => generateRandomRGB(), []); return dimension === '2d' ? ( - + ) : ( - + ); }; diff --git a/apps/client/src/components/Group/ncloud/Title.tsx b/apps/client/src/components/Group/ncloud/Title.tsx index 6ba19d10..f76a5310 100644 --- a/apps/client/src/components/Group/ncloud/Title.tsx +++ b/apps/client/src/components/Group/ncloud/Title.tsx @@ -24,7 +24,7 @@ export default ({ bounds, color, text }: Props) => { const rectWidth = fontSize * textLength; const rectHeight = 50; const rectY = 10; - const rectX = dimension === '2d' ? 10 : 90; + const rectX = 10; const matrix = convertToIsoMatrix(bounds.x, bounds.y).toString(); const centerX = rectWidth / 2 + rectX; diff --git a/apps/client/src/components/Group/ncloud/VPCGroup.tsx b/apps/client/src/components/Group/ncloud/VPCGroup.tsx index 2d222447..567a61c3 100644 --- a/apps/client/src/components/Group/ncloud/VPCGroup.tsx +++ b/apps/client/src/components/Group/ncloud/VPCGroup.tsx @@ -1,61 +1,24 @@ import Text from '@components/Group/ncloud/Title'; import { useDimensionContext } from '@contexts/DimensionContext'; import { Bounds, Group } from '@types'; -import { generateRandomRGB, gridToScreen3d, screenToGrid2d } from '@utils'; +import { calcIsoMatrixPoint, generateRandomRGB } from '@utils'; import { useMemo } from 'react'; +import Rect3D from './Rect3D'; interface Props extends Partial { color: string; bounds: Bounds; } -const Region3D = ({ bounds, properties, color }: Props) => { - const topLeftGrid = screenToGrid2d({ x: 0, y: 0 }); - const topRightGrid = screenToGrid2d({ x: bounds.width, y: 0 }); - const bottomRightGrid = screenToGrid2d({ - x: bounds.width, - y: bounds.height, - }); - const bottomLeftGrid = screenToGrid2d({ x: 0, y: bounds.height }); - - const point1 = gridToScreen3d({ - col: topLeftGrid.col + 1, - row: topLeftGrid.row, - }); - const point2 = gridToScreen3d({ - col: topRightGrid.col + 1, - row: topRightGrid.row, - }); - const point3 = gridToScreen3d({ - col: bottomRightGrid.col + 1, - row: bottomRightGrid.row, - }); - const point4 = gridToScreen3d({ - col: bottomLeftGrid.col + 1, - row: bottomLeftGrid.row, - }); - - const points = ` - ${point1.x} ${point1.y}, - ${point2.x} ${point2.y}, - ${point3.x} ${point3.y}, - ${point4.x} ${point4.y} - `; - +const VPC3D = ({ bounds, properties, color }: Props) => { return ( - <> - + - + ); }; -const Region2D = ({ bounds, color, properties }: Props) => { +const VPC2D = ({ bounds, color, properties }: Props) => { const points = `0 0, 0 ${bounds.height}, ${bounds.width} ${bounds.height}, ${bounds.width} 0`; return ( @@ -76,8 +39,8 @@ export default ({ bounds, properties }: Omit) => { const color = useMemo(() => generateRandomRGB(), []); return dimension === '2d' ? ( - + ) : ( - + ); }; diff --git a/apps/client/src/components/Layout/Header/index.tsx b/apps/client/src/components/Layout/Header/index.tsx index ce8bdf09..f6e486b8 100644 --- a/apps/client/src/components/Layout/Header/index.tsx +++ b/apps/client/src/components/Layout/Header/index.tsx @@ -44,7 +44,7 @@ const StyledIconButton = styled(IconButton)(({ theme }) => ({ export default () => { const { mode: themeMode, setMode: setThemeMode } = useColorScheme(); - const { dimension, toggleDimension } = useDimensionContext(); + const { dimension, changeDimension } = useDimensionContext(); const [openDrawer, setOpenDrawer] = useState(false); const [terraformCode, setTerraformCode] = useState(''); const { selectedResource } = useNCloud(); @@ -66,7 +66,6 @@ export default () => { } const Converter = new TerraformConverter(); - console.log(nodeProperties); Converter.addResourceFromJson([nodeProperties]); setTerraformCode(Converter.generate()); setOpenDrawer(true); @@ -95,7 +94,9 @@ export default () => { + changeDimension(dimension === '2d' ? '3d' : '2d') + } sx={{ height: '38px', }} diff --git a/apps/client/src/components/Node/index.tsx b/apps/client/src/components/Node/index.tsx index 651ca763..7db83c70 100644 --- a/apps/client/src/components/Node/index.tsx +++ b/apps/client/src/components/Node/index.tsx @@ -1,11 +1,13 @@ import CloudFunctionNode from '@components/Node/ncloud/CloudFunctionNode'; -import DBMySQLNode from '@components/Node/ncloud/DBMySQLNode'; +import ContainerRegistryNode from '@components/Node/ncloud/ContainerRegistry'; +import MySQLDBNode from '@components/Node/ncloud/MySQLDBNode'; import ObjectStorageNode from '@components/Node/ncloud/ObjectStorageNode'; import ServerNode from '@components/Node/ncloud/ServerNode'; import useDrag from '@hooks/useDrag'; import { Node, Point } from '@types'; import { useEffect } from 'react'; -import LoadBalancer from './ncloud/LoadBalancer'; +import LoadBalancerNode from './ncloud/LoadBalancer'; +import NatGatewayNode from './ncloud/NatGateway'; const nodeFactory = (node: Node) => { switch (node.type) { @@ -16,13 +18,15 @@ const nodeFactory = (node: Node) => { case 'object-storage': return ; case 'db-mysql': - return ; + return ; case 'load-balancer': - return ; + return ; case 'object-storage': return ; - // case 'container-registry': - // return ; + case 'container-registry': + return ; + case 'nat-gateway': + return ; default: null; } diff --git a/apps/client/src/components/Node/ncloud/ContainerRegistry.tsx b/apps/client/src/components/Node/ncloud/ContainerRegistry.tsx index 206cda70..c80e251b 100644 --- a/apps/client/src/components/Node/ncloud/ContainerRegistry.tsx +++ b/apps/client/src/components/Node/ncloud/ContainerRegistry.tsx @@ -4,21 +4,79 @@ import { Node } from '@types'; type Props = Partial; //TODO: const Node3D = ({ properties }: Props) => { - const width = 512; - const height = 296; - const point = `${width / 2},0 ${width},${height / 2} ${width / 2},${height} 0,${height / 2}`; - const isoMatrix = new DOMMatrix() - .rotate(30) - .skewX(-30) - .scale(1, 0.8602) - .translate(0, 0); return ( - + + + + + + + + + + + + + + + + + + + + ECS Cluster + + + ECS Cluster + + + + ); }; @@ -52,11 +110,11 @@ const Node2D = ({ properties }: Props) => { ; + +const convertToIsoMatrix = (x: number, y: number) => { + const isoMatrix = new DOMMatrix() + .rotate(30) + .skewX(-30) + .scale(1, 0.8602) + .translate(x, y); + + return isoMatrix; // 결과 행렬 반환 +}; +const Node3D = ({ properties }: Props) => { + const matrix = convertToIsoMatrix(0, 0); + + return ( + + + + + + + + + + + + ); +}; + +const Node2D = ({ properties }: Props) => { + return ( + + + + + + + + + + ); +}; + +export default ({ properties }: Partial) => { + const { dimension } = useDimensionContext(); + return dimension === '2d' ? ( + + ) : ( + + ); +}; diff --git a/apps/client/src/constants/index.ts b/apps/client/src/constants/index.ts index 9f24032b..39a4acf9 100644 --- a/apps/client/src/constants/index.ts +++ b/apps/client/src/constants/index.ts @@ -22,21 +22,21 @@ export const NCLOUD_SERVICES = [ }, ], }, - // { - // title: 'container', - // items: [ - // { - // title: 'Container Registry', - // desc: 'Container Registry', - // type: 'container-registry', - // }, - // { - // title: 'Kubernetes', - // desc: 'NCloud Kubernetes Service', - // type: 'Kubernetes', - // }, - // ], - // }, + { + title: 'container', + items: [ + { + title: 'Container Registry', + desc: 'Container Registry', + type: 'container-registry', + }, + // { + // title: 'Kubernetes', + // desc: 'NCloud Kubernetes Service', + // type: 'Kubernetes', + // }, + ], + }, { title: 'storage', items: [ @@ -85,6 +85,7 @@ export const NCLOUD_SERVICES = [ desc: 'load balancing', type: 'load-balancer', }, + { title: 'Nat Gateway', desc: 'nat gateway', type: 'nat-gateway' }, ], }, ]; diff --git a/apps/client/src/contexts/DimensionContext.tsx b/apps/client/src/contexts/DimensionContext.tsx index 274a5931..b174b9ba 100644 --- a/apps/client/src/contexts/DimensionContext.tsx +++ b/apps/client/src/contexts/DimensionContext.tsx @@ -3,27 +3,24 @@ import { createContext, ReactNode, useContext, useRef, useState } from 'react'; type DimensionState = { dimension: Dimension; - prevDimension: Dimension; - toggleDimension: () => void; + changeDimension: (newDimension: Dimension) => void; }; const DimensionContext = createContext(null); export const DimensionProvider = ({ children }: { children: ReactNode }) => { const [dimension, setDimension] = useState('2d'); - const prevDimensionRef = useRef('2d'); - const toggleDimension = () => { - prevDimensionRef.current = dimension; - setDimension((prev) => (prev === '2d' ? '3d' : '2d')); + const changeDimension = (newDimension: Dimension) => { + if (dimension === newDimension) return; + setDimension(newDimension); }; return ( {children} diff --git a/apps/client/src/contexts/GraphConetxt/index.tsx b/apps/client/src/contexts/GraphConetxt/index.tsx index 7d0b2971..ce705a13 100644 --- a/apps/client/src/contexts/GraphConetxt/index.tsx +++ b/apps/client/src/contexts/GraphConetxt/index.tsx @@ -23,7 +23,13 @@ const initialState = { viewBox: { x: 0, y: 0, width: 0, height: 0 }, }; -export const GraphProvider = ({ children }: { children: ReactNode }) => { +export const GraphProvider = ({ + children, + initialZoomFactor = 2, +}: { + children: ReactNode; + initialZoomFactor: number; +}) => { const [state, dispatch] = useReducer(graphReducer, initialState); useLayoutEffect(() => { @@ -35,8 +41,8 @@ export const GraphProvider = ({ children }: { children: ReactNode }) => { payload: { x: state.viewBox.x || 0, y: state.viewBox.y || 0, - width: state.viewBox.width || svg.clientWidth, - height: state.viewBox.height || svg.clientHeight, + width: (state.viewBox.width || svg.clientWidth) * 2, + height: (state.viewBox.height || svg.clientHeight) * 2, }, }); }; diff --git a/apps/client/src/helpers/edge.ts b/apps/client/src/helpers/edge.ts index f5b624a2..aef3e6ba 100644 --- a/apps/client/src/helpers/edge.ts +++ b/apps/client/src/helpers/edge.ts @@ -112,6 +112,7 @@ export const updateNearestConnectorPair = ( const lastBendPoint = edge.bendingPoints[edge.bendingPoints.length - 1]; const bendConnector = generateBendConnector(lastBendPoint); + //TODO: 이름 변경 필요 const { movingConnector } = findNearestConnectorPair( movingConnectors, [bendConnector], diff --git a/apps/client/src/helpers/group.ts b/apps/client/src/helpers/group.ts index 29cd1e35..cc3dc50f 100644 --- a/apps/client/src/helpers/group.ts +++ b/apps/client/src/helpers/group.ts @@ -1,6 +1,7 @@ -import { GRID_2D_SIZE } from '@constants'; -import { Bounds, Dimension, Group } from '@types'; +import { NODE_BASE_SIZE } from '@constants'; +import { Dimension, Group, Node, Size3D } from '@types'; import { convert2dTo3dPoint, convert3dTo2dPoint } from '@utils'; +import { getNodeOffsetForDimension } from './node'; export const GraphGroup = { id: '', @@ -11,44 +12,65 @@ export const GraphGroup = { parentGroupId: '', }; -export const computeBounds = (_bounds: Bounds[], dimension: Dimension) => { - const padding = GRID_2D_SIZE * 2; - let bounds = _bounds; - if (dimension === '3d') { - bounds = bounds.map((bound) => ({ - ...bound, - ...convert3dTo2dPoint({ - x: bound.x, - y: bound.y, - }), - })); - } +export const computeBounds = ( + nodes: Node[], + dimension: Dimension, + paddingSize: number = 1, +) => { + const padding = 90 * paddingSize; - const minX = Math.min(...bounds.map((bounds) => bounds.x)); - const minY = Math.min(...bounds.map((bounds) => bounds.y)); - const maxX = Math.max(...bounds.map((bounds) => bounds.x + bounds.width)); - const maxY = Math.max(...bounds.map((bounds) => bounds.y + bounds.height)); + if (dimension === '2d') { + const minX = Math.min(...nodes.map((node) => node.point.x)); + const minY = Math.min(...nodes.map((node) => node.point.y)); + const maxX = Math.max( + ...nodes.map((node) => node.point.x + node.size['2d'].width), + ); + const maxY = Math.max( + ...nodes.map((node) => node.point.y + node.size['2d'].height), + ); - let x = minX - padding; - let y = minY - padding; - let width = maxX - minX + padding * 2; - let height = maxY - minY + padding * 2; + return { + x: minX - padding, + y: minY - padding, + width: maxX - minX + padding * 2, + height: maxY - minY + padding * 2, + }; + } else { + //2d + nodes = nodes.map((node) => { + const offset = getNodeOffsetForDimension( + node.size['3d'], + NODE_BASE_SIZE['3d'] as Size3D, + ); + const pos = convert3dTo2dPoint({ + x: node.point.x - offset.x, + y: node.point.y - offset.y, + }); + return { + ...node, + point: { x: pos.x, y: pos.y }, + }; + }); + const minX = Math.min(...nodes.map((node) => node.point.x)); + const minY = Math.min(...nodes.map((node) => node.point.y)); + const maxX = Math.max( + ...nodes.map((node) => node.point.x + node.size['2d'].width), + ); + const maxY = Math.max( + ...nodes.map((node) => node.point.y + node.size['2d'].height), + ); - if (dimension === '3d') { - const minPoint = convert2dTo3dPoint({ + const { x, y } = convert2dTo3dPoint({ x: minX - padding, y: minY - padding, }); - x = minPoint.x; - y = minPoint.y; + return { + x, + y, + width: maxX - minX + padding * 2, + height: maxY - minY + padding * 2, + }; } - - return { - x, - y, - width, - height, - }; }; export const findParentGroup = ( diff --git a/apps/client/src/helpers/node.ts b/apps/client/src/helpers/node.ts index cdf97864..f39c5411 100644 --- a/apps/client/src/helpers/node.ts +++ b/apps/client/src/helpers/node.ts @@ -1,5 +1,5 @@ import { NODE_BASE_SIZE } from '@constants'; -import { Dimension, Node, Point, Size } from '@types'; +import { Dimension, Node, Point, Size, Size3D } from '@types'; import { alignPoint2d, alignPoint3d, @@ -14,8 +14,16 @@ export const GraphNode = { connectors: {}, }; -//TODO: 사이즈가 큰 노드를 다룰 떄 이상해짐 -const getNodeOffsetForDimension = (nodeSize: Size, baseSize: Size) => { +export const getNodeOffsetForDimension = ( + nodeSize: Size & Size3D, + baseSize: Size, +) => { + if (nodeSize.width % 128 === 0 && nodeSize.height % 111 === 0) { + return { + x: 0, + y: 0, + }; + } return { x: (baseSize.width - nodeSize.width) / 2, y: baseSize.height - nodeSize.height - (nodeSize.offset || 0), @@ -25,13 +33,14 @@ const getNodeOffsetForDimension = (nodeSize: Size, baseSize: Size) => { //INFO: 처음이 2d로 시작하기 때문에 nodeSize : 3d , baseSize : 3d로 해야함. 다른 방법은 잘 모르곘음. //2d에서 3d로 변환할 때는 3d에서 2d로 변환할 때와 달리 baseSize와 nodeSize가 2d 사이즈 들어가야 할 것 같음 export const adjustNodePointForDimension = ( - node: Node, + point: Point, + size: Size3D, dimension: Dimension, ) => { - const { point, size } = node; - - const offset = getNodeOffsetForDimension(size['3d'], NODE_BASE_SIZE['3d']); - console.log('offset', node.type, offset); + const offset = getNodeOffsetForDimension( + size, + NODE_BASE_SIZE['3d'] as Size3D, + ); let result; if (dimension === '2d') { result = convert3dTo2dPoint({ @@ -74,12 +83,3 @@ export const alignNodePoint = ( return result; }; - -export const getNodeBounds = (node: Node, dimension: Dimension) => { - return { - x: node.point.x, - y: node.point.y, - width: node.size[dimension].width, - height: node.size[dimension].height, - }; -}; diff --git a/apps/client/src/hooks/useGraph.ts b/apps/client/src/hooks/useGraph.ts index a0a93582..05491728 100644 --- a/apps/client/src/hooks/useGraph.ts +++ b/apps/client/src/hooks/useGraph.ts @@ -1,5 +1,6 @@ import { useDimensionContext } from '@contexts/DimensionContext'; import { useEdgeContext } from '@contexts/EdgeContext'; +import { useGraphContext } from '@contexts/GraphConetxt'; import { useGroupContext } from '@contexts/GroupContext'; import { useNodeContext } from '@contexts/NodeContext'; import { useSvgContext } from '@contexts/SvgContext'; @@ -9,13 +10,9 @@ import { updateNearestConnectorPair, } from '@helpers/edge'; import { computeBounds } from '@helpers/group'; -import { - adjustNodePointForDimension, - alignNodePoint, - getNodeBounds, -} from '@helpers/node'; +import { adjustNodePointForDimension, alignNodePoint } from '@helpers/node'; import useSelection from '@hooks/useSelection'; -import { Connection, Edge, Group, Node, Point } from '@types'; +import { Connection, Dimension, Edge, Group, Node, Point } from '@types'; import { alignPoint2d, alignPoint3d, @@ -40,8 +37,13 @@ export default () => { dispatch: groupDispatch, } = useGroupContext(); + const { + state: { viewBox }, + dispatch: graphDispatch, + } = useGraphContext(); + const { clearSelection } = useSelection(); - const { dimension, prevDimension } = useDimensionContext(); + const { dimension } = useDimensionContext(); const { svgRef } = useSvgContext(); //INFO: Node @@ -118,27 +120,116 @@ export default () => { clearSelection(); }; - const updateNodePointForDimension = () => { - const updatedNodes = Object.entries(nodes).reduce((acc, [id, node]) => { - const adjustedPoint = adjustNodePointForDimension(node, dimension); - const connectors = getConnectorPoints( - { ...node, point: adjustedPoint }, - dimension, + //TODO: Refactoring필요 + const updateNodePointForDimension = (dimension: Dimension) => { + //INFO: update node + const updatedNodes: Record = Object.entries(nodes).reduce( + (acc, [id, node]) => { + const adjustedPoint = adjustNodePointForDimension( + node.point, + node.size['3d'], + dimension, + ); + + const connectors = getConnectorPoints( + { ...node, point: adjustedPoint }, + dimension, + ); + + return { + ...acc, + [id]: { + ...node, + point: adjustedPoint, + connectors, + }, + }; + }, + {}, + ); + + //INFO:update edge + let updatedEdges = Object.entries(edges).reduce((acc, [id, edge]) => { + const adjustedBendingPoints = edge.bendingPoints.map((point) => + dimension === '2d' + ? convert3dTo2dPoint(point) + : convert2dTo3dPoint(point), ); + return { ...acc, [id]: { - ...node, - point: adjustedPoint, - connectors, + ...edge, + bendingPoints: adjustedBendingPoints, }, }; }, {}); + // const updatedEdgePairs = Object.entries(updatedNodes).reduce( + // (acc, [id, node]) => { + // const connectedEdges = Object.values(updatedEdges).filter( + // (edge) => edge.source.id === id || edge.target.id === id, + // ); + // const result = updateNearestConnectorPair( + // node, + // updatedNodes, + // connectedEdges as Edge[], + // ); + // return { + // ...acc, + // ...result, + // }; + // }, + // {}, + // ); + + //INFO: update ViewBox + const updatedNodesArr = Object.values(updatedNodes); + const minX = Math.min( + ...updatedNodesArr.map((node: Node) => node.point.x), + ); + const minY = Math.min( + ...updatedNodesArr.map((node: Node) => node.point.y), + ); + + const maxX = Math.max( + ...updatedNodesArr.map( + (node: Node) => node.point.x + node.size[dimension].width, + ), + ); + const maxY = Math.max( + ...updatedNodesArr.map( + (node: Node) => node.point.y + node.size[dimension].height, + ), + ); + + const nodesWidth = maxX - minX; + const nodesHeight = maxY - minY; + + const centerX = minX + nodesWidth / 2; + const centerY = minY + nodesHeight / 2; + + const paddingWidth = (viewBox.x + viewBox.width) / 2; + const paddingHeight = (viewBox.y + viewBox.height) / 2; + + graphDispatch({ + type: 'SET_VIEWBOX', + payload: { + ...viewBox, + x: centerX - paddingWidth, + y: centerY - paddingHeight, + }, + }); + nodeDispatch({ type: 'UPDATE_NODES', payload: updatedNodes, }); + + edgeDispatch({ + type: 'UPDATE_EDGES', + payload: updatedEdges, + }); }; //INFO: Edge @@ -256,29 +347,6 @@ export default () => { }); }; - const updateEdgePointForDimension = () => { - const updatedEdges = Object.entries(edges).reduce((acc, [id, edge]) => { - const adjustedBendingPoints = edge.bendingPoints.map((point) => - dimension === '2d' - ? convert3dTo2dPoint(point) - : convert2dTo3dPoint(point), - ); - - return { - ...acc, - [id]: { - ...edge, - bendingPoints: adjustedBendingPoints, - }, - }; - }, {}); - - edgeDispatch({ - type: 'UPDATE_EDGES', - payload: updatedEdges, - }); - }; - //INFO: Group const addGroup = (group: Group) => { @@ -374,36 +442,44 @@ export default () => { const recursiveGroupBounds = (group: Group): any => { if (group.childGroupIds.length === 0) { - const nodesBounds = group.nodeIds.map((nodeId) => - getNodeBounds(nodes[nodeId], dimension), - ); + const innerNodes = group.nodeIds.map((nodeId) => nodes[nodeId]); - return computeBounds(nodesBounds, dimension); + return computeBounds(innerNodes, dimension, 2); } - const childGroupsBounds = group.childGroupIds.map((childGroupId) => - recursiveGroupBounds(groups[childGroupId]), - ); - - const currentNodesBounds = group.nodeIds.map((nodeId) => - getNodeBounds(nodes[nodeId], dimension), - ); + const childGroups = group.childGroupIds.map((childGroupId) => { + const bounds = recursiveGroupBounds(groups[childGroupId]); + //TODO: Group 생성시 bounds 넣어줘야될 것 같음 + return { + point: { + x: bounds.x, + y: bounds.y, + }, + size: { + '2d': { + width: bounds.width, + height: bounds.height, + }, + '3d': { + width: 0, + height: 0, + }, + }, + }; + }) as Node[]; + + const currentNodes = group.nodeIds.map((nodeId) => nodes[nodeId]); return computeBounds( - [...currentNodesBounds, ...childGroupsBounds], + [...currentNodes, ...childGroups], dimension, + 2, ); }; return recursiveGroupBounds(group); }; - const updatedPointForDimension = () => { - updateNodePointForDimension(); - updateEdgePointForDimension(); - }; - return { - prevDimension, dimension, svgRef, nodes, @@ -415,6 +491,7 @@ export default () => { addEdge, removeEdge, splitEdge, + updateNodePointForDimension, moveBendingPointer, addGroup, addChildGroup, @@ -426,7 +503,6 @@ export default () => { moveGroup, removeGroup, removeNodeFromGroup, - updatedPointForDimension, excludeNodeFromGroup, }; }; diff --git a/apps/client/src/main.tsx b/apps/client/src/main.tsx index fa5241c8..a201aa48 100644 --- a/apps/client/src/main.tsx +++ b/apps/client/src/main.tsx @@ -15,9 +15,9 @@ createRoot(document.getElementById('root')!).render( - - - + + + @@ -28,8 +28,8 @@ createRoot(document.getElementById('root')!).render( - - + + , ); diff --git a/apps/client/src/models/ncloud/CloudFunction.ts b/apps/client/src/models/ncloud/CloudFunction.ts index 365537f2..3195f4e6 100644 --- a/apps/client/src/models/ncloud/CloudFunction.ts +++ b/apps/client/src/models/ncloud/CloudFunction.ts @@ -13,7 +13,7 @@ export const CloudFunctionNode: Node & { type: 'cloud-function', size: { '2d': { width: 90, height: 90 }, - '3d': { width: 96, height: 113.438, offset: 10 }, + '3d': { width: 96, height: 113.438, depth: 58, offset: 12.5 }, }, properties: { ...Networks, diff --git a/apps/client/src/models/ncloud/ContainerRegistry.ts b/apps/client/src/models/ncloud/ContainerRegistry.ts index 8a851d76..4a9d3aba 100644 --- a/apps/client/src/models/ncloud/ContainerRegistry.ts +++ b/apps/client/src/models/ncloud/ContainerRegistry.ts @@ -13,7 +13,7 @@ export const ContainerRegistryNode: Node & { type: 'container-registry', size: { '2d': { width: 360, height: 360 }, - '3d': { width: 360, height: 360 }, + '3d': { width: 512, height: 333, depth: 37, offset: 0 }, }, properties: { ...Networks, diff --git a/apps/client/src/models/ncloud/LoadBalancerNode.ts b/apps/client/src/models/ncloud/LoadBalancer.ts similarity index 91% rename from apps/client/src/models/ncloud/LoadBalancerNode.ts rename to apps/client/src/models/ncloud/LoadBalancer.ts index f4f994ee..ba15c24b 100644 --- a/apps/client/src/models/ncloud/LoadBalancerNode.ts +++ b/apps/client/src/models/ncloud/LoadBalancer.ts @@ -15,7 +15,7 @@ export const LoadBalancerNode: Node & { type: 'load-balancer', size: { '2d': { width: 90, height: 90 }, - '3d': { width: 97, height: 94, offset: 10 }, + '3d': { width: 97, height: 94, depth: 38, offset: 10 }, }, properties: { ...Networks, diff --git a/apps/client/src/models/ncloud/MySQLDB.ts b/apps/client/src/models/ncloud/MySQLDB.ts index 751f2c3d..364b49e9 100644 --- a/apps/client/src/models/ncloud/MySQLDB.ts +++ b/apps/client/src/models/ncloud/MySQLDB.ts @@ -11,7 +11,7 @@ export const MySQLDBNode: Node = { type: 'db-mysql', size: { '2d': { width: 90, height: 90 }, - '3d': { width: 128, height: 137.5 }, + '3d': { width: 128, height: 137.5, depth: 82, offset: 0 }, }, properties: { ...Networks, diff --git a/apps/client/src/models/ncloud/NatGateway.ts b/apps/client/src/models/ncloud/NatGateway.ts new file mode 100644 index 00000000..cdcea61f --- /dev/null +++ b/apps/client/src/models/ncloud/NatGateway.ts @@ -0,0 +1,21 @@ +import { GraphNode } from '@helpers/node'; +import { Node } from '@types'; +import { Networks, NetworksProp } from './Networks'; + +export interface NatGatewayProp extends NetworksProp { + //TODO: +} + +export const NatGatewayNode: Node = { + ...GraphNode, + type: 'nat-gateway', + size: { + '2d': { width: 90, height: 90 }, + '3d': { width: 123, height: 108.05, depth: 74, offset: 0 }, + }, + properties: { + ...Networks, + }, +}; + +export const NatGatewayRequiredFields = {}; diff --git a/apps/client/src/models/ncloud/ObjectStorage.tsx b/apps/client/src/models/ncloud/ObjectStorage.ts similarity index 85% rename from apps/client/src/models/ncloud/ObjectStorage.tsx rename to apps/client/src/models/ncloud/ObjectStorage.ts index 9a034495..5d659bba 100644 --- a/apps/client/src/models/ncloud/ObjectStorage.tsx +++ b/apps/client/src/models/ncloud/ObjectStorage.ts @@ -13,7 +13,7 @@ export const ObjectStorageNode: Node & { type: 'object-storage', size: { '2d': { width: 90, height: 90 }, - '3d': { width: 100.626, height: 115.695, offset: 20 }, + '3d': { width: 100.626, height: 115.695, depth: 58, offset: 20 }, }, properties: { ...Networks, diff --git a/apps/client/src/models/ncloud/Server.ts b/apps/client/src/models/ncloud/Server.ts index 453d6c5f..86cc1e82 100644 --- a/apps/client/src/models/ncloud/Server.ts +++ b/apps/client/src/models/ncloud/Server.ts @@ -15,7 +15,7 @@ export const ServerNode: Node & { type: 'server', size: { '2d': { width: 90, height: 90 }, - '3d': { width: 128, height: 111 }, + '3d': { width: 128, height: 111, depth: 37, offset: 0 }, }, properties: { ...Networks, diff --git a/apps/client/src/models/ncloud/index.ts b/apps/client/src/models/ncloud/index.ts index 338fb4b0..d8674af7 100644 --- a/apps/client/src/models/ncloud/index.ts +++ b/apps/client/src/models/ncloud/index.ts @@ -1,10 +1,11 @@ import { CloudFunctionNode } from './CloudFunction'; -import { LoadBalancerNode } from './LoadBalancerNode'; +import { ContainerRegistryNode } from './ContainerRegistry'; +import { LoadBalancerNode } from './LoadBalancer'; import { MySQLDBNode } from './MySQLDB'; +import { NatGatewayNode } from './NatGateway'; import { RegionGroup, SubnetGroup, VpcGroup } from './Networks'; -import { ServerNode } from './Server'; -import { ContainerRegistryNode } from './ContainerRegistry'; import { ObjectStorageNode } from './ObjectStorage'; +import { ServerNode } from './Server'; export const NcloudNodeFactory = (type: string) => { switch (type) { @@ -20,6 +21,8 @@ export const NcloudNodeFactory = (type: string) => { return ContainerRegistryNode; case 'object-storage': return ObjectStorageNode; + case 'nat-gateway': + return NatGatewayNode; default: { throw new Error(`Unknown type: ${type}`); } diff --git a/apps/client/src/types/index.ts b/apps/client/src/types/index.ts index 939ee9a8..e9b927c7 100644 --- a/apps/client/src/types/index.ts +++ b/apps/client/src/types/index.ts @@ -7,10 +7,13 @@ export type GridPoint = { col: number; row: number }; export type Size = { width: number; height: number; - offset?: number; - depth?: number; }; +export type Size3D = { + depth: number; + offset: number; +} & Size; + export type ViewBox = Point & Size; export type Bounds = Point & Size; @@ -21,7 +24,7 @@ export type Node = { point: Point; size: { '2d': Size; - '3d': Size; + '3d': Size3D; }; properties: { [id: string]: any }; connectors: { [key: string]: Point }; diff --git a/apps/client/src/utils/index.ts b/apps/client/src/utils/index.ts index e0a1638e..ceb8983a 100644 --- a/apps/client/src/utils/index.ts +++ b/apps/client/src/utils/index.ts @@ -3,7 +3,16 @@ import { GRID_3D_HEIGHT_SIZE, GRID_3D_WIDTH_SIZE, } from '@constants'; -import { ConnectorMap, Dimension, GridPoint, Node, Point } from '@types'; +import { + Bounds, + ConnectorMap, + Dimension, + GridPoint, + Node, + Point, + Size, + Size3D, +} from '@types'; export const getDistance = (point1: Point, point2: Point) => { return Math.sqrt((point1.x - point2.x) ** 2 + (point1.y - point2.y) ** 2); @@ -81,7 +90,8 @@ export const convert3dTo2dPoint = (point: Point) => { }; export const convert2dTo3dPoint = (point: Point) => { - return gridToScreen3d(screenToGrid2d(point)); + const { col, row } = screenToGrid2d(point); + return gridToScreen3d({ col: col + 1, row }); }; export const generateRandomRGB = () => { @@ -91,31 +101,90 @@ export const generateRandomRGB = () => { return `rgb(${r},${g},${b})`; }; +export const get3DBasePoint = (point: Point, size: Size3D) => { + const grid = screenToGrid3d({ + x: point.x, + y: point.y, + }); + + const ratio = size.width / 128; + const base = gridToScreen3d({ + col: grid.col + (ratio > 1 ? 1 : ratio), + row: grid.row, + }); + + return { + x: base.x, + y: point.y + size.height + (size.offset ?? 0) - 74, + }; +}; + +const calcConnectorFor3D = (node: Node) => { + const point = node.point; + const nodeSize = node.size['3d'] as Size3D; + const base = get3DBasePoint(point, nodeSize); + + const baseBottom = { + x: base.x, + y: base.y - nodeSize.offset - nodeSize.depth + 74, + }; + + const center = { + x: base.x, + y: point.y + (baseBottom.y - point.y) / 2, + }; + + const _ratio = nodeSize.width / 128; + const ratio = _ratio < 1 ? 1 : _ratio; + + const GRID_WIDTH_QUARTER_SIZE = GRID_3D_WIDTH_SIZE / 4; + const GRID_HEIGHT_QUARTER_SIZE = GRID_3D_HEIGHT_SIZE / 4; + const top = { + x: center.x + GRID_WIDTH_QUARTER_SIZE * ratio, + y: center.y - GRID_HEIGHT_QUARTER_SIZE * ratio, + }; + + const left = { + x: center.x - GRID_WIDTH_QUARTER_SIZE * ratio, + y: center.y - GRID_HEIGHT_QUARTER_SIZE * ratio, + }; + + const right = { + x: center.x + GRID_WIDTH_QUARTER_SIZE * ratio, + y: center.y + GRID_HEIGHT_QUARTER_SIZE * ratio, + }; + + const bottom = { + x: center.x - GRID_WIDTH_QUARTER_SIZE * ratio, + y: center.y + GRID_HEIGHT_QUARTER_SIZE * ratio, + }; + + return { + top, + right, + left, + bottom, + }; +}; + export const getConnectorPoints = ( node: Node, dimension: Dimension, -): Omit => { +): ConnectorMap => { const point = node.point; - const { width, height } = node.size[dimension]; - const depth = GRID_3D_HEIGHT_SIZE / 2; - return { - top: { x: point.x + width / 2, y: point.y }, - right: - dimension === '2d' - ? { x: point.x + width, y: point.y + height / 2 } - : { - x: point.x + width, - y: point.y + (height - depth) / 2, - }, - left: - dimension === '2d' - ? { x: point.x, y: point.y + height / 2 } - : { - x: point.x, - y: point.y + (height - depth) / 2, - }, - bottom: { x: point.x + width / 2, y: point.y + height }, - }; + const nodeSize = node.size[dimension]; + const { width, height } = nodeSize; + + if (dimension === '2d') { + return { + top: { x: point.x + width / 2, y: point.y }, + right: { x: point.x + width, y: point.y + height / 2 }, + left: { x: point.x, y: point.y + height / 2 }, + bottom: { x: point.x + width / 2, y: point.y + height }, + }; + } + + return calcConnectorFor3D(node) as any; }; //INFO: 선분과 내적/외적 사이의 최단 거리를 계산(For Bend Point) @@ -169,3 +238,13 @@ export const findKeyByValue = ( ) => { return Object.keys(list).find((key) => list[key] === value); }; + +export const calcIsoMatrixPoint = (point: Point) => { + const isoMatrix = new DOMMatrix() + .rotate(30) + .skewX(-30) + .scale(1, 0.8602) + .translate(point.x, point.y); + + return isoMatrix; // 결과 행렬 반환 +};