diff --git a/components/visualize/PageOverview.tsx b/components/visualize/PageOverview.tsx index d48090a..c56ea79 100644 --- a/components/visualize/PageOverview.tsx +++ b/components/visualize/PageOverview.tsx @@ -34,7 +34,12 @@ export const PageOverview: FC = () => {

Factorio Microservices

- +
diff --git a/components/visualize/ProducingGraph/ProducingGraph.tsx b/components/visualize/ProducingGraph/ProducingGraph.tsx index ba1054e..8ae51a4 100644 --- a/components/visualize/ProducingGraph/ProducingGraph.tsx +++ b/components/visualize/ProducingGraph/ProducingGraph.tsx @@ -16,17 +16,19 @@ interface Props> { inputs: string[] outputs?: string[] childType: FC & { node: GraphNode }> + mergeHidden?: boolean } export const ProducingGraph = >({ nodes, inputs, outputs, - childType: ChildType + childType: ChildType, + mergeHidden }: PropsWithChildren>) => { const planeRef = useRef(null) const [[rows, nodeMap]] = useState<[string[][], Dict>]>( - graphUntangled(nodes, inputs, outputs ?? [], 3000) + graphUntangled(nodes, inputs, outputs ?? [], { timeLimit: 3000, mergeHidden }) ) useEffect(() => { diff --git a/src/graph-untangle/index.ts b/src/graph-untangle/index.ts index a481f9e..29237f3 100644 --- a/src/graph-untangle/index.ts +++ b/src/graph-untangle/index.ts @@ -1,9 +1,18 @@ -import { AdditionalNode, GraphNode, GraphNodeWithIds } from './types' +import { AdditionalNode, GraphNode, GraphNodeWithIds, isAdditionalNode } from './types' import { Dict } from '../types' import deepcopy from 'deepcopy' -import { isNonNullable, shuffleInplace, sortByProperty, uniquify } from '../utils' +import { + filterInPlace, + groupBy, + isNonNullable, + shuffleInplace, + sortByProperty, + uniquify +} from '../utils' import seedrandom from 'seedrandom' +export type NodeList> = Dict> + function generateIds>( node: GraphNode, nodeUid: () => string @@ -13,7 +22,7 @@ function generateIds>( ___uid: `${node.name .replace(/[^a-z]/gi, '') .toLowerCase() - .substring(0, 3)}_${nodeUid()}`, + .substring(0, 10)}_${nodeUid()}`, ___uidInputs: [], ___uidOutputs: [] } @@ -32,7 +41,7 @@ function generateAdditional( ___uid: `${uid .replace(/[^a-z]/gi, '') .toLowerCase() - .substring(0, 3)}_${nodeUid()}_${type}`, + .substring(0, 10)}_${nodeUid()}_${type}`, ___uidInputs: [], ___uidOutputs: [] } @@ -79,10 +88,10 @@ function addAdditionalNodes>( inputs: string[], outputs: string[], nodeUid: () => string -): [string[][], Dict>] { +): [string[][], NodeList] { const addedInputs: Dict = {} const res: string[][] = deepcopy([[], ...rowsRaw, []]) - const resNodes: Dict> = deepcopy(nodesRaw) + const resNodes: NodeList = deepcopy(nodesRaw) for (let i = 0; i < rowsRaw.length; i++) { const rowInputs = uniquify(rowsRaw[i].flatMap(row => resNodes[row].inputs)) for (const rowInput of rowInputs) { @@ -126,15 +135,12 @@ function addAdditionalNodes>( return [res, resNodes] } -function linkNodes>( - rowsWithInOut: string[][], - nodesWithInOut: Dict> -): Dict> { +function linkNodes>(rows: string[][], nodes: NodeList): NodeList { type Store = { input: Dict[], output: Dict[] } const store: Store = { - input: Array.from(rowsWithInOut, Object), - output: Array.from(rowsWithInOut, Object) + input: Array.from(rows, () => ({})), + output: Array.from(rows, () => ({})) } const nodesPremapping = (node: GraphNodeWithIds, rowIdx: number) => { node.inputs.forEach( @@ -150,23 +156,30 @@ function linkNodes>( node.___uidInputs = uniquify( node.inputs.flatMap(input => store.output[rowIdx - 1][input]).filter(isNonNullable) ) - if (rowIdx < rowsWithInOut.length - 1) + if (rowIdx < rows.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 + rows.forEach((row, rowIdx) => { + row.forEach(uid => { + /*if (isAdditionalNode(nodes[uid]) && nodes[uid].___type === 'h' && (nodes[uid].inputs.length > 1 || nodes[uid].___uidInputs.length > 1)) + console.log(rowIdx, nodes[uid])*/ + nodesPremapping(nodes[uid], rowIdx) + }) + }) + /*console.table(store.input) + console.table(store.output) + console.table(nodes)*/ + rows.forEach((row, rowIdx) => row.forEach(uid => linkInOut(nodes[uid], rowIdx))) + return nodes } function crossingsOf>( fixed: string[], flex: string[], - nodes: Dict>, + nodes: NodeList, isDown: boolean ): [number[][], number] { const result = Array.from(flex, () => Array.from(flex, () => 9999999)) @@ -199,8 +212,8 @@ function crossingsOf>( } function optimizeOrder>( - rowsWithInOut: string[][], - nodesWithInOut: Dict> + rows: string[][], + nodes: NodeList ): [string[][], number] { function addCosts( costsUp: [number[][], number], @@ -213,8 +226,8 @@ function optimizeOrder>( } 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 costsUp = top ? crossingsOf(top, mid, nodes, true) : undefined + const costsDown = bot ? crossingsOf(bot, mid, nodes, false) : undefined const [costs, scoreConst] = costsUp && costsDown ? addCosts(costsUp, costsDown) : costsDown ?? costsUp ?? [undefined, 0] let score = scoreConst @@ -272,21 +285,21 @@ function optimizeOrder>( const scores: number[] = [] while (improvement) { improvement = false - rowsWithInOut.reduce((top, mid, idx) => { - scores[idx] = improveRow(top, mid, rowsWithInOut[idx + 1]) + rows.reduce((top, mid, idx) => { + scores[idx] = improveRow(top, mid, rows[idx + 1]) return mid }, undefined as string[] | undefined) - rowsWithInOut.reduceRight((bot, mid, idx) => { - scores[idx] = improveRow(rowsWithInOut[idx - 1], mid, bot) + rows.reduceRight((bot, mid, idx) => { + scores[idx] = improveRow(rows[idx - 1], mid, bot) return mid }, undefined as string[] | undefined) } - return [rowsWithInOut, scores.reduce((a, b) => a + b)] + return [rows, scores.reduce((a, b) => a + b)] } export function findBest>( rowsWithInOut: string[][], - nodesWithInOut: Dict>, + nodesWithInOut: NodeList, timeLimit: number ) { let bestScore = Infinity @@ -310,19 +323,51 @@ export function findBest>( return bestRows } +function mergeHiddenNodes>( + rows: string[][], + nodes: NodeList +): [string[][], NodeList] { + for (const row of rows) { + const groups = groupBy(row, elem => { + const node = nodes[elem] + if (isAdditionalNode(node) && node.___type === 'h') return node.___uidInputs[0] + else return node.___uid + }) + const changes = Object.values(groups).filter(group => group.length > 1) + for (const change of changes) { + const [firstUid, ...otherUids] = change + const first = nodes[firstUid] + first.___uidOutputs = uniquify(change.flatMap(c => nodes[c].___uidOutputs)) + first.outputs = uniquify(change.flatMap(c => nodes[c].outputs)) + const input = nodes[first.___uidInputs[0]] + input.___uidOutputs.filter(uid => !otherUids.includes(uid)) + for (const otherUid of otherUids) { + const outputs = nodes[otherUid].___uidOutputs + for (const output of outputs) { + nodes[output].___uidInputs = uniquify( + nodes[output].___uidInputs.map(uid => (uid === otherUid ? firstUid : uid)) + ) + } + delete nodes[otherUid] + } + filterInPlace(row, uid => !otherUids.includes(uid)) + } + } + return [rows, nodes] +} + export function graphUntangled>( nodes: GraphNode[], inputs: string[], outputs: string[], - timeLimit = 0 -): [string[][], Dict>] { + options: { + timeLimit?: number + mergeHidden?: boolean + } = {} +): [string[][], NodeList] { const nodeSeed = seedrandom.alea('node') const nodeUid = () => Math.abs(nodeSeed.int32()).toString(16).substring(0, 3) - //console.log('---------------') const [rowsRaw, nodesRaw] = splitIntoRows(inputs, nodes, nodeUid) - //console.log("Step 1") - //console.table(rowsRaw) - //console.table(nodesRaw) const [rowsWithInOut, nodesWithInOut] = addAdditionalNodes( rowsRaw, nodesRaw, @@ -330,13 +375,10 @@ export function graphUntangled>( outputs, nodeUid ) - //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 [rowsMergedHidden, nodesMergedHidden] = options.mergeHidden + ? mergeHiddenNodes(rowsWithInOut, nodesLinked) + : [rowsWithInOut, nodesLinked] + const bestRows = findBest(rowsMergedHidden, nodesMergedHidden, options.timeLimit ?? 0) return [bestRows, nodesLinked] } diff --git a/src/utils.ts b/src/utils.ts index 3ab953c..cc015bb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -35,6 +35,23 @@ export function shuffleInplace(array: T[], rng: PRNG): T[] { return array } +export function filterInPlace( + array: T[], + condition: (elem: T, idx: number, arr: T[]) => boolean +) { + let i = 0, + j = 0 + + while (i < array.length) { + const val = array[i] + if (condition(val, i, array)) array[j++] = val + i++ + } + + array.length = j + return array +} + export function fixedEncodeURIComponent(str: string): string { return encodeURIComponent(str).replace(/[!'()*]/g, c => '%' + c.charCodeAt(0).toString(16)) }