Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions src/components/Factory/Canvas/Edges/ConnectionLine.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import {
BaseEdge,
type ConnectionLineComponent,
type ConnectionLineComponentProps,
getBezierPath,
type Handle,
type Node,
} from "@xyflow/react";

import { extractResource } from "../../utils/string";

export const ConnectionLine: ConnectionLineComponent = ({
fromX,
fromY,
fromPosition,
toX,
toY,
toPosition,
connectionStatus,
fromHandle,
toHandle,
fromNode,
toNode,
}: ConnectionLineComponentProps) => {
const [path] = getBezierPath({
sourceX: fromX,
sourceY: fromY,
sourcePosition: fromPosition,
targetX: toX,
targetY: toY,
targetPosition: toPosition,
});

let color;

switch (connectionStatus) {
case "valid":
color = "green";
break;
case "invalid":
color = "red";
break;
default:
color = "gray";
}

if (
toNode &&
toHandle &&
isInvalidConnection({ fromNode, toNode, fromHandle, toHandle })
) {
color = "red";
}

const id = `connection-${fromX}-${fromY}-${toX}-${toY}`;

return (
<BaseEdge
id={id}
path={path}
style={{
stroke: color,
strokeWidth: 2,
}}
/>
);
};

const isInvalidConnection = ({
fromNode,
toNode,
fromHandle,
toHandle,
}: {
fromNode: Node;
toNode: Node;
fromHandle: Handle;
toHandle: Handle;
}) => {
const fromResource = extractResource(fromHandle.id);
const toResource = extractResource(toHandle?.id);

if (!fromResource) {
return true;
}

if (fromResource === "any" || toResource === "any") {
return false;
}

if (fromResource !== toResource) {
return true;
}

if (fromNode.id === toNode?.id) {
return true;
}

return false;
};
97 changes: 27 additions & 70 deletions src/components/Factory/Canvas/GameCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import { useEffect, useState } from "react";

import { BlockStack } from "@/components/ui/layout";

import { RESOURCES } from "../data/resources";
import { setup } from "../data/setup";
import type { Building } from "../types/buildings";
import { isResourceType } from "../types/resources";
import { extractResource } from "../utils/string";
import { createOnConnect } from "./callbacks/onConnect";
import { createOnDrop } from "./callbacks/onDrop";
import { ConnectionLine } from "./Edges/ConnectionLine";
import ResourceEdge from "./Edges/ResourceEdge";
import BuildingNode from "./Nodes/Building";

Expand All @@ -31,8 +32,6 @@ const edgeTypes: Record<string, ComponentType<any>> = {
resourceEdge: ResourceEdge,
};

let nodeIdCounter = 0;

