From 9082dcdd2659c7ad67ab6af17c37811779353f41 Mon Sep 17 00:00:00 2001 From: Sebastian Seedorf Date: Mon, 15 Aug 2022 23:24:58 +0200 Subject: [PATCH] Implemented sorted graph --- components/contexts/GroupProvider.tsx | 7 +- .../home/EntityIcon/EntityIcon.module.css | 1 + components/home/GroupBox/GroupBox.tsx | 5 +- .../ProducingGraph/ProducingGraph.module.css | 10 +- .../shared/ProducingGraph/ProducingGraph.tsx | 161 +++++++---- package.json | 5 +- pages/_document.tsx | 3 +- pages/visualize/[name].tsx | 8 +- src/graph-untangle/index.ts | 252 ++++++++++++++++++ src/graph-untangle/types.ts | 20 ++ src/svg.ts | 15 ++ src/types.ts | 6 +- src/utils.ts | 18 +- tsconfig.json | 2 +- yarn.lock | 22 ++ 15 files changed, 464 insertions(+), 71 deletions(-) create mode 100644 src/graph-untangle/index.ts create mode 100644 src/graph-untangle/types.ts create mode 100644 src/svg.ts diff --git a/components/contexts/GroupProvider.tsx b/components/contexts/GroupProvider.tsx index b9fc34c..a2d5624 100644 --- a/components/contexts/GroupProvider.tsx +++ b/components/contexts/GroupProvider.tsx @@ -3,6 +3,7 @@ import {Group} from "../../src/types"; import {useLocalStorage} from "../../src/hooks/useLocalStorage"; import {ReactNodeLike} from "prop-types"; import pako from "pako"; +import Dict = NodeJS.Dict; interface Props { children: ReactNodeLike @@ -18,7 +19,7 @@ interface GroupContextType { baseFactories: string[] setBaseFactories(factories: string[]): void - groups: Record + groups: Dict addGroup(name: string, exported?: string[], malls?: string[]): void removeGroup(name: string): void renameGroup(name: string, newName: string): void @@ -56,7 +57,7 @@ const GroupContext = createContext(defaultValues); export const useGroups = () => useContext(GroupContext) interface StoredFile { - groups: Record, + groups: Dict, basicValues: string[], excludedSuggestions: string[] } @@ -64,7 +65,7 @@ interface StoredFile { export const GroupProvider: FC = ({children}) => { const [excludedSuggestions, setExcludedSuggestions] = useLocalStorage('excludedSuggestions', []) const [basicValues, setBasicValues] = useLocalStorage('basicValues', []) - const [groups, setGroups] = useLocalStorage>('serviceGroups', {}) + const [groups, setGroups] = useLocalStorage>('serviceGroups', {}) const doNotSuggest = useMemo>(() => { return new Set([...Object.values(groups).flatMap(group => [...group.exports, ...group.malls]), ...excludedSuggestions, ...basicValues]) diff --git a/components/home/EntityIcon/EntityIcon.module.css b/components/home/EntityIcon/EntityIcon.module.css index 0b05bd5..cf40e1b 100644 --- a/components/home/EntityIcon/EntityIcon.module.css +++ b/components/home/EntityIcon/EntityIcon.module.css @@ -4,6 +4,7 @@ border: 1px solid white; display: inline-block; position: relative; + height: fit-content; } .amount { diff --git a/components/home/GroupBox/GroupBox.tsx b/components/home/GroupBox/GroupBox.tsx index 87fb513..4214951 100644 --- a/components/home/GroupBox/GroupBox.tsx +++ b/components/home/GroupBox/GroupBox.tsx @@ -6,6 +6,7 @@ import styles from "./GroupBox.module.css" import {EntitySpan} from "../EntitySpan/EntitySpan"; import {useGroups} from "../../contexts/GroupProvider"; import {calculateInputs} from "../../../src/calculateInputs"; +import {uniquify} from "../../../src/utils"; interface Props { group: Group @@ -44,8 +45,8 @@ const GroupBoxBase: FC = ({ group }) => { }, [exports, malls, ignoredFactories, baseFactories, findFactory, exportedFactories]) const [suggestionsExport, suggestionMall] = useMemo<[EnrichedEntity[], EnrichedEntity[]]>(() => { - const selectedValues = Array.from(new Set([...exports, ...malls])) - const availableIngredients = Array.from(new Set([...selectedValues, ...intermediates, ...inputs])) + const selectedValues = uniquify([...exports, ...malls]) + const availableIngredients = uniquify([...selectedValues, ...intermediates, ...inputs]) return factories .filter(factory => { if (!factory.recipe) return false diff --git a/components/shared/ProducingGraph/ProducingGraph.module.css b/components/shared/ProducingGraph/ProducingGraph.module.css index 5baa89c..e70591f 100644 --- a/components/shared/ProducingGraph/ProducingGraph.module.css +++ b/components/shared/ProducingGraph/ProducingGraph.module.css @@ -1,15 +1,17 @@ .plane { - border: 1px solid red; + box-shadow: 0 0 0 1px red; /* Border left */ padding: 2em; width: fit-content; display: flex; flex-direction: column; - gap: 5em; + gap: 10em; + position: relative; } .row { display: flex; gap: 2em; + justify-content: space-evenly; } .node { @@ -18,3 +20,7 @@ border: 1px solid #DDDDDD; background-color: #EEE; } + +.hidden { + width: 0; +} diff --git a/components/shared/ProducingGraph/ProducingGraph.tsx b/components/shared/ProducingGraph/ProducingGraph.tsx index b4e1604..cc6d9b3 100644 --- a/components/shared/ProducingGraph/ProducingGraph.tsx +++ b/components/shared/ProducingGraph/ProducingGraph.tsx @@ -1,16 +1,10 @@ -import {FC, HTMLProps, PropsWithChildren, useMemo} from "react"; +import {FC, HTMLProps, PropsWithChildren, useEffect, useRef, useState} 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 +import {createPath, drawLine} from "../../../src/svg"; +import {AdditionalNode, GraphNode, GraphNodeWithIds, isAdditionalNode} from "../../../src/graph-untangle/types"; +import {graphUntangled} from "../../../src/graph-untangle"; +import {Dict} from "../../../src/types"; interface Props { nodes: GraphNode[] @@ -19,53 +13,116 @@ interface Props { 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([]) +export const ProducingGraph = ,>({nodes, inputs, outputs, childType: ChildType}: PropsWithChildren>) => { + const planeRef = useRef(null) + const [[rows, nodeMap], setGraph] = useState<[string[][], Dict>]>([[], {}]) + useEffect(() => { + setGraph(graphUntangled(nodes, inputs, outputs ?? [])) + setTimeout(() => setGraph(graphUntangled(nodes, inputs, outputs ?? [], 5000)), 0) + }, [inputs, nodes, outputs]) - 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 + /*const rowsWithInOut: (GraphNode|AdditionalNode)[][] = useMemo(() => { + const addedInputs = new Set() + const res: (GraphNode|AdditionalNode)[][] = deepcopy([[], ...rows, []]) + for (let i = 0; i < rows.length; i++) { + const rowOutputs = uniquify(rows[i].flatMap(row => row.outputs)) + for (let rowOutput of rowOutputs) { + outputs?.includes(rowOutput) && res[i+2].push({___type: 'o', value: rowOutput}) + } + const rowInputs = uniquify(rows[i].flatMap(row => row.inputs)) + for (let rowInput of rowInputs) { + if (addedInputs.has(rowInput)) + !res[i].some(node => isAdditionalNode(node) && node.value === rowInput) && res[i].push({___type: 'h', value: rowInput}) + else if (inputs.includes(rowInput)) + res[i].push({___type: 'i', value: rowInput}) + addedInputs.add(rowInput) } } - return result - }, [inputs, nodes]) + return res + }, [rows, inputs, outputs])*/ - return
-
- {inputs.map((input, idx) => )} -
- {rows.map((row, colIdx) => { - return
+ useEffect(() => { + if (!planeRef.current) return + const plane = planeRef.current + function createSvgElement() { + const elem = document.createElementNS("http://www.w3.org/2000/svg", "svg") + elem.id = 'arrows' + elem.setAttribute('style', 'position: absolute; inset: 0 0 0 0; pointer-events: none; z-index: -10;') + elem.setAttributeNS(null, 'stroke', 'black') + elem.setAttributeNS(null, 'fill', 'none') + plane.appendChild(elem) + return elem + } + const svg = document.getElementById('arrows') ?? createSvgElement() + const width = Math.round(plane.getBoundingClientRect().width) + const height = Math.round(plane.getBoundingClientRect().height) + svg.setAttribute('width', `${width}`) + svg.setAttribute('height', `${height}`) + svg.setAttributeNS(null, 'viewBox', `0 0 ${width} ${height}`) + svg.replaceChildren() + + plane.querySelectorAll('[data-outputs]').forEach(startNode => { + if (!(startNode instanceof HTMLElement)) return + (startNode.dataset.outputs?.split(' ') ?? []).forEach(output => { + const endNode = plane.querySelector(`[data-name="${output}"]`) + if (!(endNode instanceof HTMLElement)) return + const path = document.createElementNS('http://www.w3.org/2000/svg',"path") + path.setAttributeNS(null, 'd', createPath(plane.getBoundingClientRect(), startNode.getBoundingClientRect(), endNode.getBoundingClientRect())) + svg.appendChild(path) + }) + }) + plane.querySelectorAll('[data-hidden][data-outputs]').forEach(elem => { + if (!(elem instanceof HTMLElement)) return + const path = document.createElementNS('http://www.w3.org/2000/svg',"path") + path.setAttributeNS(null, 'd', drawLine(plane.getBoundingClientRect(), elem.getBoundingClientRect())) + svg.appendChild(path) + }) + }) + + return
+ {rows.map((row, colIdx) => ( +
{ - row.map((node) => ) + row.map((uid) => { + const node = nodeMap[uid] + return ( + !isAdditionalNode(node) + ? + : node.___type === 'h' + ? + : node.___type === 'i' + ? + + + : + ); + }) }
- })} - {outputs ?
- {outputs.map((input, idx) => )} -
: null } + ))}
} diff --git a/package.json b/package.json index 70feb47..8097d1e 100644 --- a/package.json +++ b/package.json @@ -10,18 +10,21 @@ }, "dependencies": { "classnames": "^2.3.1", + "deepcopy": "^2.1.0", "next": "12.2.4", "pako": "^2.0.4", "react": "18.2.0", "react-dom": "18.2.0", "react-select": "^5.4.0", - "react-tooltip": "^4.2.21" + "react-tooltip": "^4.2.21", + "seedrandom": "^3.0.5" }, "devDependencies": { "@types/node": "18.6.4", "@types/pako": "^2.0.0", "@types/react": "18.0.17", "@types/react-dom": "18.0.6", + "@types/seedrandom": "^3.0.2", "eslint": "8.21.0", "eslint-config-next": "12.2.4", "typescript": "4.7.4" diff --git a/pages/_document.tsx b/pages/_document.tsx index fb65320..f52fbcb 100644 --- a/pages/_document.tsx +++ b/pages/_document.tsx @@ -1,8 +1,9 @@ import {Html, Main, NextScript, DocumentProps, Head} from 'next/document'; import {FC} from "react"; +import Dict = NodeJS.Dict; const MyDocument: FC = ({ __NEXT_DATA__ }) => { - const pageProps: Record|undefined = __NEXT_DATA__?.props?.pageProps; + const pageProps: Dict|undefined = __NEXT_DATA__?.props?.pageProps; return ( diff --git a/pages/visualize/[name].tsx b/pages/visualize/[name].tsx index 1fccb9b..499577f 100644 --- a/pages/visualize/[name].tsx +++ b/pages/visualize/[name].tsx @@ -2,11 +2,11 @@ import type { NextPage } from 'next' import Head from 'next/head' import {useGroups} from "../../components/contexts/GroupProvider"; import {useFactories} from "../../src/hooks/useFactories"; -import {GraphNode, ProducingGraph} from "../../components/shared/ProducingGraph/ProducingGraph"; +import {ProducingGraph} from "../../components/shared/ProducingGraph/ProducingGraph"; import {useMemo} from "react"; import {calculateInputs} from "../../src/calculateInputs"; import {useRouter} from "next/router"; -import {groupBy, isNonNullable} from "../../src/utils"; +import {groupBy, isNonNullable, uniquify} from "../../src/utils"; import {EnrichedEntity, Recipe} from "../../src/types"; import {DetailGraphNode, NodeDetails} from "../../components/visualize/NodeDetails/NodeDetails"; @@ -39,7 +39,7 @@ const Page: NextPage = () => { const producingNodes: DetailGraphNode[] = useMemo(() => { if (!group) return [] - const nodes = Array.from(new Set([...intermediateFactories, ...group.exports, ...group.malls])) + const nodes = uniquify([...intermediateFactories, ...group.exports, ...group.malls]) .map(findFactory) .filter(isNonNullable) .map((factory: EnrichedEntity) => ({ @@ -52,7 +52,7 @@ const Page: NextPage = () => { .values(groupBy(nodes, node => node.inputs.join())) .map(nodesOfInput => ({ inputs: nodesOfInput[0].inputs, - outputs: Array.from(new Set(nodesOfInput.flatMap(node => node.outputs))), + outputs: uniquify(nodesOfInput.flatMap(node => node.outputs)), name: nodesOfInput.map(node => node.name).join(', '), recipes: nodesOfInput.flatMap(node => node.recipes) } as DetailGraphNode)) diff --git a/src/graph-untangle/index.ts b/src/graph-untangle/index.ts new file mode 100644 index 0000000..02e97c0 --- /dev/null +++ b/src/graph-untangle/index.ts @@ -0,0 +1,252 @@ +import {AdditionalNode, GraphNode, GraphNodeWithIds, isAdditionalNode} from "./types"; +import {Dict} from "../types"; +import deepcopy from "deepcopy"; +import {isNonNullable, shuffleInplace, sortByProperty, uniquify} from "../utils"; + + + +function generateIds>(node: GraphNode): GraphNodeWithIds { + return { + ...node, + ___uid: `${node.name.replace(/[^a-z]/gi, '').toLowerCase().substring(0, 3)}_${crypto.randomUUID().substring(0, 3)}`, + ___uidInputs: [], + ___uidOutputs: [] + } +} + +function generateAdditional>(uid: string, type: 'i'|'h'|'o'): GraphNodeWithIds { + return { + inputs: type !== 'i' ? [uid] : [], + outputs: type !== 'o' ? [uid] : [], + name: uid, + ___type: type, + ___uid: `${uid.replace(/[^a-z]/gi, '').toLowerCase().substring(0, 3)}_${crypto.randomUUID().substring(0, 3)}_${type}`, + ___uidInputs: [], + ___uidOutputs: [] + } +} + +function splitIntoRows>(inputs: string[], nodes: GraphNode[]): [string[][], Dict>] { + const nodesWithId: Dict> = {} + const available = new Set(inputs) + let queue = [...nodes] + const rows: string[][] = [] + while (queue.length) { + const amount = queue.length + const availableOfRow: string[] = [] + rows.push([]) + + queue = queue.filter((node) => { + const isPlaceable = node.inputs.every(input => available.has(input)) + if (isPlaceable) { + const nodeWithId: GraphNodeWithIds = generateIds(node) + rows[rows.length - 1].push(nodeWithId.___uid) + nodesWithId[nodeWithId.___uid] = nodeWithId + availableOfRow.push(...node.outputs) + } + return !isPlaceable + }) + availableOfRow.map(uid => available.add(uid)) + + if (amount === queue.length) { + console.warn("Loop detected! Left over:", queue) + rows.pop() + break + } + } + return [rows, nodesWithId] +} + +function addAdditionalNodes>(rowsRaw: string[][], nodesRaw: Dict>, inputs: string[], outputs: string[]): [string[][], Dict>] { + const addedInputs: Dict = {} + const res: string[][] = deepcopy([[], ...rowsRaw, []]) + const resNodes: Dict> = deepcopy(nodesRaw) + for (let i = 0; i < rowsRaw.length; i++) { + const rowInputs = uniquify(rowsRaw[i].flatMap(row => resNodes[row].inputs)) + for (let rowInput of rowInputs) { + if (rowInput in addedInputs) { + for (let j = addedInputs[rowInput]+1; j < i+1; j++) { + const nodeWithId: GraphNodeWithIds = generateAdditional(rowInput, 'h') + res[j].push(nodeWithId.___uid) + resNodes[nodeWithId.___uid] = nodeWithId + } + addedInputs[rowInput] = i + } else if (inputs.includes(rowInput)) { + const nodeWithId: GraphNodeWithIds = generateAdditional(rowInput, 'i') + res[i].push(nodeWithId.___uid) + resNodes[nodeWithId.___uid] = nodeWithId + addedInputs[rowInput] = i + } + } + const rowOutputs = uniquify(rowsRaw[i].flatMap(row => resNodes[row].outputs)) + for (let rowOutput of rowOutputs) { + if (outputs?.includes(rowOutput)) { + const nodeWithId: GraphNodeWithIds = generateAdditional(rowOutput, 'o') + res[i+2].push(nodeWithId.___uid) + resNodes[nodeWithId.___uid] = nodeWithId + } + addedInputs[rowOutput] = i+1 + } + } + if (res[res.length-1].length === 0) res.splice(res.length-1, 1) + return [res, resNodes] +} + +function linkNodes>(rowsWithInOut: string[][], nodesWithInOut: Dict>): Dict> { + type Store = {input: Dict[], output: Dict[]} + + const store: Store = { + input: Array.from(rowsWithInOut, Object), + output: Array.from(rowsWithInOut, Object) + } + const nodesPremapping = (node: GraphNodeWithIds, rowIdx: number) => { + node.inputs.forEach(input => store.input[rowIdx][input] = [...(store.input[rowIdx][input] ?? []), node.___uid]) + node.outputs.forEach(output => store.output[rowIdx][output] = [...(store.output[rowIdx][output] ?? []), node.___uid]) + }; + const linkInOut = (node: GraphNodeWithIds, rowIdx: number) => { + if (rowIdx > 0) + node.___uidInputs = uniquify(node.inputs.flatMap(input => store.output[rowIdx - 1][input]).filter(isNonNullable)) + if (rowIdx < rowsWithInOut.length-1) + node.___uidOutputs = uniquify(node.outputs.flatMap(output => store.input[rowIdx + 1][output]).filter(isNonNullable)) + }; + + rowsWithInOut.forEach((row, rowIdx) => row.forEach(uid => nodesPremapping(nodesWithInOut[uid], rowIdx))) + rowsWithInOut.forEach((row, rowIdx) => row.forEach(uid => linkInOut(nodesWithInOut[uid], rowIdx))) + return nodesWithInOut +} + +function crossingsOf>(fixed: string[], flex: string[], nodes: Dict>, isDown: boolean): [number[][], number] { + const result = Array.from(flex, () => Array.from(flex, () => 9999999)) + let score = 0 + for (let i = 0; i < flex.length-1; i++) { + for (let j = i+1; j < flex.length; j++) { + const inputsI = new Set(nodes[flex[i]][isDown ? '___uidInputs' : '___uidOutputs']) + const inputsJ = new Set(nodes[flex[j]][isDown ? '___uidInputs' : '___uidOutputs']) + const diff = fixed.reduce(([size, acc], curr) => { + inputsI.has(curr) && size-- + return [size, acc + (inputsJ.has(curr) ? size : 0)] + }, [inputsI.size, 0])[1] - fixed.reduce(([size, acc], curr) => { + inputsJ.has(curr) && size-- + return [size, acc + (inputsI.has(curr) ? size : 0)] + }, [inputsJ.size, 0])[1] + result[i][j] = diff + result[j][i] = -diff + score += diff + } + } + return [result, score] +} + +function optimizeOrder>(rowsWithInOut: string[][], nodesWithInOut: Dict>): [string[][], number] { + + function addCosts(costsUp: [number[][], number], costsDown: [number[][], number]): [number[][], number] { + return [ + costsUp[0].map((row, rowIdx) => row.map((col, colIdx) => col+costsDown[0][rowIdx][colIdx])), + costsUp[1] + costsDown[1] + ] + } + + function improveRow(top: string[]|undefined, mid: string[], bot: string[]|undefined) { + const costsUp = top ? crossingsOf(top, mid, nodesWithInOut, true) : undefined + const costsDown = bot ? crossingsOf(bot, mid, nodesWithInOut, false) : undefined + const [costs, scoreConst] = costsUp && costsDown ? addCosts(costsUp, costsDown) : costsDown ?? costsUp ?? [undefined, 0] + let score = scoreConst + if (!costs) return score + let improvementInRow = true + while (improvementInRow) { + improvementInRow = false + const colBest = costs + .map((_, idx) => costs + .slice(0, idx) + .map(r => r[idx]) + .reverse() + .reduce(([sum, best], curr, i) => { + return sum + curr > best.sum + ? [sum + curr, {sum: sum+curr, move: -i-1, idx}] as const + : [sum + curr, best] as const + }, [0, {sum: -10000, move: 0, idx: 0}] as readonly [number, Reduce])[1] + ) + .filter(isNonNullable) + const rowBest = costs + .map((_, idx) => costs[idx] + .slice(idx+1) + .reduce(([sum, best], curr, i) => { + return sum + curr > best.sum + ? [sum + curr, {sum: sum+curr, move: i+1, idx}] as const + : [sum + curr, best] as const + }, [0, {sum: -10000, move: 0, idx: 0}] as readonly [number, Reduce])[1] + ) + .filter(isNonNullable) + const replacement = [...colBest, ...rowBest].sort(sortByProperty(red => -red.sum))[0] + if (replacement.sum > 0) { + score -= 2*replacement.sum + improvement = true + improvementInRow = true + costs.splice(replacement.move+replacement.idx, 0, ...costs.splice(replacement.idx, 1)) + costs.map(col => col.splice(replacement.move+replacement.idx, 0, ...col.splice(replacement.idx, 1))) + mid.splice(replacement.move+replacement.idx, 0, ...mid.splice(replacement.idx, 1)) + } + } + return score + } + + type Reduce = {sum: number, move: number, idx: number} + let improvement = true + let scores: number[] = [] + while (improvement) { + improvement = false + rowsWithInOut.reduce((top, mid, idx) => { + scores[idx] = improveRow(top, mid, rowsWithInOut[idx+1]) + return mid + }, undefined as string[]|undefined) + rowsWithInOut.reduceRight((bot, mid, idx) => { + scores[idx] = improveRow(rowsWithInOut[idx-1], mid, bot) + return mid + }, undefined as string[]|undefined) + } + return [rowsWithInOut, scores.reduce((a, b) => a+b)] +} + +export function findBest>(rowsWithInOut: string[][], nodesWithInOut: Dict>, timeLimit: number) { + let bestScore = Infinity + let bestRows = deepcopy(rowsWithInOut) + let limit = Date.now() + timeLimit + let iterSinceImprovements = 0 + while (true) { + if (Date.now() > limit) break + if (iterSinceImprovements > 100) break + const [rowsOptimized, score] = optimizeOrder(rowsWithInOut, nodesWithInOut) + if (score < bestScore) { + bestScore = score + bestRows = deepcopy(rowsOptimized) + iterSinceImprovements = 0 + } else { + iterSinceImprovements++ + } + rowsOptimized.forEach(shuffleInplace) + } + console.log(bestScore) + return bestRows +} + +export function graphUntangled>(nodes: GraphNode[], inputs: string[], outputs: string[], timeLimit = 0): [string[][], Dict>] { + //console.log('---------------') + const [rowsRaw, nodesRaw] = splitIntoRows(inputs, nodes) + //console.log("Step 1") + //console.table(rowsRaw) + //console.table(nodesRaw) + const [rowsWithInOut, nodesWithInOut] = addAdditionalNodes(rowsRaw, nodesRaw, inputs, outputs) + //console.log("Step 2") + //console.table(rowsWithInOut) + //console.table(nodesWithInOut) + const nodesLinked = linkNodes(rowsWithInOut, nodesWithInOut) + //console.log("Step 3") + //console.table(rowsWithInOut) + //console.table(nodesLinked) + const bestRows = findBest(rowsWithInOut, nodesWithInOut, timeLimit) + const orderRow = bestRows.find(row => row.length > 1) + if ((orderRow?.[0]?.localeCompare(orderRow[orderRow.length-1]) ?? 0) > 0) { + bestRows.forEach(row => row.reverse()) + } + return [bestRows, nodesLinked] +} diff --git a/src/graph-untangle/types.ts b/src/graph-untangle/types.ts new file mode 100644 index 0000000..98b1042 --- /dev/null +++ b/src/graph-untangle/types.ts @@ -0,0 +1,20 @@ +import {Dict} from "../types"; + +interface GraphNodeBase { + inputs: string[] + outputs: string[] + name: string +} + +export type GraphNode> = GraphNodeBase & T +export type GraphNodeWithIds> = GraphNode & { + ___uid: string + ___uidInputs: string[] + ___uidOutputs: string[] +} + +export type AdditionalNode = { + ___type: 'i'|'h'|'o' +} + +export const isAdditionalNode = (node: unknown): node is GraphNodeWithIds => !!(node && typeof node === 'object' && '___type' in node) diff --git a/src/svg.ts b/src/svg.ts new file mode 100644 index 0000000..e4f0185 --- /dev/null +++ b/src/svg.ts @@ -0,0 +1,15 @@ +export function createPath(container: DOMRect, start: DOMRect, end: DOMRect) { + const startX = Math.round(start.x + start.width/2 - container.x) + const startY = Math.round(start.bottom - container.y) + const endX = Math.round(end.x + end.width/2 - container.x) + const endY = Math.round(end.top - container.y) + const mid = Math.round((start.bottom + end.top) / 2 - container.y) + return `M${startX},${startY} C${startX},${mid} ${endX},${mid} ${endX},${endY}` +} + +export function drawLine(container: DOMRect, elem: DOMRect) { + const x = Math.round(elem.x + elem.width/2 - container.x) + const top = Math.round(elem.top - container.y) + const bottom = Math.round(elem.bottom - container.y) + return `M${x},${top} L${x},${bottom}` +} diff --git a/src/types.ts b/src/types.ts index 34c4e33..bc6014f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,7 +1,7 @@ export interface Recipe { - prerequisites: Record + prerequisites: Dict time: number - output: Record + output: Dict } export interface UnfetchedEntity { @@ -23,3 +23,5 @@ export interface Group { exports: string[] malls: string[] } + +export type Dict = Record diff --git a/src/utils.ts b/src/utils.ts index dccdf26..7b81d68 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ -import {FC, PropsWithChildren} from "react"; +import {Dict} from "./types"; export function isNonNullable(any: T): any is NonNullable { return any !== undefined && any !== null @@ -13,11 +13,23 @@ export function sortByProperty(transform: (val: T) => number | string): (a: T } } -export function groupBy(arr: T[], transform: (val: T) => string): Record { - const result: Record = {} +export function groupBy(arr: T[], transform: (val: T) => string): Dict { + const result: Dict = {} for (const elem of arr) { const key = transform(elem) result[key] = [...(result[key] ?? []), elem] } return result } + +export function uniquify(array: T[]): T[] { + return Array.from(new Set(array)) +} + +export function shuffleInplace(array: T[]): T[] { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array +} diff --git a/tsconfig.json b/tsconfig.json index 99710e8..bad4c60 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "target": "es5", + "target": "es2015", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, diff --git a/yarn.lock b/yarn.lock index f3de586..ebc1371 100644 --- a/yarn.lock +++ b/yarn.lock @@ -351,6 +351,11 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== +"@types/seedrandom@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/seedrandom/-/seedrandom-3.0.2.tgz#7f30db28221067a90b02e73ffd46b6685b18df1a" + integrity sha512-YPLqEOo0/X8JU3rdiq+RgUKtQhQtrppE766y7vMTu8dGML7TVtZNiiiaC/hhU9Zqw9UYopXxhuWWENclMVBwKQ== + "@typescript-eslint/parser@^5.21.0": version "5.33.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.33.0.tgz#26ec3235b74f0667414613727cb98f9b69dc5383" @@ -664,6 +669,13 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +deepcopy@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/deepcopy/-/deepcopy-2.1.0.tgz#2deb0dd52d079c2ecb7924b640a7c3abd4db1d6d" + integrity sha512-8cZeTb1ZKC3bdSCP6XOM1IsTczIO73fdqtwa2B0N15eAz7gmyhQo+mc5gnFuulsgN3vIQYmTgbmQVKalH1dKvQ== + dependencies: + type-detect "^4.0.8" + define-properties@^1.1.3, define-properties@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" @@ -1875,6 +1887,11 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" +seedrandom@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/seedrandom/-/seedrandom-3.0.5.tgz#54edc85c95222525b0c7a6f6b3543d8e0b3aa0a7" + integrity sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg== + semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -2052,6 +2069,11 @@ type-check@^0.4.0, type-check@~0.4.0: dependencies: prelude-ls "^1.2.1" +type-detect@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + type-fest@^0.20.2: version "0.20.2" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"