diff --git a/components/shared/ProducingGraph.module.css b/components/shared/ProducingGraph.module.css deleted file mode 100644 index 59ee7f6..0000000 --- a/components/shared/ProducingGraph.module.css +++ /dev/null @@ -1,32 +0,0 @@ -.plane { - position: relative; - overflow: scroll; - overflow-scrolling: touch; - background-color: lightsalmon; - padding: 2em; - height: 80vh; -} - -.tiny { - font-size: 0.5em; - margin-top: -1.6em; -} - -.small { - font-size: 0.8em; -} - -.node { - display: inline-block; - position: absolute; - width: 200px; - height: fit-content; - padding: 0.2em; - background-color: red; -} - -.linkOut { - position: absolute; - top: 0.5em; - right: 0.5em; -} diff --git a/components/shared/ProducingGraph.tsx b/components/shared/ProducingGraph.tsx deleted file mode 100644 index b13acd0..0000000 --- a/components/shared/ProducingGraph.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import {FC, useMemo} from "react"; -import {EnrichedEntity, Recipe} from "../../src/types"; -import styles from './ProducingGraph.module.css' -import {EntityIcon} from "../home/EntityIcon/EntityIcon"; -import {sortByProperty} from "../../src/utils"; -import Link from "next/link"; -import {RecipeSpan} from "../home/Recipe/Recipe"; - -export interface ProducingNode { - inputs: string[] - outputs: string[] - name: string - icons?: (EnrichedEntity|string)[] - linkOut?: string - recipe?: Recipe -} - -interface Props { - nodes: ProducingNode[] - inputs: string[] -} - -export const ProducingGraph: FC = ({nodes, inputs}) => { - const rows: ProducingNode[][] = useMemo(() => { - const available = new Set(inputs) - let todo = [...nodes] - const result: ProducingNode[][] = [] - while (todo.length) { - const amount = todo.length - const thisRow: string[] = [] - result.push([]) - - todo = todo.filter((node) => { - if (node.inputs.every(input => available.has(input))) { - result[result.length - 1].push(node) - thisRow.push(...node.outputs) - return false - } - return true - }) - thisRow.map(uid => available.add(uid)) - result[result.length - 1].sort(sortByProperty(val => -val.outputs.length * 1000 + -val.inputs.length)) - - if (amount === todo.length) { - console.warn("Loop detected! Left over:", todo) - result.pop() - break - } - } - return result - }, [inputs, nodes]) - - return
- {inputs.map((input, idx) => )} - {rows.map((row, colIdx) => row.map((node, idx) => ( -
- { node.linkOut && 🔗 } -

{node.name}

- { node.icons?.length ?
- {node.icons.map((input) => )} -
: null } - { - node.recipe - ? - : <> -

Inputs

-
- {node.inputs.map((input) => )} -
- {node.outputs.length ? <> -

Outputs

-
- {node.outputs.map((input) => )} -
- : null} - - } -
- )))} -
-} diff --git a/components/shared/ProducingGraph/ProducingGraph.module.css b/components/shared/ProducingGraph/ProducingGraph.module.css new file mode 100644 index 0000000..5baa89c --- /dev/null +++ b/components/shared/ProducingGraph/ProducingGraph.module.css @@ -0,0 +1,20 @@ +.plane { + border: 1px solid red; + padding: 2em; + width: fit-content; + display: flex; + flex-direction: column; + gap: 5em; +} + +.row { + display: flex; + gap: 2em; +} + +.node { + padding: 0.5em; + border-radius: 4px; + border: 1px solid #DDDDDD; + background-color: #EEE; +} diff --git a/components/shared/ProducingGraph/ProducingGraph.tsx b/components/shared/ProducingGraph/ProducingGraph.tsx new file mode 100644 index 0000000..b4e1604 --- /dev/null +++ b/components/shared/ProducingGraph/ProducingGraph.tsx @@ -0,0 +1,71 @@ +import {FC, HTMLProps, PropsWithChildren, useMemo} from "react"; +import styles from './ProducingGraph.module.css' +import {EntityIcon} from "../../home/EntityIcon/EntityIcon"; +import {sortByProperty} from "../../../src/utils"; +import {EnrichedEntity, Recipe} from "../../../src/types"; + +interface GraphNodeBase { + inputs: string[] + outputs: string[] + name: string +} + +export type GraphNode> = GraphNodeBase & T + +interface Props { + nodes: GraphNode[] + inputs: string[] + outputs?: string[] + childType: FC & {node: GraphNode}> +} + +export const ProducingGraph = ,>({nodes, inputs, outputs, childType: ChildType}: PropsWithChildren>) => { + const rows: GraphNode[][] = useMemo(() => { + const available = new Set(inputs) + let todo = [...nodes] + const result: GraphNode[][] = [] + while (todo.length) { + const amount = todo.length + const thisRow: string[] = [] + result.push([]) + + todo = todo.filter((node) => { + if (node.inputs.every(input => available.has(input))) { + result[result.length - 1].push(node) + thisRow.push(...node.outputs) + return false + } + return true + }) + thisRow.map(uid => available.add(uid)) + result[result.length - 1].sort(sortByProperty(val => -val.outputs.length * 1000 + -val.inputs.length)) + + if (amount === todo.length) { + console.warn("Loop detected! Left over:", todo) + result.pop() + break + } + } + return result + }, [inputs, nodes]) + + return
+
+ {inputs.map((input, idx) => )} +
+ {rows.map((row, colIdx) => { + return
+ { + row.map((node) => ) + } +
+ })} + {outputs ?
+ {outputs.map((input, idx) => )} +
: null } +
+} diff --git a/components/visualize/NodeDetails/NodeDetails.module.css b/components/visualize/NodeDetails/NodeDetails.module.css new file mode 100644 index 0000000..297334a --- /dev/null +++ b/components/visualize/NodeDetails/NodeDetails.module.css @@ -0,0 +1,5 @@ +.root { + height: fit-content; + width: fit-content; + position: relative; +} diff --git a/components/visualize/NodeDetails/NodeDetails.tsx b/components/visualize/NodeDetails/NodeDetails.tsx new file mode 100644 index 0000000..2cf4922 --- /dev/null +++ b/components/visualize/NodeDetails/NodeDetails.tsx @@ -0,0 +1,21 @@ +import {FC, HTMLProps} from "react"; +import {GraphNode} from "../../shared/ProducingGraph/ProducingGraph"; +import {Recipe} from "../../../src/types"; +import cx from "classnames"; +import styles from "./NodeDetails.module.css"; +import {RecipeSpan} from "../../home/Recipe/Recipe"; + +export type DetailGraphNode = GraphNode<{ + recipes: Recipe[] +}> + +interface Props extends HTMLProps { + node: DetailGraphNode +} + +export const NodeDetails: FC = ({node, className, ...props}) => { + return
+

{node.name}

+ {node.recipes.map((recipe, idx) => )} +
+} diff --git a/components/visualize/NodeOverview/NodeOverview.module.css b/components/visualize/NodeOverview/NodeOverview.module.css new file mode 100644 index 0000000..ad1f1ab --- /dev/null +++ b/components/visualize/NodeOverview/NodeOverview.module.css @@ -0,0 +1,18 @@ +.root { + height: fit-content; + width: fit-content; +} + +.tiny { + font-size: 0.5em; + margin-top: -1.6em; + } + +.small { + font-size: 0.8em; +} + +.linkOut { + margin-inline-end: 0.5em; +} + diff --git a/components/visualize/NodeOverview/NodeOverview.tsx b/components/visualize/NodeOverview/NodeOverview.tsx new file mode 100644 index 0000000..c408ece --- /dev/null +++ b/components/visualize/NodeOverview/NodeOverview.tsx @@ -0,0 +1,36 @@ +import {FC, HTMLProps} from "react"; +import {GraphNode} from "../../shared/ProducingGraph/ProducingGraph"; +import {EnrichedEntity, Recipe} from "../../../src/types"; +import cx from "classnames"; +import styles from "./NodeOverview.module.css"; +import {RecipeSpan} from "../../home/Recipe/Recipe"; +import Link from "next/link"; +import {EntityIcon} from "../../home/EntityIcon/EntityIcon"; + +export type OverviewGraphNode = GraphNode<{ + icons: (EnrichedEntity|string)[] + linkOut: string +}> + +interface Props extends HTMLProps { + node: OverviewGraphNode +} + +export const NodeOverview: FC = ({node, className, ...props}) => { + return
+

🔗{node.name}

+ { node.icons?.length ?
+ {node.icons.map((input) => )} +
: null } +

Inputs

+
+ {node.inputs.map((input) => )} +
+ {node.outputs.length ? <> +

Outputs

+
+ {node.outputs.map((input) => )} +
+ : null} +
+} diff --git a/pages/_app.tsx b/pages/_app.tsx index 6c8d204..0609ff4 100644 --- a/pages/_app.tsx +++ b/pages/_app.tsx @@ -1,8 +1,9 @@ import '../styles/globals.css' import type { AppProps } from 'next/app' import {GroupProvider} from "../components/contexts/GroupProvider"; +import {FC} from "react"; -function MyApp({ Component, pageProps }: AppProps) { +const MyApp: FC = ({ Component, pageProps }) => { return diff --git a/pages/_document.tsx b/pages/_document.tsx new file mode 100644 index 0000000..fb65320 --- /dev/null +++ b/pages/_document.tsx @@ -0,0 +1,17 @@ +import {Html, Main, NextScript, DocumentProps, Head} from 'next/document'; +import {FC} from "react"; + +const MyDocument: FC = ({ __NEXT_DATA__ }) => { + const pageProps: Record|undefined = __NEXT_DATA__?.props?.pageProps; + return ( + + + +
+ + + + ); +} + +export default MyDocument diff --git a/pages/index.tsx b/pages/index.tsx index 674bddb..4f1c27c 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,17 +1,22 @@ import type { NextPage } from 'next' import Head from 'next/head' import {Home} from "../components/home/Home"; +import {useEffect} from "react"; const Page: NextPage = () => { + useEffect(() => + { + document.body.classList.add("scroll"); + }); return ( -
+ <> Factorio Microservices -
+ ) } diff --git a/pages/visualize/[name].tsx b/pages/visualize/[name].tsx index 24ae34d..1fccb9b 100644 --- a/pages/visualize/[name].tsx +++ b/pages/visualize/[name].tsx @@ -2,12 +2,15 @@ import type { NextPage } from 'next' import Head from 'next/head' import {useGroups} from "../../components/contexts/GroupProvider"; import {useFactories} from "../../src/hooks/useFactories"; -import {ProducingGraph, ProducingNode} from "../../components/shared/ProducingGraph"; +import {GraphNode, ProducingGraph} from "../../components/shared/ProducingGraph/ProducingGraph"; import {useMemo} from "react"; import {calculateInputs} from "../../src/calculateInputs"; import {useRouter} from "next/router"; -import {isNonNullable} from "../../src/utils"; -import {EnrichedEntity} from "../../src/types"; +import {groupBy, isNonNullable} from "../../src/utils"; +import {EnrichedEntity, Recipe} from "../../src/types"; +import {DetailGraphNode, NodeDetails} from "../../components/visualize/NodeDetails/NodeDetails"; + + const Page: NextPage = () => { const {query: {name}} = useRouter() @@ -34,21 +37,29 @@ const Page: NextPage = () => { ) }, [baseFactories, exportedFactories, findFactory, group, ignoredFactories]) - const producingNodes: ProducingNode[] = useMemo(() => { + const producingNodes: DetailGraphNode[] = useMemo(() => { if (!group) return [] - return Array.from(new Set([...intermediateFactories, ...group.exports, ...group.malls])) + const nodes = Array.from(new Set([...intermediateFactories, ...group.exports, ...group.malls])) .map(findFactory) .filter(isNonNullable) .map((factory: EnrichedEntity) => ({ inputs: Object.keys(factory.recipe?.prerequisites ?? {}).sort((a, b) => a.localeCompare(b)), outputs: Object.keys(factory.recipe?.output ?? {}).sort((a, b) => a.localeCompare(b)), name: factory.name, - recipe: factory.recipe - })) + recipes: [factory.recipe] + } as DetailGraphNode)) + return Object + .values(groupBy(nodes, node => node.inputs.join())) + .map(nodesOfInput => ({ + inputs: nodesOfInput[0].inputs, + outputs: Array.from(new Set(nodesOfInput.flatMap(node => node.outputs))), + name: nodesOfInput.map(node => node.name).join(', '), + recipes: nodesOfInput.flatMap(node => node.recipes) + } as DetailGraphNode)) }, [findFactory, group, intermediateFactories]) return ( -
+ <> Factorio Microservices @@ -56,10 +67,19 @@ const Page: NextPage = () => {

Factorio Microservices

- +
-
+ ) } +export async function getServerSideProps() { + return { props: { bodyClassName: 'scroll' } }; +} + export default Page diff --git a/pages/visualize/index.tsx b/pages/visualize/index.tsx index e0b2c3d..67f402c 100644 --- a/pages/visualize/index.tsx +++ b/pages/visualize/index.tsx @@ -2,9 +2,10 @@ import type { NextPage } from 'next' import Head from 'next/head' import {useGroups} from "../../components/contexts/GroupProvider"; import {useFactories} from "../../src/hooks/useFactories"; -import {ProducingGraph, ProducingNode} from "../../components/shared/ProducingGraph"; +import {GraphNode, ProducingGraph} from "../../components/shared/ProducingGraph/ProducingGraph"; import {useMemo} from "react"; import {calculateInputs} from "../../src/calculateInputs"; +import {NodeOverview, OverviewGraphNode} from "../../components/visualize/NodeOverview/NodeOverview"; const Page: NextPage = () => { const { @@ -17,7 +18,7 @@ const Page: NextPage = () => { findFactory } = useFactories() - const producingNodes: ProducingNode[] = useMemo(() => { + const producingNodes: OverviewGraphNode[] = useMemo(() => { return Object.values(groups).map(group => ({ inputs: calculateInputs( [...group.exports, ...group.malls], @@ -34,7 +35,7 @@ const Page: NextPage = () => { }, [baseFactories, exportedFactories, findFactory, groups, ignoredFactories]) return ( -
+ <> Factorio Microservices @@ -42,12 +43,16 @@ const Page: NextPage = () => {

Factorio Microservices

- +
-
+ ) } +export async function getStaticProps() { + return { props: { bodyClassName: 'scroll' } }; +} + function fixedEncodeURIComponent(str: string): string { return encodeURIComponent(str).replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16)); } diff --git a/res/manual.json b/res/manual.json new file mode 100644 index 0000000..7cc7f3c --- /dev/null +++ b/res/manual.json @@ -0,0 +1,75 @@ +[ + { + "name": "Empty barrel", + "href": "/Empty_barrel", + "image": "/images/thumb/Empty_barrel.png/32px-Empty_barrel.png", + "recipe": { + "prerequisites": { + "/Steel_plate": 1 + }, + "time": 1, + "output": { + "/Empty_barrel": 1 + } + } + }, + { + "name": "Light oil", + "href": "/Light_oil", + "image": "/images/thumb/Light_oil.png/32px-Light_oil.png", + "recipe": { + "prerequisites": { + "/Heavy_oil": 40, + "/Water": 30 + }, + "time": 2, + "output": { + "/Light_oil": 30 + } + } + }, + { + "name": "Heavy oil", + "href": "/Heavy_oil", + "image": "/images/thumb/Heavy_oil.png/32px-Heavy_oil.png", + "recipe": { + "prerequisites": { + "/Crude_oil": 100, + "/Water": 50 + }, + "time": 5, + "output": { + "/Heavy_oil": 30 + } + } + }, + { + "name": "Petroleum gas", + "href": "/Petroleum_gas", + "image": "/images/thumb/Petroleum_gas.png/32px-Petroleum_gas.png", + "recipe": { + "prerequisites": { + "/Light_oil": 30, + "/Water": 50 + }, + "time": 2, + "output": { + "/Petroleum_gas": 20 + } + } + }, + { + "name": "Solid fuel", + "href": "/Solid_fuel", + "image": "/images/thumb/Solid_fuel.png/32px-Solid_fuel.png", + "recipe": { + "prerequisites": { + "/Light_oil": 10 + }, + "time": 2, + "output": { + "/Solid_fuel": 1 + } + } + } +] diff --git a/src/hooks/useFactories.ts b/src/hooks/useFactories.ts index 3d5013a..b240497 100644 --- a/src/hooks/useFactories.ts +++ b/src/hooks/useFactories.ts @@ -1,8 +1,11 @@ import {EnrichedEntity, Entity} from "../types"; import details from "../../res/details.json"; +import manual from "../../res/manual.json"; -const factories = (details as Entity[]).map((detail: EnrichedEntity) => { - detail.usedBy = (details as Entity[]) +const joined = [...details, ...manual] as Entity[] + +const factories = joined.map((detail: EnrichedEntity) => { + detail.usedBy = joined .filter(f => Object .keys(f.recipe?.prerequisites ?? {}) .includes(detail.href) diff --git a/src/utils.ts b/src/utils.ts index 7931900..dccdf26 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,3 +1,5 @@ +import {FC, PropsWithChildren} from "react"; + export function isNonNullable(any: T): any is NonNullable { return any !== undefined && any !== null } @@ -10,3 +12,12 @@ export function sortByProperty(transform: (val: T) => number | string): (a: T return a2 === b2 ? 0 : -1 } } + +export function groupBy(arr: T[], transform: (val: T) => string): Record { + const result: Record = {} + for (const elem of arr) { + const key = transform(elem) + result[key] = [...(result[key] ?? []), elem] + } + return result +} diff --git a/styles/globals.css b/styles/globals.css index f6294c2..84e97b4 100644 --- a/styles/globals.css +++ b/styles/globals.css @@ -12,6 +12,11 @@ body { padding: 2em; } +body.scroll { + overflow: auto; + overflow-scrolling: touch; +} + a { color: inherit; text-decoration: none;