const GameCanvas = ({ children, ...rest }: ReactFlowProps) => {
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([]);
Expand All @@ -48,34 +47,32 @@ const GameCanvas = ({ children, ...rest }: ReactFlowProps) => {
instance.fitView({ maxZoom: 1, padding: 0.2 });
};

const onConnect = (connection: Connection) => {
if (connection.source === connection.target) return;
const onConnect = createOnConnect(setEdges);
const onDrop = createOnDrop(reactFlowInstance, setNodes);

const sourceResource = connection.sourceHandle?.split("-").pop();
const targetResource = connection.targetHandle?.split("-").pop();
const isValidConnection = (connection: Connection | Edge) => {
if (connection.source === connection.target) return false;

const resourceName = sourceResource ?? targetResource;
const sourceResource = extractResource(connection.sourceHandle);
const targetResource = extractResource(connection.targetHandle);

if (!isResourceType(resourceName)) {
console.error("Invalid resource type:", resourceName);
return;
if (
sourceResource !== "any" &&
targetResource !== "any" &&
sourceResource !== targetResource
) {
return false;
}

const newEdge: Edge = {
...connection,
id: `${connection.source}-${connection.sourceHandle}-${connection.target}-${connection.targetHandle}`,
type: "resourceEdge",
data: { ...RESOURCES[resourceName] },
animated: true,
};

setEdges((eds) => [
...eds,
{
...connection,
...newEdge,
} as Edge,
]);
const hasExistingConnection = edges.some(
(edge) =>
(edge.source === connection.source &&
edge.sourceHandle === connection.sourceHandle) ||
(edge.target === connection.target &&
edge.targetHandle === connection.targetHandle),
);

return !hasExistingConnection;
};

const onNodesDelete = (deleted: Node[]) => {
Expand All @@ -91,48 +88,6 @@ const GameCanvas = ({ children, ...rest }: ReactFlowProps) => {
event.dataTransfer.dropEffect = "move";
};

const onDrop = (event: DragEvent<HTMLDivElement>) => {
event.preventDefault();

if (!reactFlowInstance) return;

const buildingData = event.dataTransfer.getData("application/reactflow");
if (!buildingData) return;

try {
const { building } = JSON.parse(buildingData) as { building: Building };

const position = reactFlowInstance.screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});

const offsetData = event.dataTransfer.getData("DragStart.offset");
if (offsetData) {
const { offsetX, offsetY } = JSON.parse(offsetData);
position.x -= offsetX;
position.y -= offsetY;
}

const newNode: Node = {
id: `${building.id}-${nodeIdCounter++}`,
type: "building",
position,
data: {
...building,
label: building.name,
},
draggable: true,
deletable: true,
selectable: true,
};

setNodes((nds) => [...nds, newNode]);
} catch (error) {
console.error("Failed to drop building:", error);
}
};

return (
<BlockStack fill className="relative">
<ReactFlow
Expand All @@ -151,6 +106,8 @@ const GameCanvas = ({ children, ...rest }: ReactFlowProps) => {
edgeTypes={edgeTypes}
minZoom={0.1}
maxZoom={2}
connectionLineComponent={ConnectionLine}
isValidConnection={isValidConnection}
deleteKeyCode={["Delete", "Backspace"]}
proOptions={{ hideAttribution: true }}
fitView
Expand Down
14 changes: 13 additions & 1 deletion src/components/Factory/Canvas/Handles/BuildingInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,30 @@ import {
type BuildingInput as BuildingInputConfig,
} from "../../types/buildings";
import { isLightColor } from "../../utils/color";
import { layoutHandleAtPosition } from "./utils";

const BuildingInput = ({
building,
input,
selected,
index,
groupIndex,
totalInGroup,
}: {
building: Building;
input: BuildingInputConfig;
selected?: boolean;
index: number;
groupIndex: number;
totalInGroup: number;
}) => {
const { resource, position } = input;

return (
<Handle
type="target"
position={position}
id={`input-${building.id}-${position}-${resource}`}
id={`input-building:${building.id}-resource:${resource}-${index}`}
className={cn(selected && "border-selected!")}
style={{
background: RESOURCE_COLORS[resource],
Expand All @@ -41,6 +48,11 @@ const BuildingInput = ({
alignItems: "center",
justifyContent: "center",
display: "flex",
...layoutHandleAtPosition({
position,
groupIndex,
totalInGroup,
}),
}}
>
<TooltipProvider>
Expand Down
14 changes: 13 additions & 1 deletion src/components/Factory/Canvas/Handles/BuildingOutput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,23 +15,30 @@ import {
type BuildingOutput as BuildingOutputConfig,
} from "../../types/buildings";
import { isLightColor } from "../../utils/color";
import { layoutHandleAtPosition } from "./utils";

const BuildingOutput = ({
building,
output,
selected,
index,
groupIndex,
totalInGroup,
}: {
building: Building;
output: BuildingOutputConfig;
selected?: boolean;
index: number;
groupIndex: number;
totalInGroup: number;
}) => {
const { resource, position } = output;

return (
<Handle
type="source"
position={position}
id={`output-${building.id}-${resource}`}
id={`output-building:${building.id}-resource:${resource}-${index}`}
className={cn(selected && "border-selected!")}
style={{
background: RESOURCE_COLORS[resource],
Expand All @@ -41,6 +48,11 @@ const BuildingOutput = ({
alignItems: "center",
justifyContent: "center",
display: "flex",
...layoutHandleAtPosition({
position,
groupIndex,
totalInGroup,
}),
}}
>
<TooltipProvider>
Expand Down
29 changes: 29 additions & 0 deletions src/components/Factory/Canvas/Handles/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Position } from "@xyflow/react";

export const layoutHandleAtPosition = ({
position,
groupIndex,
totalInGroup,
}: {
position: Position;
groupIndex: number;
totalInGroup: number;
}) => {
if (totalInGroup === 1) {
return {};
}

const spacing = 100 / (totalInGroup + 1);
const offset = spacing * (groupIndex + 1);

switch (position) {
case Position.Top:
case Position.Bottom:
return { left: `${offset}%` };
case Position.Left:
case Position.Right:
return { top: `${offset}%` };
default:
return {};
}
};
Loading
Loading