Skip to content

Commit

Permalink
feat: Node 이동 구현 (#162)
Browse files Browse the repository at this point in the history
* refactor: 노드 핸들러 타입 간소화

* feat: 이동 관련 핸들러 함수 구현

* feat: gooeyConnection이 선택적으로 표시될 수 있도록함

* feat: 이동 관련 핸들러 설정 및 컴포넌트 분리

* refactor: 분리된 jsx 이름 수정

* feat: 이동 로직과 기존 드래그 로직이 충돌하지 않도록 기능 추가

* feat: 이동 시 다른 노드와 겹치지 않게하는 기능 구현

* feat: 홀딩 애니메이션 구현

* feat: shadowColor 기본 색 지정

* refactor: 리뷰 반영

* fix: 충돌 및 빌드 에러 해결
  • Loading branch information
parkblo authored Nov 28, 2024
1 parent b6e586f commit d05bb82
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 77 deletions.
17 changes: 10 additions & 7 deletions packages/frontend/src/components/Node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { Circle, Group, KonvaNodeEvents, Text } from "react-konva";
import { useNavigate } from "react-router-dom";

import Konva from "konva";
import { KonvaEventObject } from "konva/lib/Node";
import { Vector2d } from "konva/lib/types";

type NodeProps = {
Expand All @@ -15,11 +14,8 @@ type NodeProps = {
KonvaNodeEvents;

type NodeHandlers = {
onDragStart: () => void;
onDragMove: (e: KonvaEventObject<DragEvent>) => void;
onDragEnd: () => void;
dragBoundFunc?: () => Vector2d;
};
} & KonvaNodeEvents;

export default function Node({
x,
Expand All @@ -38,10 +34,17 @@ export default function Node({
type NodeCircleProps = {
radius: number;
fill: string;
shadowColor?: string;
};

Node.Circle = function NodeCircle({ radius, fill }: NodeCircleProps) {
return <Circle x={0} y={0} radius={radius} fill={fill} />;
Node.Circle = function NodeCircle({
radius,
fill,
shadowColor = "#F9D46B",
}: NodeCircleProps) {
return (
<Circle x={0} y={0} radius={radius} fill={fill} shadowColor={shadowColor} />
);
};

type NodeTextProps = {
Expand Down
21 changes: 11 additions & 10 deletions packages/frontend/src/components/space/GooeyNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,25 @@ import GooeyConnection from "./GooeyConnection";
type GooeyNodeProps = {
startPosition: Vector2d;
dragPosition: Vector2d;
connectionVisible?: boolean;
color?: string;
};

export default function GooeyNode({
startPosition,
dragPosition,
connectionVisible = true,
color = "#FFF2CB",
}: GooeyNodeProps) {
return (
<>
<Circle
x={dragPosition.x}
y={dragPosition.y}
radius={64}
fill="#FFF2CB"
/>
<GooeyConnection
startPosition={startPosition}
endPosition={dragPosition}
/>
<Circle x={dragPosition.x} y={dragPosition.y} radius={64} fill={color} />
{connectionVisible && (
<GooeyConnection
startPosition={startPosition}
endPosition={dragPosition}
/>
)}
</>
);
}
164 changes: 108 additions & 56 deletions packages/frontend/src/components/space/SpaceView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Edge from "@/components/Edge";
import { HeadNode, NoteNode, SubspaceNode } from "@/components/Node";
import useAutofit from "@/hooks/useAutofit";
import useDragNode from "@/hooks/useDragNode";
import useMoveNode from "@/hooks/useMoveNode";
import useYjsSpace from "@/hooks/useYjsSpace";
import { useZoomSpace } from "@/hooks/useZoomSpace.ts";

Expand All @@ -32,10 +33,15 @@ export default function SpaceView({ spaceId, autofitTo }: SpaceViewProps) {
const stageRef = React.useRef<Konva.Stage>(null);
const { zoomSpace } = useZoomSpace({ stageRef });

const { nodes, edges, defineNode, defineEdge } = useYjsSpace();
const { nodes, edges, defineNode, defineEdge, updateNode } = useYjsSpace();

const nodesArray = nodes ? Object.values(nodes) : [];

const { move, moveState } = useMoveNode({
nodes: nodesArray,
spaceActions: { updateNode },
});

const { drag, dropPosition, handlePaletteSelect } = useDragNode(nodesArray, {
createNode: (type, parentNode, position, name = "New Note") => {
if (type === "note") {
Expand Down Expand Up @@ -95,7 +101,6 @@ export default function SpaceView({ spaceId, autofitTo }: SpaceViewProps) {
defineEdge(fromNode.id, toNode.id);
},
});
const { startNode, handlers } = drag;

const stageSize = useAutofit(autofitTo);

Expand All @@ -104,9 +109,9 @@ export default function SpaceView({ spaceId, autofitTo }: SpaceViewProps) {
<HeadNode
key={node.id}
name={node.name}
onDragStart={() => handlers.onDragStart(node)}
onDragMove={handlers.onDragMove}
onDragEnd={handlers.onDragEnd}
onDragStart={() => drag.handlers.onDragStart(node)}
onDragMove={drag.handlers.onDragMove}
onDragEnd={() => drag.handlers.onDragEnd()}
dragBoundFunc={dragBoundFunc}
/>
),
Expand All @@ -116,28 +121,110 @@ export default function SpaceView({ spaceId, autofitTo }: SpaceViewProps) {
x={node.x}
y={node.y}
name={node.name}
src={node.src}
onDragStart={() => handlers.onDragStart(node)}
onDragMove={handlers.onDragMove}
onDragEnd={handlers.onDragEnd}
src={node.src || ""}
onDragStart={() => drag.handlers.onDragStart(node)}
onDragMove={(e) => {
drag.handlers.onDragMove(e);
move.callbacks.monitorHoldingPosition(e);
}}
onDragEnd={(e) => {
drag.handlers.onDragEnd(moveState.isMoving);
move.callbacks.endMove(e);
}}
dragBoundFunc={dragBoundFunc}
onMouseDown={(e) => move.callbacks.startHold(node, e)}
onMouseUp={move.callbacks.endHold}
onTouchStart={(e) => move.callbacks.startHold(node, e)}
onTouchEnd={move.callbacks.endHold}
/>
),
subspace: (node: Node) => (
<SubspaceNode
key={node.id}
src={node.src}
x={node.x}
y={node.y}
name={node.name}
onDragStart={() => handlers.onDragStart(node)}
onDragMove={handlers.onDragMove}
onDragEnd={handlers.onDragEnd}
src={node.src || ""}
onDragStart={() => drag.handlers.onDragStart(node)}
onDragMove={(e) => {
drag.handlers.onDragMove(e);
move.callbacks.monitorHoldingPosition(e);
}}
onDragEnd={(e) => {
drag.handlers.onDragEnd(moveState.isMoving);
move.callbacks.endMove(e);
}}
dragBoundFunc={dragBoundFunc}
onMouseDown={(e) => move.callbacks.startHold(node, e)}
onMouseUp={move.callbacks.endHold}
onTouchStart={(e) => move.callbacks.startHold(node, e)}
onTouchEnd={move.callbacks.endHold}
/>
),
};

const gooeyNodeCreatingRenderer = drag.isActive &&
drag.position &&
drag.startNode && (
<GooeyNode
startPosition={{ x: drag.startNode.x, y: drag.startNode.y }}
dragPosition={drag.position}
/>
);

const gooeyNodeMovingRenderer = drag.position && (
<GooeyNode
startPosition={{ x: 0, y: 0 }}
dragPosition={drag.position}
connectionVisible={false}
color={moveState.isOverlapping ? "#ECE8E4" : "#FFF2CB"}
/>
);

const nearIndicatorRenderer = !moveState.isMoving &&
drag.position &&
drag.overlapNode && (
<MemoizedNearIndicator overlapNode={drag.overlapNode} />
);

const nodesRenderer =
nodes &&
Object.entries(nodes).map(([, node]) => {
const Component =
nodeComponents[node.type as keyof typeof nodeComponents];
return Component ? Component(node) : null;
});

const edgesRenderer =
edges &&
Object.entries(edges).map(([edgeId, edge]) => (
<Edge
key={edgeId || `${edge.from.id}-${edge.to.id}`}
from={edge.from}
to={edge.to}
nodes={nodes}
/>
));

const paletteRenderer = !moveState.isMoving && dropPosition && (
<Html>
<div
style={{
position: "absolute",
left: dropPosition.x,
top: dropPosition.y,
transform: "translate(-50%, -50%)",
pointerEvents: "auto",
}}
>
<PaletteMenu
items={["note", "image", "url", "subspace"]}
onSelect={handlePaletteSelect}
/>
</div>
</Html>
);

return (
<Stage
width={stageSize.width}
Expand All @@ -148,49 +235,14 @@ export default function SpaceView({ spaceId, autofitTo }: SpaceViewProps) {
onWheel={zoomSpace}
draggable
>
<Layer>
{drag.isActive && drag.position && startNode && (
<GooeyNode
startPosition={{ x: startNode.x, y: startNode.y }}
dragPosition={drag.position}
/>
)}
{drag.position && drag.overlapNode && (
<MemoizedNearIndicator overlapNode={drag.overlapNode} />
)}
{nodes &&
Object.entries(nodes).map(([, node]) => {
const Component =
nodeComponents[node.type as keyof typeof nodeComponents];
return Component ? Component(node) : null;
})}
{edges &&
Object.entries(edges).map(([edgeId, edge]) => (
<Edge
key={edgeId || `${edge.from.id}-${edge.to.id}`}
from={edge.from}
to={edge.to}
nodes={nodes}
/>
))}
{dropPosition && (
<Html>
<div
style={{
position: "absolute",
left: dropPosition.x,
top: dropPosition.y,
transform: "translate(-50%, -50%)",
pointerEvents: "auto",
}}
>
<PaletteMenu
items={["note", "image", "url", "subspace"]}
onSelect={handlePaletteSelect}
/>
</div>
</Html>
)}
<Layer offsetX={-stageSize.width / 2} offsetY={-stageSize.height / 2}>
{moveState.isMoving
? gooeyNodeMovingRenderer
: gooeyNodeCreatingRenderer}
{nearIndicatorRenderer}
{nodesRenderer}
{edgesRenderer}
{paletteRenderer}
</Layer>
<PointerLayer />
</Stage>
Expand Down
6 changes: 3 additions & 3 deletions packages/frontend/src/hooks/useDragNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,15 +60,15 @@ export default function useDragNode(nodes: Node[], spaceActions: spaceActions) {
}));
};

const handleDragEnd = () => {
const handleDragEnd = (isMoving: boolean = false) => {
const { startNode, dragPosition, overlapNode } = dragState;
if (!startNode || !dragPosition) return;

if (!overlapNode) {
if (!overlapNode && !isMoving) {
setDropPosition(dragPosition);
}

if (overlapNode && overlapNode.id !== startNode.id) {
if (overlapNode && overlapNode.id !== startNode.id && !isMoving) {
setDropPosition(null);
spaceActions.createEdge(startNode, overlapNode);
}
Expand Down
Loading

0 comments on commit d05bb82

Please sign in to comment.