diff --git a/components/home/EntityIcon/EntityIcon.module.css b/components/home/EntityIcon/EntityIcon.module.css index a70ebfb..0b05bd5 100644 --- a/components/home/EntityIcon/EntityIcon.module.css +++ b/components/home/EntityIcon/EntityIcon.module.css @@ -22,6 +22,11 @@ margin-inline-end: 0.2em; } +.noAmount { + margin-inline: 0.1em; + margin-block: 0.1em -0.1em; +} + @media (prefers-color-scheme: dark) { .span { border-color: #111111; diff --git a/components/home/EntityIcon/EntityIcon.tsx b/components/home/EntityIcon/EntityIcon.tsx index 871302c..c0cbc49 100644 --- a/components/home/EntityIcon/EntityIcon.tsx +++ b/components/home/EntityIcon/EntityIcon.tsx @@ -2,28 +2,36 @@ import {FC, HTMLProps, useMemo} from "react" import {Entity} from "../../../src/types" import {useFactories} from "../../../src/hooks/useFactories" import styles from './EntityIcon.module.css' +import cx from "classnames"; interface Props extends Omit, 'value'> { value: Entity|string amount?: number } -export const EntityIcon: FC = ({value, amount, ...rest}) => { +export const EntityIcon: FC = ({className, value, amount, ...rest}) => { const {findFactory} = useFactories() const entity = useMemo(() => { return typeof value === "object" ? value - : findFactory(value) ?? { - href: value, - name: value, - image: value, - recipe: undefined - } + : value === '/Time' + ? { + href: '/Time', + name: 'Time', + image: '/images/Time.png', + recipe: undefined + } + : findFactory(value) ?? { + href: value, + name: value, + image: value, + recipe: undefined + } }, [findFactory, value]) - return + return {/* eslint-disable-next-line @next/next/no-img-element */} - {entity.name}/ + {entity.name}/ {amount !== undefined && {amount}} } diff --git a/components/home/Group/Group.module.css b/components/home/GroupBox/GroupBox.module.css similarity index 100% rename from components/home/Group/Group.module.css rename to components/home/GroupBox/GroupBox.module.css diff --git a/components/home/Group/Group.tsx b/components/home/GroupBox/GroupBox.tsx similarity index 81% rename from components/home/Group/Group.tsx rename to components/home/GroupBox/GroupBox.tsx index 6a04bb6..87fb513 100644 --- a/components/home/Group/Group.tsx +++ b/components/home/GroupBox/GroupBox.tsx @@ -1,10 +1,11 @@ -import {FC, memo, useCallback, useEffect, useMemo, useState} from "react"; +import {FC, memo, useCallback, useMemo, useState} from "react"; import {FactorySelect} from "../FactorySelect/FactorySelect"; import {useFactories} from "../../../src/hooks/useFactories"; -import {EnrichedEntity, Entity, Group} from "../../../src/types"; -import styles from "./Group.module.css" +import {EnrichedEntity, Group} from "../../../src/types"; +import styles from "./GroupBox.module.css" import {EntitySpan} from "../EntitySpan/EntitySpan"; import {useGroups} from "../../contexts/GroupProvider"; +import {calculateInputs} from "../../../src/calculateInputs"; interface Props { group: Group @@ -31,30 +32,15 @@ const GroupBoxBase: FC = ({ group }) => { const [isDeleteConfirm, setDeleteConfirm] = useState(false) - const [inputs, intermediates] = useMemo<[string[], string[]]>(() => { + const [inputs, intermediates] = useMemo(() => { const allProducingFactories = [...exports, ...malls] - const prducingSet = new Set(allProducingFactories) - const ignored = new Set(ignoredFactories) - const base = new Set(baseFactories) - const inputs = new Set() - const intermediates = new Set() - - let next: string|undefined - while (next = allProducingFactories.pop()) { - const pres = Object.keys(findFactory(next)?.recipe?.prerequisites ?? {}) - for (const pre of pres) { - if (exportedFactories.has(pre) || base.has(pre)) { - if (!prducingSet.has(pre)) inputs.add(pre) - } else if (!intermediates.has(pre)) { - if (!prducingSet.has(pre)) intermediates.add(pre) - allProducingFactories.push(pre) - } - } - } - return [ - Array.from(inputs).sort((a, b) => a.localeCompare(b)), - Array.from(intermediates).sort((a, b) => a.localeCompare(b)) - ] + return calculateInputs( + allProducingFactories, + ignoredFactories, + baseFactories, + exportedFactories, + findFactory + ) }, [exports, malls, ignoredFactories, baseFactories, findFactory, exportedFactories]) const [suggestionsExport, suggestionMall] = useMemo<[EnrichedEntity[], EnrichedEntity[]]>(() => { @@ -99,7 +85,7 @@ const GroupBoxBase: FC = ({ group }) => { onBlur={() => setDeleteConfirm(false)} onClick={() => !isDeleteConfirm ? setDeleteConfirm(true) : removeGroup(name)} style={{display: 'block'}} > - {isDeleteConfirm ? 'Delete Group?' : 'X'} + {isDeleteConfirm ? 'Delete GroupBox?' : 'X'}

Exported Factories

{ +export const Home: FC = () => { const {factories} = useFactories() const { groups, @@ -48,6 +48,7 @@ export const HomeComponent: FC = () => { if (inputRef.current) inputRef.current.value = null as unknown as string } }}/> + Visualize
Missing export factories diff --git a/components/home/Recipe/Recipe.tsx b/components/home/Recipe/Recipe.tsx index e9511e6..120ed91 100644 --- a/components/home/Recipe/Recipe.tsx +++ b/components/home/Recipe/Recipe.tsx @@ -8,6 +8,7 @@ interface Props { } export const RecipeSpan: FC = ({recipe}) => { + const toEntityIcon = ([key, amount]: [string, number]) => const joinByPlus = (elems: JSX.Element[]) => elems.reduce((acc, curr) => { if (acc.length) { return [...acc, '+', curr] @@ -15,9 +16,9 @@ export const RecipeSpan: FC = ({recipe}) => { return [curr] } }, [] as (JSX.Element|string)[]) - const before = Object.entries({...recipe.prerequisites}).map(([key, amount]) => ) - const after = Object.entries({...recipe.output}).map(([key, amount]) => ) + const before = Object.entries({...recipe.prerequisites}).map(toEntityIcon) + const after = Object.entries({...recipe.output}).map(toEntityIcon) return - {joinByPlus(before)} → {joinByPlus(after)} + {joinByPlus([toEntityIcon(['/Time', recipe.time]), ...before])} → {joinByPlus(after)} } diff --git a/components/shared/ProducingGraph.module.css b/components/shared/ProducingGraph.module.css new file mode 100644 index 0000000..59ee7f6 --- /dev/null +++ b/components/shared/ProducingGraph.module.css @@ -0,0 +1,32 @@ +.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 new file mode 100644 index 0000000..b13acd0 --- /dev/null +++ b/components/shared/ProducingGraph.tsx @@ -0,0 +1,85 @@ +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/next.config.js b/next.config.js index ae88795..e674786 100644 --- a/next.config.js +++ b/next.config.js @@ -1,7 +1,7 @@ /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: true, - swcMinify: true, + swcMinify: true } module.exports = nextConfig diff --git a/pages/index.tsx b/pages/index.tsx index 1ae52fd..674bddb 100644 --- a/pages/index.tsx +++ b/pages/index.tsx @@ -1,8 +1,8 @@ import type { NextPage } from 'next' import Head from 'next/head' -import {HomeComponent} from "../components/home/Home"; +import {Home} from "../components/home/Home"; -const Home: NextPage = () => { +const Page: NextPage = () => { return (
@@ -10,9 +10,9 @@ const Home: NextPage = () => { - +
) } -export default Home +export default Page diff --git a/pages/visualize/[name].tsx b/pages/visualize/[name].tsx new file mode 100644 index 0000000..24ae34d --- /dev/null +++ b/pages/visualize/[name].tsx @@ -0,0 +1,65 @@ +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 {useMemo} from "react"; +import {calculateInputs} from "../../src/calculateInputs"; +import {useRouter} from "next/router"; +import {isNonNullable} from "../../src/utils"; +import {EnrichedEntity} from "../../src/types"; + +const Page: NextPage = () => { + const {query: {name}} = useRouter() + const { + exportedFactories, + baseFactories, + ignoredFactories, + groups + } = useGroups() + const { + findFactory + } = useFactories() + + const group = typeof name === 'string' ? groups[name] : undefined + + const [inputFactories, intermediateFactories] = useMemo>(() => { + if (!group) return [[], []] + return calculateInputs( + [...group.exports, ...group.malls], + ignoredFactories, + baseFactories, + exportedFactories, + findFactory + ) + }, [baseFactories, exportedFactories, findFactory, group, ignoredFactories]) + + const producingNodes: ProducingNode[] = useMemo(() => { + if (!group) return [] + return 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 + })) + }, [findFactory, group, intermediateFactories]) + + return ( +
+ + Factorio Microservices + + + +
+

Factorio Microservices

+ +
+
+ ) +} + +export default Page diff --git a/pages/visualize/index.tsx b/pages/visualize/index.tsx new file mode 100644 index 0000000..e0b2c3d --- /dev/null +++ b/pages/visualize/index.tsx @@ -0,0 +1,56 @@ +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 {useMemo} from "react"; +import {calculateInputs} from "../../src/calculateInputs"; + +const Page: NextPage = () => { + const { + exportedFactories, + ignoredFactories, + baseFactories, + groups + } = useGroups() + const { + findFactory + } = useFactories() + + const producingNodes: ProducingNode[] = useMemo(() => { + return Object.values(groups).map(group => ({ + inputs: calculateInputs( + [...group.exports, ...group.malls], + ignoredFactories, + baseFactories, + exportedFactories, + findFactory + )[0], + outputs: group.exports, + name: group.name, + icons: [...group.exports, ...group.malls], + linkOut: `./visualize/${fixedEncodeURIComponent(group.name)}` + })) + }, [baseFactories, exportedFactories, findFactory, groups, ignoredFactories]) + + return ( +
+ + Factorio Microservices + + + +
+

Factorio Microservices

+ +
+
+ ) +} + +function fixedEncodeURIComponent(str: string): string { + return encodeURIComponent(str).replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16)); +} + + +export default Page diff --git a/src/calculateInputs.ts b/src/calculateInputs.ts new file mode 100644 index 0000000..104c85b --- /dev/null +++ b/src/calculateInputs.ts @@ -0,0 +1,26 @@ +import {EnrichedEntity} from "./types"; + +export const calculateInputs = (allProducingFactories: string[], ignoredFactories: string[], baseFactories: string[], exportedFactories: Set, findFactory: (uid: string) => EnrichedEntity|undefined) => { + const prducingSet = new Set(allProducingFactories) + const ignored = new Set(ignoredFactories) + const base = new Set(baseFactories) + const inputs = new Set() + const intermediates = new Set() + + let next: string|undefined + while (next = allProducingFactories.pop()) { + const pres = Object.keys(findFactory(next)?.recipe?.prerequisites ?? {}) + for (const pre of pres) { + if (exportedFactories.has(pre) || base.has(pre)) { + if (!prducingSet.has(pre)) inputs.add(pre) + } else if (!intermediates.has(pre)) { + if (!prducingSet.has(pre)) intermediates.add(pre) + allProducingFactories.push(pre) + } + } + } + return [ + Array.from(inputs).sort((a, b) => a.localeCompare(b)), + Array.from(intermediates).sort((a, b) => a.localeCompare(b)) + ] as const +} diff --git a/src/hooks/useLocalStorage.ts b/src/hooks/useLocalStorage.ts index 944f33f..bdef419 100644 --- a/src/hooks/useLocalStorage.ts +++ b/src/hooks/useLocalStorage.ts @@ -98,7 +98,7 @@ function parseJSON(value: string | null): T | undefined { try { return value === 'undefined' ? undefined : JSON.parse(value ?? '') } catch { - console.log('parsing error on', { value }) + console.error('parsing error on', { value }) return undefined } }