Implemented sorted graph
This commit is contained in:
@@ -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<string, Group>
|
||||
groups: Dict<Group>
|
||||
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<GroupContextType>(defaultValues);
|
||||
export const useGroups = () => useContext(GroupContext)
|
||||
|
||||
interface StoredFile {
|
||||
groups: Record<string, Group>,
|
||||
groups: Dict<Group>,
|
||||
basicValues: string[],
|
||||
excludedSuggestions: string[]
|
||||
}
|
||||
@@ -64,7 +65,7 @@ interface StoredFile {
|
||||
export const GroupProvider: FC<Props> = ({children}) => {
|
||||
const [excludedSuggestions, setExcludedSuggestions] = useLocalStorage<string[]>('excludedSuggestions', [])
|
||||
const [basicValues, setBasicValues] = useLocalStorage<string[]>('basicValues', [])
|
||||
const [groups, setGroups] = useLocalStorage<Record<string, Group>>('serviceGroups', {})
|
||||
const [groups, setGroups] = useLocalStorage<Dict<Group>>('serviceGroups', {})
|
||||
|
||||
const doNotSuggest = useMemo<Set<string>>(() => {
|
||||
return new Set([...Object.values(groups).flatMap(group => [...group.exports, ...group.malls]), ...excludedSuggestions, ...basicValues])
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
border: 1px solid white;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
height: fit-content;
|
||||
}
|
||||
|
||||
.amount {
|
||||
|
||||
@@ -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<Props> = ({ 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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<T extends Record<string, unknown>> = 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<T extends {}> {
|
||||
nodes: GraphNode<T>[]
|
||||
@@ -19,53 +13,116 @@ interface Props<T extends {}> {
|
||||
childType: FC<HTMLProps<HTMLDivElement> & {node: GraphNode<T>}>
|
||||
}
|
||||
|
||||
export const ProducingGraph = <T extends Record<string, unknown>,>({nodes, inputs, outputs, childType: ChildType}: PropsWithChildren<Props<T>>) => {
|
||||
const rows: GraphNode<T>[][] = useMemo(() => {
|
||||
const available = new Set(inputs)
|
||||
let todo = [...nodes]
|
||||
const result: GraphNode<T>[][] = []
|
||||
while (todo.length) {
|
||||
const amount = todo.length
|
||||
const thisRow: string[] = []
|
||||
result.push([])
|
||||
export const ProducingGraph = <T extends Dict<unknown>,>({nodes, inputs, outputs, childType: ChildType}: PropsWithChildren<Props<T>>) => {
|
||||
const planeRef = useRef<HTMLDivElement>(null)
|
||||
const [[rows, nodeMap], setGraph] = useState<[string[][], Dict<GraphNodeWithIds<T|AdditionalNode>>]>([[], {}])
|
||||
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
|
||||
/*const rowsWithInOut: (GraphNode<T>|AdditionalNode)[][] = useMemo(() => {
|
||||
const addedInputs = new Set<string>()
|
||||
const res: (GraphNode<T>|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})
|
||||
}
|
||||
return true
|
||||
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 res
|
||||
}, [rows, inputs, outputs])*/
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
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 <div className={styles.plane}>
|
||||
<div className={styles.row}>
|
||||
{inputs.map((input, idx) => <EntityIcon className={styles.input} key={input} value={input} />)}
|
||||
</div>
|
||||
{rows.map((row, colIdx) => {
|
||||
return <div className={styles.row} key={colIdx}>
|
||||
return <div className={styles.plane} ref={planeRef}>
|
||||
{rows.map((row, colIdx) => (
|
||||
<div className={styles.row} key={colIdx} data-row={true}>
|
||||
{
|
||||
row.map((node) => <ChildType
|
||||
row.map((uid) => {
|
||||
const node = nodeMap[uid]
|
||||
return (
|
||||
!isAdditionalNode(node)
|
||||
? <ChildType
|
||||
data-name={node.___uid}
|
||||
data-inputs={node.___uidInputs.join(' ')}
|
||||
data-outputs={node.___uidOutputs.join(' ')}
|
||||
className={styles.node}
|
||||
key={node.name}
|
||||
node={node}
|
||||
/>)
|
||||
/>
|
||||
: node.___type === 'h'
|
||||
? <span
|
||||
className={styles.hidden}
|
||||
key={node.___uid}
|
||||
data-name={node.___uid}
|
||||
data-inputs={node.___uidInputs.join(' ')}
|
||||
data-outputs={node.___uidOutputs.join(' ')}
|
||||
data-hidden={true}></span>
|
||||
: node.___type === 'i'
|
||||
? <span
|
||||
className={styles.input}
|
||||
key={node.___uid}
|
||||
data-name={node.___uid}
|
||||
data-outputs={node.___uidOutputs.join(' ')}
|
||||
data-hidden={true}>
|
||||
<EntityIcon value={node.name}/>
|
||||
</span>
|
||||
: <EntityIcon
|
||||
className={styles.input}
|
||||
key={node.___uid}
|
||||
value={node.name}
|
||||
data-name={node.___uid}
|
||||
data-inputs={node.___uidInputs.join(' ')}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
})}
|
||||
{outputs ? <div className={styles.row}>
|
||||
{outputs.map((input, idx) => <EntityIcon className={styles.input} key={input} value={input} />)}
|
||||
</div> : null }
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {Html, Main, NextScript, DocumentProps, Head} from 'next/document';
|
||||
import {FC} from "react";
|
||||
import Dict = NodeJS.Dict;
|
||||
|
||||
const MyDocument: FC<DocumentProps> = ({ __NEXT_DATA__ }) => {
|
||||
const pageProps: Record<string, unknown>|undefined = __NEXT_DATA__?.props?.pageProps;
|
||||
const pageProps: Dict<unknown>|undefined = __NEXT_DATA__?.props?.pageProps;
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
|
||||
@@ -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))
|
||||
|
||||
252
src/graph-untangle/index.ts
Normal file
252
src/graph-untangle/index.ts
Normal file
@@ -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<T extends Dict<unknown>>(node: GraphNode<T>): GraphNodeWithIds<T> {
|
||||
return {
|
||||
...node,
|
||||
___uid: `${node.name.replace(/[^a-z]/gi, '').toLowerCase().substring(0, 3)}_${crypto.randomUUID().substring(0, 3)}`,
|
||||
___uidInputs: [],
|
||||
___uidOutputs: []
|
||||
}
|
||||
}
|
||||
|
||||
function generateAdditional<T extends Dict<unknown>>(uid: string, type: 'i'|'h'|'o'): GraphNodeWithIds<AdditionalNode> {
|
||||
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<T extends Dict<unknown>>(inputs: string[], nodes: GraphNode<T>[]): [string[][], Dict<GraphNodeWithIds<T>>] {
|
||||
const nodesWithId: Dict<GraphNodeWithIds<T>> = {}
|
||||
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<T> = 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<T extends Dict<unknown>>(rowsRaw: string[][], nodesRaw: Dict<GraphNodeWithIds<T>>, inputs: string[], outputs: string[]): [string[][], Dict<GraphNodeWithIds<T|AdditionalNode>>] {
|
||||
const addedInputs: Dict<number> = {}
|
||||
const res: string[][] = deepcopy([[], ...rowsRaw, []])
|
||||
const resNodes: Dict<GraphNodeWithIds<T|AdditionalNode>> = 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<AdditionalNode> = generateAdditional(rowInput, 'h')
|
||||
res[j].push(nodeWithId.___uid)
|
||||
resNodes[nodeWithId.___uid] = nodeWithId
|
||||
}
|
||||
addedInputs[rowInput] = i
|
||||
} else if (inputs.includes(rowInput)) {
|
||||
const nodeWithId: GraphNodeWithIds<AdditionalNode> = 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<AdditionalNode> = 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<T extends Dict<unknown>>(rowsWithInOut: string[][], nodesWithInOut: Dict<GraphNodeWithIds<T|AdditionalNode>>): Dict<GraphNodeWithIds<T|AdditionalNode>> {
|
||||
type Store = {input: Dict<string[]>[], output: Dict<string[]>[]}
|
||||
|
||||
const store: Store = {
|
||||
input: Array.from(rowsWithInOut, Object),
|
||||
output: Array.from(rowsWithInOut, Object)
|
||||
}
|
||||
const nodesPremapping = (node: GraphNodeWithIds<T|AdditionalNode>, 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<T|AdditionalNode>, 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<T extends Dict<unknown>>(fixed: string[], flex: string[], nodes: Dict<GraphNodeWithIds<T|AdditionalNode>>, 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<T extends Dict<unknown>>(rowsWithInOut: string[][], nodesWithInOut: Dict<GraphNodeWithIds<T|AdditionalNode>>): [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<T extends Dict<unknown>>(rowsWithInOut: string[][], nodesWithInOut: Dict<GraphNodeWithIds<T|AdditionalNode>>, 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<T extends Dict<unknown>>(nodes: GraphNode<T>[], inputs: string[], outputs: string[], timeLimit = 0): [string[][], Dict<GraphNodeWithIds<T|AdditionalNode>>] {
|
||||
//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]
|
||||
}
|
||||
20
src/graph-untangle/types.ts
Normal file
20
src/graph-untangle/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {Dict} from "../types";
|
||||
|
||||
interface GraphNodeBase {
|
||||
inputs: string[]
|
||||
outputs: string[]
|
||||
name: string
|
||||
}
|
||||
|
||||
export type GraphNode<T extends Dict<unknown>> = GraphNodeBase & T
|
||||
export type GraphNodeWithIds<T extends Dict<unknown>> = GraphNode<T> & {
|
||||
___uid: string
|
||||
___uidInputs: string[]
|
||||
___uidOutputs: string[]
|
||||
}
|
||||
|
||||
export type AdditionalNode = {
|
||||
___type: 'i'|'h'|'o'
|
||||
}
|
||||
|
||||
export const isAdditionalNode = (node: unknown): node is GraphNodeWithIds<AdditionalNode> => !!(node && typeof node === 'object' && '___type' in node)
|
||||
15
src/svg.ts
Normal file
15
src/svg.ts
Normal file
@@ -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}`
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
export interface Recipe {
|
||||
prerequisites: Record<string, number>
|
||||
prerequisites: Dict<number>
|
||||
time: number
|
||||
output: Record<string, number>
|
||||
output: Dict<number>
|
||||
}
|
||||
|
||||
export interface UnfetchedEntity {
|
||||
@@ -23,3 +23,5 @@ export interface Group {
|
||||
exports: string[]
|
||||
malls: string[]
|
||||
}
|
||||
|
||||
export type Dict<T> = Record<string, T>
|
||||
|
||||
18
src/utils.ts
18
src/utils.ts
@@ -1,4 +1,4 @@
|
||||
import {FC, PropsWithChildren} from "react";
|
||||
import {Dict} from "./types";
|
||||
|
||||
export function isNonNullable<T>(any: T): any is NonNullable<T> {
|
||||
return any !== undefined && any !== null
|
||||
@@ -13,11 +13,23 @@ export function sortByProperty<T>(transform: (val: T) => number | string): (a: T
|
||||
}
|
||||
}
|
||||
|
||||
export function groupBy<T>(arr: T[], transform: (val: T) => string): Record<string, T[]> {
|
||||
const result: Record<string, T[]> = {}
|
||||
export function groupBy<T>(arr: T[], transform: (val: T) => string): Dict<T[]> {
|
||||
const result: Dict<T[]> = {}
|
||||
for (const elem of arr) {
|
||||
const key = transform(elem)
|
||||
result[key] = [...(result[key] ?? []), elem]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function uniquify<T>(array: T[]): T[] {
|
||||
return Array.from(new Set(array))
|
||||
}
|
||||
|
||||
export function shuffleInplace<T>(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
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"target": "es2015",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
22
yarn.lock
22
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"
|
||||
|
||||
Reference in New Issue
Block a user