From 21fc258191224b6adfbbae8283975fed2a993562 Mon Sep 17 00:00:00 2001 From: Marcin Bonk Date: Tue, 30 Nov 2021 03:08:38 +0100 Subject: [PATCH 1/9] Added docker support --- .docker/nginx/conf.d/domain.conf | 27 +++++++++++++++ .docker/node/docker-entrypoint.sh | 18 ++++++++++ .docker/php/docker-entrypoint.sh | 9 +++++ Dockerfile | 57 +++++++++++++++++++++++++++++++ README.md | 18 ++++++++-- docker-compose.yml | 38 +++++++++++++++++++++ package.json | 5 +-- 7 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 .docker/nginx/conf.d/domain.conf create mode 100644 .docker/node/docker-entrypoint.sh create mode 100644 .docker/php/docker-entrypoint.sh create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.docker/nginx/conf.d/domain.conf b/.docker/nginx/conf.d/domain.conf new file mode 100644 index 00000000..57ec9f80 --- /dev/null +++ b/.docker/nginx/conf.d/domain.conf @@ -0,0 +1,27 @@ +server { + listen 80 default_server; + + root /var/www/www; + index index.php index.html index.htm; + + client_max_body_size 100M; + + location / { + try_files $uri $uri/ /index.php$is_args$args; + } + + location ~ \.php$ { + try_files $uri /index.php =404; + fastcgi_pass php:9000; + fastcgi_index index.php; + + fastcgi_buffer_size 128k; + fastcgi_buffers 4 256k; + fastcgi_busy_buffers_size 256k; + + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + #fixes timeouts + fastcgi_read_timeout 600; + include fastcgi_params; + } +} diff --git a/.docker/node/docker-entrypoint.sh b/.docker/node/docker-entrypoint.sh new file mode 100644 index 00000000..ecd73af8 --- /dev/null +++ b/.docker/node/docker-entrypoint.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -e + +# first arg is `-f` or `--some-option` +if [ "${1#-}" != "$1" ]; then + set -- node "$@" +fi + +if [ "$1" = 'node' ] || [ "$1" = 'yarn' ]; then + yarn install + + >&2 echo "Waiting for PHP to be ready..." + until nc -z "$PHP_HOST" "$PHP_PORT"; do + sleep 1 + done +fi + +exec "$@" diff --git a/.docker/php/docker-entrypoint.sh b/.docker/php/docker-entrypoint.sh new file mode 100644 index 00000000..ab5e1f3f --- /dev/null +++ b/.docker/php/docker-entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -e + +# first arg is `-f` or `--some-option` +if [ "${1#-}" != "$1" ]; then + set -- php-fpm "$@" +fi + +exec docker-php-entrypoint "$@" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..71081361 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,57 @@ +ARG PHP_VERSION=8.0 +ARG NODE_VERSION=16 +ARG NGINX_VERSION=1.27.3 + +######### PHP CONFIG +FROM php:${PHP_VERSION}-fpm-alpine as php + +WORKDIR /srv/app +COPY www www/ + +EXPOSE 9000 + +COPY ./.docker/php/docker-entrypoint.sh /usr/local/bin/docker-entrypoint +RUN chmod +x /usr/local/bin/docker-entrypoint + +ENTRYPOINT ["docker-entrypoint"] +CMD ["php-fpm"] + +######### NODE CONFIG +FROM node:${NODE_VERSION}-alpine as node + +WORKDIR /srv/app + +RUN apk update; \ + apk add yarn; +RUN apk add --no-cache --virtual .build-deps alpine-sdk python3 + +# prevent the reinstallation of vendors at every changes in the source code +COPY package.json yarn.lock ./ +COPY tsconfig.json tslint.json webpack.config.js ./ +COPY src src/ +COPY styles styles/ +COPY templates templates/ +COPY bin bin/ +COPY data data/ + +RUN set -eux; \ + yarn install; \ + yarn cache clean; \ + yarn build + +RUN apk del .build-deps + +COPY ./.docker/node/docker-entrypoint.sh /usr/local/bin/docker-entrypoint +RUN chmod +x /usr/local/bin/docker-entrypoint + +ENTRYPOINT ["docker-entrypoint"] +CMD ["yarn", "start"] + +######### NGINX CONFIG +FROM nginx:${NGINX_VERSION}-alpine as nginx +COPY .docker/nginx/conf.d/domain.conf /etc/nginx/conf.d/default.conf + +WORKDIR /srv/app + +COPY --from=node /srv/app/www/assets www/assets +COPY --from=php /srv/app/www/index.php www/index.php diff --git a/README.md b/README.md index 1deedce0..b4c7b51a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # SatisfactoryTools Satisfactory Tools for planning and building the perfect base. +# Standard application development ## Requirements - node.js version 16 (lower may work, 17+ doesn't work) - yarn @@ -12,15 +13,26 @@ Satisfactory Tools for planning and building the perfect base. - `yarn build` - Set up a virtual host pointing to `/www` directory (using e.g. Apache or ngnix) +## Development +Run `yarn start` to start the automated build process. It will watch over the code and rebuild it on change. + +# Dockerized application development +## Requirements +- docker +- docker-compose + +## Installation / Building +- docker build . + +## Development +- docker compose up -d + ## Contributing Any pull requests are welcome, though some rules must be followed: - try to follow current coding style (there's `tslint` and `.editorconfig`, those should help you with that) - one PR per feature - all PRs must target `dev` branch -## Development -Run `yarn start` to start the automated build process. It will watch over the code and rebuild it on change. - ## Updating data Get the latest Docs.json from your game installation and place it into `data` folder. Then run `yarn parseDocs`command and the `data.json` file would get updated automatically. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..312af2ea --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,38 @@ +version: "3.2" +services: + nginx: + build: + context: . + target: nginx + depends_on: + - php + - node + ports: + - "80:80" + links: + - php + volumes: + - ./.docker/nginx/conf.d/domain.conf:/etc/nginx/conf.d/default.conf:ro + - ./www:/var/www/www:ro + php: + build: + context: . + target: php + depends_on: + - node + volumes: + - ./www:/var/www/www + - ./node_modules:/var/www/node_modules + node: + build: + context: . + target: node + environment: + PHP_HOST: php + PHP_PORT: 9000 + volumes: + - ./src:/var/www/src + - ./styles:/var/www/styles + - ./templates:/var/www/templates + - ./bin:/var/www/bin + - ./data:/var/www/data diff --git a/package.json b/package.json index 13ce28d8..961e69e5 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,10 @@ "author": "greeny", "private": true, "scripts": { - "build": "export SET NODE_OPTIONS=--openssl-legacy-provider && webpack --mode production", + "build": "webpack --mode production", "buildCI": "webpack --mode production", - "start": "export SET NODE_OPTIONS=--openssl-legacy-provider && webpack --mode development --progress --color --watch", + "start": "webpack --mode development --progress --color --watch", + "serve": "webpack serve --open", "parseDocs": "ts-node -r tsconfig-paths/register bin/parseDocs.ts", "parsePak": "ts-node -r tsconfig-paths/register bin/parsePak.ts", "generateImages": "ts-node -r tsconfig-paths/register bin/generateImages.ts" From f0be55faabd63e83d5f0879d5405a6db89e124a9 Mon Sep 17 00:00:00 2001 From: Bergmann89 Date: Wed, 5 Feb 2025 17:21:40 +0100 Subject: [PATCH 2/9] Implemented partial completion and selection feature --- .../Components/VisualizationComponent.ts | 1 + .../VisualizationComponentController.ts | 52 +++- .../Controllers/ProductionController.ts | 1 + src/Tools/Production/IProductionData.ts | 9 + src/Tools/Production/ProductionTab.ts | 72 +++++- src/Tools/Production/Result/CalcCompleted.ts | 142 +++++++++++ src/Tools/Production/Result/CalcHighlight.ts | 228 ++++++++++++++++++ .../Production/Result/Edges/GraphEdge.ts | 24 +- src/Tools/Production/Result/Graph.ts | 128 ++++++++-- src/Tools/Production/Result/ItemAmount.ts | 42 +++- .../Production/Result/Nodes/GraphNode.ts | 20 +- .../Production/Result/Nodes/RecipeNode.ts | 66 ++++- .../Result/ProductionResultFactory.ts | 12 +- src/Tools/Production/Result/ResourceAmount.ts | 9 + src/Utils/Numbers.ts | 6 +- styles/production.scss | 13 +- templates/Controllers/production.html | 113 ++++++++- 17 files changed, 873 insertions(+), 65 deletions(-) create mode 100644 src/Tools/Production/Result/CalcCompleted.ts create mode 100644 src/Tools/Production/Result/CalcHighlight.ts diff --git a/src/Module/Components/VisualizationComponent.ts b/src/Module/Components/VisualizationComponent.ts index 06ff3ae2..6b4dda7f 100644 --- a/src/Module/Components/VisualizationComponent.ts +++ b/src/Module/Components/VisualizationComponent.ts @@ -8,6 +8,7 @@ export class VisualizationComponent implements IComponentOptions public controller = VisualizationComponentController; public bindings = { result: '=', + settings: '=', }; } diff --git a/src/Module/Components/VisualizationComponentController.ts b/src/Module/Components/VisualizationComponentController.ts index 685d2706..90178fe5 100644 --- a/src/Module/Components/VisualizationComponentController.ts +++ b/src/Module/Components/VisualizationComponentController.ts @@ -5,18 +5,20 @@ import cytoscape from 'cytoscape'; import {IVisNode} from '@src/Tools/Production/Result/IVisNode'; import {IVisEdge} from '@src/Tools/Production/Result/IVisEdge'; import {IElkGraph} from '@src/Solver/IElkGraph'; -import {Strings} from '@src/Utils/Strings'; -import model from '@src/Data/Model'; import {ProductionResult} from '@src/Tools/Production/Result/ProductionResult'; +import {GraphSettings} from '@src/Tools/Production/Result/Graph'; export class VisualizationComponentController implements IController { public result: ProductionResult; + public settings: GraphSettings; public static $inject = ['$element', '$scope', '$timeout']; - private unregisterWatcherCallback: () => void; + private unregisterResultWatcher: () => void; + private unregisterSettingsWatcher: () => void; + private network: Network; private fitted: boolean = false; @@ -25,16 +27,26 @@ export class VisualizationComponentController implements IController public $onInit(): void { - this.unregisterWatcherCallback = this.$scope.$watch(() => { + this.unregisterResultWatcher = this.$scope.$watch(() => { return this.result; - }, (newValue) => { - this.updateData(newValue); + }, (data) => { + this.updateData(data); }); + + this.unregisterSettingsWatcher = this.$scope.$watch(() => { + return this.settings; + }, (data) => { + if (this.result) { + this.result.graph.setSettings(data); + this.updateData(this.result); + } + }, true); } public $onDestroy(): void { - this.unregisterWatcherCallback(); + this.unregisterResultWatcher(); + this.unregisterSettingsWatcher(); } public useCytoscape(result: ProductionResult): void @@ -112,10 +124,16 @@ export class VisualizationComponentController implements IController const edges = new DataSet(); for (const node of result.graph.nodes) { - nodes.add(node.getVisNode()); + if (node.visible) { + nodes.add(node.getVisNode()); + } } for (const edge of result.graph.edges) { + if (!edge.to.visible || !edge.from.visible) { + continue + } + const smooth: any = { enabled: false, }; @@ -130,7 +148,7 @@ export class VisualizationComponentController implements IController id: edge.id, from: edge.from.id, to: edge.to.id, - label: model.getItem(edge.itemAmount.item).prototype.name + '\n' + Strings.formatNumber(edge.itemAmount.amount) + ' / min', + label: edge.getText(), color: { color: 'rgba(105, 125, 145, 1)', highlight: 'rgba(134, 151, 167, 1)', @@ -219,7 +237,7 @@ export class VisualizationComponentController implements IController private drawVisualisation(nodes: DataSet, edges: DataSet): Network { - return new Network(this.$element[0], { + const network = new Network(this.$element[0], { nodes: nodes, edges: edges, }, { @@ -264,6 +282,20 @@ export class VisualizationComponentController implements IController tooltipDelay: 0, }, }); + + network.on('doubleClick', (event) => { + if (event.nodes?.length) { + (event.nodes as number[]).forEach((nodeId) => { + const node = this.result.graph.nodes.find((graphNode) => graphNode.id === nodeId); + if (node) { + this.result.graph.highlight(node); + this.updateData(this.result); + } + }); + } + }); + + return network; } } diff --git a/src/Module/Controllers/ProductionController.ts b/src/Module/Controllers/ProductionController.ts index a12ceb2c..f4ea9faf 100644 --- a/src/Module/Controllers/ProductionController.ts +++ b/src/Module/Controllers/ProductionController.ts @@ -33,6 +33,7 @@ export class ProductionController public readonly alternateRecipes: IRecipeSchema[] = data.getAlternateRecipes(); public readonly basicRecipes: IRecipeSchema[] = data.getBaseItemRecipes(); public readonly machines: IBuildingSchema[] = data.getManufacturers(); + public readonly recipes: IRecipeSchema[] = this.basicRecipes.concat(this.alternateRecipes); public result: string; diff --git a/src/Tools/Production/IProductionData.ts b/src/Tools/Production/IProductionData.ts index 3c0393c3..85fd7a37 100644 --- a/src/Tools/Production/IProductionData.ts +++ b/src/Tools/Production/IProductionData.ts @@ -31,6 +31,7 @@ export interface IProductionDataRequest production: IProductionDataRequestItem[]; input: IProductionDataRequestInput[]; + completed?: IProductionDataRequestCompleted[]; } @@ -59,6 +60,14 @@ export interface IProductionDataRequestInput } +export interface IProductionDataRequestCompleted +{ + + recipe: string|null; // classname of the recipe + amount: number; // amount of factories executing the recipe + +} + export interface IProductionDataApiResponse { diff --git a/src/Tools/Production/ProductionTab.ts b/src/Tools/Production/ProductionTab.ts index c0436bb1..0dac4465 100644 --- a/src/Tools/Production/ProductionTab.ts +++ b/src/Tools/Production/ProductionTab.ts @@ -6,13 +6,14 @@ import axios from 'axios'; import {Strings} from '@src/Utils/Strings'; import {IItemSchema} from '@src/Schema/IItemSchema'; import {Callbacks} from '@src/Utils/Callbacks'; -import {IProductionData, IProductionDataApiRequest, IProductionDataRequestInput, IProductionDataRequestItem} from '@src/Tools/Production/IProductionData'; +import {IProductionData, IProductionDataApiRequest, IProductionDataRequestCompleted, IProductionDataRequestInput, IProductionDataRequestItem} from '@src/Tools/Production/IProductionData'; import {ResultStatus} from '@src/Tools/Production/ResultStatus'; import {Solver} from '@src/Solver/Solver'; import {ProductionResult} from '@src/Tools/Production/Result/ProductionResult'; import {ProductionResultFactory} from '@src/Tools/Production/Result/ProductionResultFactory'; import {DataProvider} from '@src/Data/DataProvider'; import {IRecipeSchema} from '@src/Schema/IRecipeSchema'; +import { GraphSettings } from './Result/Graph'; export class ProductionTab { @@ -35,6 +36,13 @@ export class ProductionTab overviewCollapsed: {}, }; + public graphSettings: GraphSettings = { + applyCompleted: true, + showCompleted: true, + showHighlightDependents: true, + showHighlightLimits: true, + }; + public tab: string = 'production'; public resultTab: string = 'visualization'; public shareLink: string = ''; @@ -138,7 +146,10 @@ export class ProductionTab } apiRequest.blockedRecipes = blockedRecipes; + const completed = apiRequest.completed; + delete apiRequest.blockedMachines; + delete apiRequest.completed; Solver.solveProduction(apiRequest, (result) => { const res = () => { @@ -158,8 +169,10 @@ export class ProductionTab return; } + apiRequest.completed = completed; + const factory = new ProductionResultFactory; - this.resultNew = factory.create(apiRequest, result, DataProvider.get()); + this.resultNew = factory.create(this.graphSettings, apiRequest, result, DataProvider.get()); this.resultStatus = ResultStatus.RESULT; }; @@ -196,6 +209,7 @@ export class ProductionTab sinkableResources: [], production: [], input: [], + completed: [], resourceMax: angular.copy(Data.resourceAmounts), resourceWeight: angular.copy(Data.resourceWeights), }, @@ -338,6 +352,44 @@ export class ProductionTab } } + public addEmptyCompleted(): void + { + this.addCompletedItem({ + recipe: null, + amount: 10, + }); + } + + public addCompletedItem(item: IProductionDataRequestCompleted): void + { + this.data.request.completed = this.data.request.completed || []; + this.data.request.completed.push(item); + } + + public cloneCompleted(item: IProductionDataRequestCompleted): void + { + this.data.request.completed = this.data.request.completed || []; + this.data.request.completed.push({ + recipe: item.recipe, + amount: item.amount, + }); + } + + public clearCompleted(): void + { + this.data.request.completed = []; + this.addEmptyCompleted(); + } + + public removeCompleted(item: IProductionDataRequestCompleted): void + { + this.data.request.completed = this.data.request.completed || []; + const index = this.data.request.completed.indexOf(item); + if (index in this.data.request.completed) { + this.data.request.completed.splice(index, 1); + } + } + public setSinkableResourcesSort(sort: string) { if (this.state.sinkableResourcesSortBy === sort) { @@ -429,6 +481,22 @@ export class ProductionTab } } + public toggleApplyCompleted(): void { + this.graphSettings.applyCompleted = !this.graphSettings.applyCompleted; + } + + public toggleShowCompleted(): void { + this.graphSettings.showCompleted = !this.graphSettings.showCompleted; + } + + public toggleHighlightDependents(): void { + this.graphSettings.showHighlightDependents = !this.graphSettings.showHighlightDependents; + } + + public toggleHighlightLimits(): void { + this.graphSettings.showHighlightLimits = !this.graphSettings.showHighlightLimits; + } + public recipeMachineDisabled(recipe: IRecipeSchema): boolean { if (typeof this.data.request.blockedMachines === 'undefined') { diff --git a/src/Tools/Production/Result/CalcCompleted.ts b/src/Tools/Production/Result/CalcCompleted.ts new file mode 100644 index 00000000..308cb658 --- /dev/null +++ b/src/Tools/Production/Result/CalcCompleted.ts @@ -0,0 +1,142 @@ +import { Numbers } from '@src/Utils/Numbers'; +import { Graph } from './Graph'; +import { ByproductNode } from './Nodes/ByproductNode'; +import { ProductNode } from './Nodes/ProductNode'; +import { RecipeNode } from './Nodes/RecipeNode'; +import { SinkNode } from './Nodes/SinkNode'; + +export class CalcCompleted { + private indent: number = 0; + + public constructor(protected readonly graph: Graph) { } + + public update() { + this.resetConsumed(); + this.updateInternal(); + this.updateVisibility(); + } + + protected updateInternal() { + if (this.graph.settings.applyCompleted) { + for (const node of this.graph.nodes) { + if (node instanceof RecipeNode) { + if (this.graph.completedMap[node.recipeData.recipe.className]) { + this.updateNodeCompletion(node); + } + } + } + } + } + + protected resetConsumed() { + for (const node of this.graph.nodes) { + if (node instanceof RecipeNode) { + (node as RecipeNode).completed = 0; + (node as RecipeNode).limit = undefined; + } + + for (const edge of node.connectedEdges) { + edge.itemAmount.consumed = 0; + edge.itemAmount.limit = undefined; + } + } + } + + protected updateVisibility() { + for (const node of this.graph.nodes) { + const output = Numbers.round(node + .getEdgesOut() + .map((x) => x.itemAmount.getAvailable()) + .reduce((acc, sum) => acc + sum, 0)); + + node.visible = node.isAvailable() + && (this.graph.settings.showCompleted + || output > 0 + || node instanceof ProductNode + || node instanceof SinkNode + || node instanceof ByproductNode + || node.highlighted === 'product' + || node.highlighted === 'dependent'); + } + } + + private updateNodeCompletion(node: RecipeNode): void { + const indent = ' |'.repeat(this.indent) + const outputsUsed = node.getOutputs().map((output) => { + let consumed = 0; + let total = 0; + + for (const edge of node.getEdgesOut(output.resource.className)) { + if (edge.to.isAvailable() && !edge.isLoop()) { + consumed += edge.itemAmount.consumed; + total += edge.itemAmount.getAmount(); + } + } + + return total === 0 + ? 1.0 + : consumed / total; + }); + + const outputUsed = Math.min(...outputsUsed); + const outputCompleted = outputUsed * node.getAmount(); + + const userCompleted = this.graph.completedMap[node.recipeData.recipe.className] || 0.0; + + const completedTotal = Math.min(outputCompleted + userCompleted, node.getAmount()); + + this.setNodeCompleted(node, completedTotal); + } + + private setNodeCompleted(node: RecipeNode, completed: number) { + const diff = completed - node.completed; + if (Numbers.floor(diff) <= 0) { + return; + } + + node.completed = completed; + const inputs = node.getInputs(); + const multiplier = node.getMultiplier(diff); + + for (const input of inputs) { + const ingredient = node.recipeData.recipe.ingredients.find((x) => x.item === input.resource.className); + if (!ingredient) { + continue; + } + + const edges = node.getEdgesIn(input.resource.className); + const inputAmount = ingredient.amount * multiplier; + const totalAvailable = edges + .map((edge) => edge.from.isAvailable() + ? edge.itemAmount.getAvailable() + : 0.0) + .reduce((acc, sum) => acc + sum, 0); + + if (totalAvailable <= 0) { + continue; + } + + for (const edge of edges) { + if (!edge.from.isAvailable()) { + continue; + } + + const edgeAvailable = edge.itemAmount.getAvailable(); + const ratio = edgeAvailable / totalAvailable; + const relativeEdgeAmount = ratio * inputAmount; + const consumed = edge.itemAmount.increaseConsumed(relativeEdgeAmount); + } + } + + for (const edge of node.getEdgesIn()) { + if ( edge.from.isAvailable() + && edge.to !== edge.from + && edge.from instanceof RecipeNode) + { + this.indent += 1; + this.updateNodeCompletion(edge.from); + this.indent -= 1; + } + } + } +} diff --git a/src/Tools/Production/Result/CalcHighlight.ts b/src/Tools/Production/Result/CalcHighlight.ts new file mode 100644 index 00000000..a9358cdf --- /dev/null +++ b/src/Tools/Production/Result/CalcHighlight.ts @@ -0,0 +1,228 @@ +import { Numbers } from '@src/Utils/Numbers'; +import { CalcCompleted } from './CalcCompleted'; +import { GraphNode, HighlightState } from './Nodes/GraphNode'; +import { RecipeNode } from './Nodes/RecipeNode'; + +export class CalcHighlight extends CalcCompleted { + private cache: NodeCache = { }; + + public set(node: GraphNode) { + if (this.graph.highlightedNode === node) { + this.graph.highlightedNode = undefined; + } else { + this.graph.highlightedNode = node; + } + + this.update(); + } + + public update() { + let limit = 0; + if (this.graph.highlightedNode instanceof RecipeNode) { + limit = this.graph.highlightedNode.limit || this.graph.highlightedNode.recipeData.amount; + } + + super.resetConsumed(); + + if (!this.graph.highlightedNode) { + for (const n of this.graph.nodes) { + n.highlighted = undefined; + } + } else { + for (const n of this.graph.nodes) { + this.cache[n.id] = this.cache[n.id] || {}; + const cache = this.cache[n.id]; + cache.highlighted = n.highlighted; + + n.highlighted = 'unrelated'; + } + + this.graph.highlightedNode.highlighted = 'highlighted'; + + this.setHighlighted(this.graph.highlightedNode, true, true); + + if (this.graph.settings.showHighlightLimits) { + if (this.graph.highlightedNode instanceof RecipeNode) { + this.setNodeLimit(this.graph.highlightedNode, limit); + } + + if (this.graph.settings.showHighlightDependents) { + for (const node of this.graph.nodes) { + if (node instanceof RecipeNode) { + const recipeNode = node as RecipeNode; + this.setNodeLimit(node, node.recipeData.amount); + } + } + } + } + } + + super.updateInternal(); + super.updateVisibility(); + } + + private setHighlighted(node: GraphNode, showDependencies: boolean, showDependents: boolean): void { + this.cache[node.id] = this.cache[node.id] || { } + + const cache = this.cache[node.id]; + + if ( (!showDependents || (showDependents === cache.showDependents)) + && (!showDependencies || (showDependencies === cache.showDependencies))) { + return; + } + + if (showDependents) { + cache.showDependents = true; + + for (const edge of node.getEdgesOut()) { + if (edge.to.highlighted === 'unrelated') { + const oldState = this.cache[edge.to.id]?.highlighted; + + if (node.highlighted === 'highlighted' + && (oldState === undefined + || oldState === 'dependency' + || oldState === 'highlighted' + || oldState === 'product')) + { + edge.to.highlighted = 'product'; + } else if (this.graph.settings.showHighlightDependents) { + edge.to.highlighted = 'dependent'; + } + } + + this.setHighlighted(edge.to, false, false); + } + } + + if (showDependencies) { + cache.showDependencies = true; + + for (const edge of node.getEdgesIn()) { + if (edge.from.highlighted === 'unrelated' || edge.from.highlighted === 'dependent') { + edge.from.highlighted = 'dependency'; + } + + this.setHighlighted(edge.from, true, edge.from instanceof RecipeNode); + } + } + } + + private setNodeLimit(node: RecipeNode, limit: number): void { + const diff = limit - (node.limit || 0); + if (Numbers.floor(diff) <= 0) { + return; + } + + node.limit = limit; + + const multiplier = node.getMultiplier(diff); + + if (node.highlighted === 'highlighted' || node.highlighted === 'dependent' || node.highlighted === 'dependency') { + for (const input of node.getInputs()) { + const ingredient = node.recipeData.recipe.ingredients.find((x) => x.item === input.resource.className); + if (!ingredient) { + continue; + } + + let inputAmount = ingredient.amount * multiplier; + + for (const edge of node.getEdgesIn(input.resource.className)) { + if (!edge.from.isAvailable()) { + continue; + } + + const consumed = edge.itemAmount.increaseLimit(inputAmount); + inputAmount -= consumed; + } + } + + for (const edge of node.getEdgesIn()) { + if (edge.from instanceof RecipeNode) { + this.updateNodeLimit(edge.from); + } + } + } + + if (node.highlighted === 'highlighted') { + for (const output of node.getOutputs()) { + const product = node.recipeData.recipe.products.find((x) => x.item === output.resource.className); + if (!product) { + continue; + } + + let outputAmount = product.amount * multiplier; + + for (const edge of node.getEdgesOut(output.resource.className)) { + if (!edge.to.isAvailable()) { + continue; + } + + outputAmount -= edge.itemAmount.increaseLimit(outputAmount); + } + } + + for (const edge of node.getEdgesOut()) { + if (edge.to instanceof RecipeNode) { + this.updateNodeLimit(edge.to); + } + } + } + } + + private updateNodeLimit(node: RecipeNode): void { + if (this.cache[node.id]?.visited) { + return; + } + + this.cache[node.id] = this.cache[node.id] || {}; + const cache = this.cache[node.id]; + cache.visited = true; + + if (node.highlighted === 'product') { + const inputsUsed = node.getInputs().map((input) => { + const limit = node + .getEdgesIn(input.resource.className) + .map((edge) => edge.from.isAvailable() + ? (edge.itemAmount.limit || 0) + : edge.itemAmount.amount) + .reduce((acc, sum) => acc + sum, 0); + const total = input.maxAmount; + + return limit / total; + }); + const inputUsed = Math.min(...inputsUsed); + const inputLimit = inputUsed * node.recipeData.amount; + + this.setNodeLimit(node, inputLimit); + } else if (node.highlighted === 'dependency') { + const outputsUsed = node.getOutputs().map((output) => { + const limit = node + .getEdgesOut(output.resource.className) + .map((edge) => edge.to.isAvailable() + ? (edge.itemAmount.limit || 0) + : 0) + .reduce((acc, sum) => acc + sum, 0); + const total = output.maxAmount; + + return limit / total; + }); + const outputUsed = Math.max(...outputsUsed); + const outputLimit = outputUsed * node.recipeData.amount; + + this.setNodeLimit(node, outputLimit); + } + + cache.visited = false; + } +} + +interface NodeState { + visited?: boolean, + highlighted?: HighlightState, + showDependents?: boolean; + showDependencies?: boolean; +} + +interface NodeCache { + [key:number]: NodeState; +} diff --git a/src/Tools/Production/Result/Edges/GraphEdge.ts b/src/Tools/Production/Result/Edges/GraphEdge.ts index a9e85eee..1c351000 100644 --- a/src/Tools/Production/Result/Edges/GraphEdge.ts +++ b/src/Tools/Production/Result/Edges/GraphEdge.ts @@ -1,5 +1,7 @@ +import model from '@src/Data/Model'; import {GraphNode} from '@src/Tools/Production/Result/Nodes/GraphNode'; import {ItemAmount} from '@src/Tools/Production/Result/ItemAmount'; +import { Strings } from '@src/Utils/Strings'; export class GraphEdge { @@ -8,8 +10,26 @@ export class GraphEdge public constructor(public readonly from: GraphNode, public readonly to: GraphNode, public readonly itemAmount: ItemAmount) { - from.connectedEdges.push(this); - to.connectedEdges.push(this); + if (this.to === this.from) { + this.to.connectedEdges.push(this); + } else { + this.from.connectedEdges.push(this); + this.to.connectedEdges.push(this); + } + } + + public getText(): string { + const missing = this.itemAmount.getAvailable(); + const amount = Strings.formatNumber(this.itemAmount.getAmount()); + const amountText = this.itemAmount.consumed + ? Strings.formatNumber(missing) + ' of ' + amount + : amount; + + return model.getItem(this.itemAmount.item).prototype.name + '\n' + amountText + ' / min'; + } + + public isLoop(): boolean { + return this.to.hasOutputTo(this.from); } } diff --git a/src/Tools/Production/Result/Graph.ts b/src/Tools/Production/Result/Graph.ts index 3f6b30c8..e0a46d9b 100644 --- a/src/Tools/Production/Result/Graph.ts +++ b/src/Tools/Production/Result/Graph.ts @@ -1,20 +1,39 @@ import {GraphNode} from '@src/Tools/Production/Result/Nodes/GraphNode'; import {GraphEdge} from '@src/Tools/Production/Result/Edges/GraphEdge'; import {ItemAmount} from '@src/Tools/Production/Result/ItemAmount'; +import {IProductionDataRequestCompleted} from '@src/Tools/Production/IProductionData'; +import {CalcHighlight} from '@src/Tools/Production/Result/CalcHighlight'; +import {Numbers} from '@src/Utils/Numbers'; + +export interface GraphSettings { + applyCompleted: boolean, + showCompleted: boolean, + showHighlightDependents: boolean, + showHighlightLimits: boolean, +} export class Graph { - public readonly DELTA = 1e-8; - public nodes: GraphNode[] = []; public edges: GraphEdge[] = []; + public completedMap: CompletedMap = { }; + public highlightedNode?: GraphNode; private lastId = 1; + private outputToNodeMap?: ItemToNodeMap; + + public constructor(public settings: GraphSettings) { } + + public setSettings(settings: GraphSettings) { + this.settings = settings; + this.recalculate(); + } public addNode(node: GraphNode): void { this.nodes.push(node); + this.outputToNodeMap = undefined; node.id = this.lastId++; } @@ -24,36 +43,99 @@ export class Graph edge.id = this.lastId++; } - public generateEdges(): void + public highlight(node: GraphNode) { + new CalcHighlight(this).set(node); + } + + public generateEdges(completed: IProductionDataRequestCompleted[]): void { - for (const nodeOut of this.nodes) { - outputLoop: - for (const output of nodeOut.getOutputs()) { - - for (const nodeIn of this.nodes) { - for (const input of nodeIn.getInputs()) { - if (input.resource === output.resource && input.amount < input.maxAmount) { - const diff = Math.min(input.maxAmount - input.amount, output.amount); - - output.amount -= diff; - input.amount += diff; - if (Math.abs(input.maxAmount - input.amount) < this.DELTA) { - input.amount = input.maxAmount; - } - if (Math.abs(output.amount) < this.DELTA) { - output.amount = 0; - } + this.edges = []; + this.completedMap = { }; - this.addEdge(new GraphEdge(nodeOut, nodeIn, new ItemAmount(output.resource.className, diff))); + const outputToNodeMap = this.getOutputToNodeMap(); - if (output.amount === 0) { - continue outputLoop; + for (const item of completed) { + if (item.recipe) { + this.completedMap[item.recipe] = (this.completedMap[item.recipe] || 0) + item.amount; + } + } + + for (const checkSharedResources of [true, false]) { + for (const nodeIn of this.nodes) { + for (const input of nodeIn.getInputs()) { + const nodesOut = outputToNodeMap[input.resource.className]; + for (const nodeOut of nodesOut) { + if (checkSharedResources && !hasSharedResources(nodeIn, nodeOut)) { + continue; + } + + for (const output of nodeOut.getOutputs()) { + if (input.resource === output.resource && input.amount < input.maxAmount) { + const diff = Numbers.round(Math.min(input.maxAmount - input.amount, output.amount)); + + if (diff <= 0) { + continue; + } + + output.decrease(diff); + input.increase(diff); + + this.addEdge(new GraphEdge(nodeOut, nodeIn, new ItemAmount(output.resource.className, diff))); } } } } } } + + this.recalculate(); + } + + private recalculate() { + new CalcHighlight(this).update(); + } + + private getOutputToNodeMap(): ItemToNodeMap { + if (!this.outputToNodeMap) { + this.outputToNodeMap = { }; + + for (const node of this.nodes) { + for (const output of node.getOutputs()) { + const className = output.resource.className; + const nodeList = this.outputToNodeMap[className] || []; + nodeList.push(node); + this.outputToNodeMap[className] = nodeList; + } + } + } + + return this.outputToNodeMap; + } + +} + +function hasSharedResources(nodeIn: GraphNode, nodeOut: GraphNode): boolean { + let sharedInputs = 0; + let sharedOutputs = 0; + + for (const input of nodeIn.getInputs()) { + for (const output of nodeOut.getOutputs()) { + if (input.resource === output.resource) { + ++sharedInputs; + } + } } + for (const output of nodeIn.getOutputs()) { + for (const input of nodeOut.getInputs()) { + if (input.resource === output.resource) { + ++sharedOutputs; + } + } + } + + return (sharedInputs > 0) && (sharedOutputs > 0); } + +interface ItemToNodeMap { [key:string]: GraphNode[] } +interface CompletedMap { [key:string]: number } diff --git a/src/Tools/Production/Result/ItemAmount.ts b/src/Tools/Production/Result/ItemAmount.ts index 5683e703..ac84cec9 100644 --- a/src/Tools/Production/Result/ItemAmount.ts +++ b/src/Tools/Production/Result/ItemAmount.ts @@ -1,9 +1,49 @@ +import { Numbers } from '@src/Utils/Numbers'; + export class ItemAmount { + public consumed: number = 0.0; + public limit?: number; + public constructor(public readonly item: string, public amount: number) { - // + } + + public increaseConsumed(diff: number): number { + const available = this.getAvailable(); + const maxDiff = Math.min(available, diff); + + this.consumed = Numbers.round(this.consumed + maxDiff); + + return maxDiff; + } + + public increaseLimit(diff: number): number { + const buffer = this.getBuffer(); + const maxDiff = Math.min(buffer, diff); + + this.limit = Numbers.round((this.limit || 0) + maxDiff); + + return maxDiff; + } + + public getConsumed(): number { + return this.consumed; + } + + public getAvailable(): number { + return Numbers.round(this.getAmount() - this.consumed); + } + + public getBuffer(): number { + return Numbers.round(this.amount - (this.limit || 0)); + } + + public getAmount(): number { + return this.limit === undefined + ? this.amount + : this.limit; } } diff --git a/src/Tools/Production/Result/Nodes/GraphNode.ts b/src/Tools/Production/Result/Nodes/GraphNode.ts index 8e60314d..6a48be6a 100644 --- a/src/Tools/Production/Result/Nodes/GraphNode.ts +++ b/src/Tools/Production/Result/Nodes/GraphNode.ts @@ -2,10 +2,14 @@ import {ResourceAmount} from '@src/Tools/Production/Result/ResourceAmount'; import {GraphEdge} from '@src/Tools/Production/Result/Edges/GraphEdge'; import {IVisNode} from '@src/Tools/Production/Result/IVisNode'; +export type HighlightState = 'highlighted'|'dependency'|'dependent'|'product'|'unrelated'; + export abstract class GraphNode { public id: number; + public visible: boolean = true; + public highlighted?: HighlightState; public connectedEdges: GraphEdge[] = []; @@ -17,16 +21,28 @@ export abstract class GraphNode public abstract getVisNode(): IVisNode; - public hasOutputTo(target: GraphNode): boolean + public hasOutputTo(target: GraphNode, filter?: string): boolean { for (const edge of this.connectedEdges) { - if (edge.from === this && edge.to === target) { + if (edge.from === this && edge.to === target && (!filter || edge.itemAmount.item === filter)) { return true; } } return false; } + public getEdgesOut(filter?: string): GraphEdge[] { + return this.connectedEdges.filter((edge) => edge.from === this && (!filter || edge.itemAmount.item === filter)); + } + + public getEdgesIn(filter?: string): GraphEdge[] { + return this.connectedEdges.filter((edge) => edge.to === this && (!filter || edge.itemAmount.item === filter)); + } + + public isAvailable(): boolean { + return this.highlighted !== 'unrelated'; + } + protected formatText(text: string, bold: boolean = true) { const parts = text.split(' '); diff --git a/src/Tools/Production/Result/Nodes/RecipeNode.ts b/src/Tools/Production/Result/Nodes/RecipeNode.ts index fd821382..025e973c 100644 --- a/src/Tools/Production/Result/Nodes/RecipeNode.ts +++ b/src/Tools/Production/Result/Nodes/RecipeNode.ts @@ -10,14 +10,18 @@ import {Numbers} from '@src/Utils/Numbers'; export class RecipeNode extends GraphNode { + public readonly DELTA = 1e-8; + public ingredients: ResourceAmount[] = []; public products: ResourceAmount[] = []; public machineData: MachineGroup; + public completed: number; + public limit?: number; public constructor(public readonly recipeData: RecipeData, data: IJsonSchema) { super(); - const multiplier = this.getMultiplier(); + const multiplier = this.getMultiplier(this.recipeData.amount); for (const ingredient of recipeData.recipe.ingredients) { this.ingredients.push(new ResourceAmount(data.items[ingredient.item], ingredient.amount * multiplier, 0)); } @@ -27,6 +31,12 @@ export class RecipeNode extends GraphNode this.machineData = new MachineGroup(this.recipeData); } + public getAmount(): number { + return this.limit === undefined + ? this.recipeData.amount + : this.limit; + } + public getInputs(): ResourceAmount[] { return this.ingredients; @@ -39,7 +49,13 @@ export class RecipeNode extends GraphNode public getTitle(): string { - return this.formatText(this.recipeData.recipe.name) + '\n' + Strings.formatNumber(this.recipeData.amount) + 'x ' + this.recipeData.machine.name; + const missing = this.getAmount() - this.completed; + const amount = Strings.formatNumber(this.getAmount()); + const amountText = this.completed + ? Strings.formatNumber(missing) + ' of ' + amount + : amount; + + return this.formatText(this.recipeData.recipe.name) + '\n' + amountText + 'x ' + this.recipeData.machine.name; } public getTooltip(): string|null @@ -65,29 +81,55 @@ export class RecipeNode extends GraphNode public getVisNode(): IVisNode { + const alpha = Math.abs(this.getAmount() - this.completed) < this.DELTA + ? '0.25' + : '1.0'; + + const border = this.highlighted === 'highlighted' + ? 'rgba(80, 160, 80, 1)' + : 'rgba(0, 0, 0, 0)'; + + const color = + this.highlighted === 'dependent' ? { + border: 'rgba(0, 0, 0, 0)', + background: `rgba(27, 112, 137, ${alpha})`, + highlight: { + border: 'rgba(238, 238, 238, 1)', + background: 'rgba(38, 159, 194, 1)', + }, + } : + this.highlighted === 'product' ? { + border: 'rgba(0, 0, 0, 0)', + background: `rgba(80, 160, 80, ${alpha})`, + highlight: { + border: 'rgba(238, 238, 238, 1)', + background: 'rgba(111, 182, 111, 1)', + }, + } : { + border: border, + background: `rgba(223, 105, 26, ${alpha})`, + highlight: { + border: 'rgba(238, 238, 238, 1)', + background: 'rgba(231, 122, 49, 1)', + }, + }; + const el = document.createElement('div'); el.innerHTML = this.getTooltip() || ''; return { id: this.id, label: this.getTitle(), title: el as unknown as string, - color: { - border: 'rgba(0, 0, 0, 0)', - background: 'rgba(223, 105, 26, 1)', - highlight: { - border: 'rgba(238, 238, 238, 1)', - background: 'rgba(231, 122, 49, 1)', - }, - }, + color: color, font: { color: 'rgba(238, 238, 238, 1)', }, }; } - private getMultiplier(): number + public getMultiplier(amount: number): number { - return this.recipeData.amount * this.recipeData.machine.metadata.manufacturingSpeed * (this.recipeData.clockSpeed / 100) * (60 / this.recipeData.recipe.time); + return amount * this.recipeData.machine.metadata.manufacturingSpeed * (this.recipeData.clockSpeed / 100) * (60 / this.recipeData.recipe.time); } } diff --git a/src/Tools/Production/Result/ProductionResultFactory.ts b/src/Tools/Production/Result/ProductionResultFactory.ts index 3fc360f3..0cb761fc 100644 --- a/src/Tools/Production/Result/ProductionResultFactory.ts +++ b/src/Tools/Production/Result/ProductionResultFactory.ts @@ -7,21 +7,21 @@ import {MinerNode} from '@src/Tools/Production/Result/Nodes/MinerNode'; import {ProductNode} from '@src/Tools/Production/Result/Nodes/ProductNode'; import {ByproductNode} from '@src/Tools/Production/Result/Nodes/ByproductNode'; import {InputNode} from '@src/Tools/Production/Result/Nodes/InputNode'; -import {Graph} from '@src/Tools/Production/Result/Graph'; +import {Graph, GraphSettings} from '@src/Tools/Production/Result/Graph'; import {ProductionResult} from '@src/Tools/Production/Result/ProductionResult'; import {SinkNode} from '@src/Tools/Production/Result/Nodes/SinkNode'; export class ProductionResultFactory { - public create(request: IProductionDataApiRequest, response: IProductionDataApiResponse, data: IJsonSchema): ProductionResult + public create(settings: GraphSettings, request: IProductionDataApiRequest, response: IProductionDataApiResponse, data: IJsonSchema): ProductionResult { - return new ProductionResult(request, ProductionResultFactory.createGraph(response, data), data); + return new ProductionResult(request, ProductionResultFactory.createGraph(settings, request, response, data), data); } - private static createGraph(response: IProductionDataApiResponse, data: IJsonSchema): Graph + private static createGraph(settings: GraphSettings, request: IProductionDataApiRequest, response: IProductionDataApiResponse, data: IJsonSchema): Graph { - const graph = new Graph; + const graph = new Graph(settings); for (const recipeData in response) { if (!response.hasOwnProperty(recipeData)) { @@ -83,7 +83,7 @@ export class ProductionResultFactory } } - graph.generateEdges(); + graph.generateEdges(request.completed || []); return graph; } diff --git a/src/Tools/Production/Result/ResourceAmount.ts b/src/Tools/Production/Result/ResourceAmount.ts index ae524f8f..33db31b8 100644 --- a/src/Tools/Production/Result/ResourceAmount.ts +++ b/src/Tools/Production/Result/ResourceAmount.ts @@ -1,4 +1,5 @@ import {IItemSchema} from '@src/Schema/IItemSchema'; +import { Numbers } from '@src/Utils/Numbers'; export class ResourceAmount { @@ -7,4 +8,12 @@ export class ResourceAmount { } + public increase(diff: number) { + this.amount = Numbers.round(this.amount + diff); + } + + public decrease(diff: number) { + this.amount = Numbers.round(this.amount - diff); + } + } diff --git a/src/Utils/Numbers.ts b/src/Utils/Numbers.ts index 47cb2c86..2a7e200f 100644 --- a/src/Utils/Numbers.ts +++ b/src/Utils/Numbers.ts @@ -1,17 +1,17 @@ export class Numbers { - public static round(num: number, decimals: number = 3): number + public static round(num: number, decimals: number = 2): number { return Math.round((num + Number.EPSILON) * Math.pow(10, decimals)) / Math.pow(10, decimals); } - public static ceil(num: number, decimals: number = 3): number + public static ceil(num: number, decimals: number = 2): number { return Math.ceil((num + Number.EPSILON) * Math.pow(10, decimals)) / Math.pow(10, decimals); } - public static floor(num: number, decimals: number = 3): number + public static floor(num: number, decimals: number = 2): number { return Math.floor((num + Number.EPSILON) * Math.pow(10, decimals)) / Math.pow(10, decimals); } diff --git a/styles/production.scss b/styles/production.scss index 2776377b..5e81a5fe 100644 --- a/styles/production.scss +++ b/styles/production.scss @@ -84,7 +84,12 @@ $border: 1px solid rgba(0, 0, 0, 0.125); } } - .input-table { + .completed-card { + margin-top: 15px; + } + + .input-table, + .completed-table { width: 100%; tr { @@ -264,6 +269,12 @@ $border: 1px solid rgba(0, 0, 0, 0.125); } } +.visualization-settings { + tr { + cursor: pointer; + } +} + .alternate-recipe-list { width: 100%; diff --git a/templates/Controllers/production.html b/templates/Controllers/production.html index 0e4a6b5a..e6609fc3 100644 --- a/templates/Controllers/production.html +++ b/templates/Controllers/production.html @@ -366,7 +366,65 @@

- + + + + + + + + + + +
+

+ + Completed recipes + +

+
+

+ Provide a list of recipes that are already constructed in your factory. The amount of already implemented factories (including their ingredients), will be hidden in the visualization. +

+ + + + + + + + + + + + + @@ -846,9 +904,58 @@

- You can move the nodes around using drag'n'drop. +

You can move the nodes around using drag'n'drop.

+

Double click a node to highlight it. In highlight mode only elements and dependencies related to this node is shown.

+ +

+ + + + + {{$select.selected.name}} + + + + + + + + + + + + + + +
+ + + Add factory + + +
+ + + + + + + + + + + + + + + + +
+ + + + + Apply completed recipes. + +
+ + + + + Show completed nodes. + +
+ + + + + Show the dependents of a recipe in highlight mode. + +
+ + + + + Show only resources needed for the current selection in highlight mode. + +
- + +
From 1cbfe64d3426c0c0754b041bf8539f302f6f11fd Mon Sep 17 00:00:00 2001 From: Bergmann89 Date: Wed, 5 Feb 2025 20:20:04 +0100 Subject: [PATCH 3/9] Manually disable nodes and some few bug fixes --- .../VisualizationComponentController.ts | 6 +++- src/Tools/Production/Result/CalcCompleted.ts | 2 +- src/Tools/Production/Result/CalcHighlight.ts | 30 +++++++++++++------ src/Tools/Production/Result/Graph.ts | 5 ++++ .../Production/Result/Nodes/GraphNode.ts | 3 +- .../Production/Result/Nodes/RecipeNode.ts | 4 +-- 6 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/Module/Components/VisualizationComponentController.ts b/src/Module/Components/VisualizationComponentController.ts index 90178fe5..6b99ef86 100644 --- a/src/Module/Components/VisualizationComponentController.ts +++ b/src/Module/Components/VisualizationComponentController.ts @@ -288,7 +288,11 @@ export class VisualizationComponentController implements IController (event.nodes as number[]).forEach((nodeId) => { const node = this.result.graph.nodes.find((graphNode) => graphNode.id === nodeId); if (node) { - this.result.graph.highlight(node); + if (event.event?.srcEvent?.ctrlKey) { + this.result.graph.toogleNode(node); + } else { + this.result.graph.highlight(node); + } this.updateData(this.result); } }); diff --git a/src/Tools/Production/Result/CalcCompleted.ts b/src/Tools/Production/Result/CalcCompleted.ts index 308cb658..6839ad72 100644 --- a/src/Tools/Production/Result/CalcCompleted.ts +++ b/src/Tools/Production/Result/CalcCompleted.ts @@ -49,7 +49,7 @@ export class CalcCompleted { .map((x) => x.itemAmount.getAvailable()) .reduce((acc, sum) => acc + sum, 0)); - node.visible = node.isAvailable() + node.visible = node.highlighted !== 'unrelated' && (this.graph.settings.showCompleted || output > 0 || node instanceof ProductNode diff --git a/src/Tools/Production/Result/CalcHighlight.ts b/src/Tools/Production/Result/CalcHighlight.ts index a9358cdf..a7ca4f89 100644 --- a/src/Tools/Production/Result/CalcHighlight.ts +++ b/src/Tools/Production/Result/CalcHighlight.ts @@ -9,23 +9,31 @@ export class CalcHighlight extends CalcCompleted { public set(node: GraphNode) { if (this.graph.highlightedNode === node) { this.graph.highlightedNode = undefined; + this.graph.highlightedLimit = undefined; } else { this.graph.highlightedNode = node; + + if (this.graph.highlightedNode instanceof RecipeNode) { + const recipeNode = (node as RecipeNode); + this.graph.highlightedLimit = recipeNode.limit || recipeNode.recipeData.amount; + } } this.update(); } - public update() { - let limit = 0; - if (this.graph.highlightedNode instanceof RecipeNode) { - limit = this.graph.highlightedNode.limit || this.graph.highlightedNode.recipeData.amount; - } + public toggle(node: GraphNode) { + node.userIgnore = !node.userIgnore; + this.update(); + } + + public update() { super.resetConsumed(); if (!this.graph.highlightedNode) { for (const n of this.graph.nodes) { + n.userIgnore = false; n.highlighted = undefined; } } else { @@ -43,6 +51,8 @@ export class CalcHighlight extends CalcCompleted { if (this.graph.settings.showHighlightLimits) { if (this.graph.highlightedNode instanceof RecipeNode) { + const recipeNode = this.graph.highlightedNode as RecipeNode; + const limit = this.graph.highlightedLimit || recipeNode.limit || 0; this.setNodeLimit(this.graph.highlightedNode, limit); } @@ -50,7 +60,9 @@ export class CalcHighlight extends CalcCompleted { for (const node of this.graph.nodes) { if (node instanceof RecipeNode) { const recipeNode = node as RecipeNode; - this.setNodeLimit(node, node.recipeData.amount); + if (recipeNode.highlighted === 'dependent') { + this.setNodeLimit(node, node.recipeData.amount); + } } } } @@ -109,7 +121,7 @@ export class CalcHighlight extends CalcCompleted { private setNodeLimit(node: RecipeNode, limit: number): void { const diff = limit - (node.limit || 0); - if (Numbers.floor(diff) <= 0) { + if (Numbers.floor(diff) <= 0 || node.userIgnore) { return; } @@ -153,7 +165,7 @@ export class CalcHighlight extends CalcCompleted { let outputAmount = product.amount * multiplier; for (const edge of node.getEdgesOut(output.resource.className)) { - if (!edge.to.isAvailable()) { + if (edge.to.highlighted !== 'product') { continue; } @@ -194,7 +206,7 @@ export class CalcHighlight extends CalcCompleted { const inputLimit = inputUsed * node.recipeData.amount; this.setNodeLimit(node, inputLimit); - } else if (node.highlighted === 'dependency') { + } else if (node.highlighted !== 'unrelated') { const outputsUsed = node.getOutputs().map((output) => { const limit = node .getEdgesOut(output.resource.className) diff --git a/src/Tools/Production/Result/Graph.ts b/src/Tools/Production/Result/Graph.ts index e0a46d9b..232af8ad 100644 --- a/src/Tools/Production/Result/Graph.ts +++ b/src/Tools/Production/Result/Graph.ts @@ -19,6 +19,7 @@ export class Graph public edges: GraphEdge[] = []; public completedMap: CompletedMap = { }; public highlightedNode?: GraphNode; + public highlightedLimit?: number; private lastId = 1; private outputToNodeMap?: ItemToNodeMap; @@ -47,6 +48,10 @@ export class Graph new CalcHighlight(this).set(node); } + public toogleNode(node: GraphNode) { + new CalcHighlight(this).toggle(node); + } + public generateEdges(completed: IProductionDataRequestCompleted[]): void { this.edges = []; diff --git a/src/Tools/Production/Result/Nodes/GraphNode.ts b/src/Tools/Production/Result/Nodes/GraphNode.ts index 6a48be6a..3db92db0 100644 --- a/src/Tools/Production/Result/Nodes/GraphNode.ts +++ b/src/Tools/Production/Result/Nodes/GraphNode.ts @@ -9,6 +9,7 @@ export abstract class GraphNode public id: number; public visible: boolean = true; + public userIgnore: boolean = false; public highlighted?: HighlightState; public connectedEdges: GraphEdge[] = []; @@ -40,7 +41,7 @@ export abstract class GraphNode } public isAvailable(): boolean { - return this.highlighted !== 'unrelated'; + return this.highlighted !== 'unrelated' && !this.userIgnore; } protected formatText(text: string, bold: boolean = true) diff --git a/src/Tools/Production/Result/Nodes/RecipeNode.ts b/src/Tools/Production/Result/Nodes/RecipeNode.ts index 025e973c..62e0cb51 100644 --- a/src/Tools/Production/Result/Nodes/RecipeNode.ts +++ b/src/Tools/Production/Result/Nodes/RecipeNode.ts @@ -10,8 +10,6 @@ import {Numbers} from '@src/Utils/Numbers'; export class RecipeNode extends GraphNode { - public readonly DELTA = 1e-8; - public ingredients: ResourceAmount[] = []; public products: ResourceAmount[] = []; public machineData: MachineGroup; @@ -81,7 +79,7 @@ export class RecipeNode extends GraphNode public getVisNode(): IVisNode { - const alpha = Math.abs(this.getAmount() - this.completed) < this.DELTA + const alpha = (Math.round(this.getAmount() - this.completed) <= 0) || this.userIgnore ? '0.25' : '1.0'; From 45d075003530cf81314bf91a7275094cba4621c1 Mon Sep 17 00:00:00 2001 From: Bergmann89 Date: Sun, 9 Feb 2025 18:44:12 +0100 Subject: [PATCH 4/9] Implement option to hide nodes disabled by the user --- src/Tools/Production/ProductionTab.ts | 5 +++++ src/Tools/Production/Result/CalcCompleted.ts | 1 + src/Tools/Production/Result/Graph.ts | 1 + templates/Controllers/production.html | 11 +++++++++++ 4 files changed, 18 insertions(+) diff --git a/src/Tools/Production/ProductionTab.ts b/src/Tools/Production/ProductionTab.ts index 0dac4465..a4d67438 100644 --- a/src/Tools/Production/ProductionTab.ts +++ b/src/Tools/Production/ProductionTab.ts @@ -41,6 +41,7 @@ export class ProductionTab showCompleted: true, showHighlightDependents: true, showHighlightLimits: true, + showDisabledNodes: true, }; public tab: string = 'production'; @@ -497,6 +498,10 @@ export class ProductionTab this.graphSettings.showHighlightLimits = !this.graphSettings.showHighlightLimits; } + public toggleShowDisabledNodes(): void { + this.graphSettings.showDisabledNodes = !this.graphSettings.showDisabledNodes; + } + public recipeMachineDisabled(recipe: IRecipeSchema): boolean { if (typeof this.data.request.blockedMachines === 'undefined') { diff --git a/src/Tools/Production/Result/CalcCompleted.ts b/src/Tools/Production/Result/CalcCompleted.ts index 6839ad72..0df72c30 100644 --- a/src/Tools/Production/Result/CalcCompleted.ts +++ b/src/Tools/Production/Result/CalcCompleted.ts @@ -50,6 +50,7 @@ export class CalcCompleted { .reduce((acc, sum) => acc + sum, 0)); node.visible = node.highlighted !== 'unrelated' + && (this.graph.settings.showDisabledNodes || !node.userIgnore) && (this.graph.settings.showCompleted || output > 0 || node instanceof ProductNode diff --git a/src/Tools/Production/Result/Graph.ts b/src/Tools/Production/Result/Graph.ts index 232af8ad..bc7187ed 100644 --- a/src/Tools/Production/Result/Graph.ts +++ b/src/Tools/Production/Result/Graph.ts @@ -10,6 +10,7 @@ export interface GraphSettings { showCompleted: boolean, showHighlightDependents: boolean, showHighlightLimits: boolean, + showDisabledNodes: boolean, } export class Graph diff --git a/templates/Controllers/production.html b/templates/Controllers/production.html index e6609fc3..30f80aa8 100644 --- a/templates/Controllers/production.html +++ b/templates/Controllers/production.html @@ -952,6 +952,17 @@

+ + + + + + + Show the nodes that were disabled by the user (ctrl + double click). + + +

From 2c3e9c9089aedfbce586f9ac3872f13558cdffcb Mon Sep 17 00:00:00 2001 From: Bergmann89 Date: Sun, 9 Feb 2025 19:04:45 +0100 Subject: [PATCH 5/9] Fix bug in node visibility calculation --- src/Tools/Production/Result/CalcCompleted.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Tools/Production/Result/CalcCompleted.ts b/src/Tools/Production/Result/CalcCompleted.ts index 0df72c30..5223e9ed 100644 --- a/src/Tools/Production/Result/CalcCompleted.ts +++ b/src/Tools/Production/Result/CalcCompleted.ts @@ -46,7 +46,9 @@ export class CalcCompleted { for (const node of this.graph.nodes) { const output = Numbers.round(node .getEdgesOut() - .map((x) => x.itemAmount.getAvailable()) + .map((x) => (x.to.highlighted === 'unrelated' || x.to.userIgnore) + ? 0.0 + : x.itemAmount.getAvailable()) .reduce((acc, sum) => acc + sum, 0)); node.visible = node.highlighted !== 'unrelated' From 45b9338a14ecb1832763ef877e2a5f26855839e6 Mon Sep 17 00:00:00 2001 From: Bergmann89 Date: Sun, 9 Feb 2025 19:45:02 +0100 Subject: [PATCH 6/9] Fix bug in dependency calculation --- src/Module/Components/VisualizationComponentController.ts | 2 +- src/Tools/Production/Result/CalcHighlight.ts | 4 ++-- src/Tools/Production/Result/Edges/GraphEdge.ts | 7 +++++++ src/Tools/Production/Result/Graph.ts | 5 ++++- 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Module/Components/VisualizationComponentController.ts b/src/Module/Components/VisualizationComponentController.ts index 6b99ef86..8f92dc7d 100644 --- a/src/Module/Components/VisualizationComponentController.ts +++ b/src/Module/Components/VisualizationComponentController.ts @@ -130,7 +130,7 @@ export class VisualizationComponentController implements IController } for (const edge of result.graph.edges) { - if (!edge.to.visible || !edge.from.visible) { + if (!edge.isAvailable()) { continue } diff --git a/src/Tools/Production/Result/CalcHighlight.ts b/src/Tools/Production/Result/CalcHighlight.ts index a7ca4f89..8342a5b7 100644 --- a/src/Tools/Production/Result/CalcHighlight.ts +++ b/src/Tools/Production/Result/CalcHighlight.ts @@ -47,7 +47,7 @@ export class CalcHighlight extends CalcCompleted { this.graph.highlightedNode.highlighted = 'highlighted'; - this.setHighlighted(this.graph.highlightedNode, true, true); + this.setHighlighted(this.graph.highlightedNode, true, this.graph.settings.showHighlightDependents); if (this.graph.settings.showHighlightLimits) { if (this.graph.highlightedNode instanceof RecipeNode) { @@ -114,7 +114,7 @@ export class CalcHighlight extends CalcCompleted { edge.from.highlighted = 'dependency'; } - this.setHighlighted(edge.from, true, edge.from instanceof RecipeNode); + this.setHighlighted(edge.from, true, this.graph.settings.showHighlightDependents && edge.from instanceof RecipeNode); } } } diff --git a/src/Tools/Production/Result/Edges/GraphEdge.ts b/src/Tools/Production/Result/Edges/GraphEdge.ts index 1c351000..18ed305b 100644 --- a/src/Tools/Production/Result/Edges/GraphEdge.ts +++ b/src/Tools/Production/Result/Edges/GraphEdge.ts @@ -2,6 +2,7 @@ import model from '@src/Data/Model'; import {GraphNode} from '@src/Tools/Production/Result/Nodes/GraphNode'; import {ItemAmount} from '@src/Tools/Production/Result/ItemAmount'; import { Strings } from '@src/Utils/Strings'; +import { Numbers } from '@src/Utils/Numbers'; export class GraphEdge { @@ -18,6 +19,12 @@ export class GraphEdge } } + public isAvailable(): boolean { + return this.to.visible + && this.from.visible + && (Numbers.round(this.itemAmount.getAmount()) > 0.0); + } + public getText(): string { const missing = this.itemAmount.getAvailable(); const amount = Strings.formatNumber(this.itemAmount.getAmount()); diff --git a/src/Tools/Production/Result/Graph.ts b/src/Tools/Production/Result/Graph.ts index bc7187ed..4280c001 100644 --- a/src/Tools/Production/Result/Graph.ts +++ b/src/Tools/Production/Result/Graph.ts @@ -76,7 +76,10 @@ export class Graph } for (const output of nodeOut.getOutputs()) { - if (input.resource === output.resource && input.amount < input.maxAmount) { + if ( input.resource === output.resource + && input.amount < input.maxAmount + && output.amount > 0.0) + { const diff = Numbers.round(Math.min(input.maxAmount - input.amount, output.amount)); if (diff <= 0) { From 9644582e11861e54fed24b21d2561b1f3f40ad16 Mon Sep 17 00:00:00 2001 From: Bergmann89 Date: Sun, 9 Feb 2025 19:58:07 +0100 Subject: [PATCH 7/9] Fix bug in nod visibility calculation --- src/Tools/Production/Result/CalcCompleted.ts | 1 + src/Tools/Production/Result/Nodes/RecipeNode.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/Tools/Production/Result/CalcCompleted.ts b/src/Tools/Production/Result/CalcCompleted.ts index 5223e9ed..354c0a0c 100644 --- a/src/Tools/Production/Result/CalcCompleted.ts +++ b/src/Tools/Production/Result/CalcCompleted.ts @@ -59,6 +59,7 @@ export class CalcCompleted { || node instanceof SinkNode || node instanceof ByproductNode || node.highlighted === 'product' + || node.highlighted === 'highlighted' || node.highlighted === 'dependent'); } } diff --git a/src/Tools/Production/Result/Nodes/RecipeNode.ts b/src/Tools/Production/Result/Nodes/RecipeNode.ts index 62e0cb51..d1f1ec9a 100644 --- a/src/Tools/Production/Result/Nodes/RecipeNode.ts +++ b/src/Tools/Production/Result/Nodes/RecipeNode.ts @@ -74,6 +74,8 @@ export class RecipeNode extends GraphNode title.push('OUT: ' + Strings.formatNumber(product.maxAmount) + ' / min - ' + product.resource.name); } + title.push(`Status: ${this.highlighted || '-'}`); + return title.join('
'); } From 78b98be96d9346c3f6050bbc2948f16c5956f201 Mon Sep 17 00:00:00 2001 From: Bergmann89 Date: Sun, 9 Feb 2025 20:25:00 +0100 Subject: [PATCH 8/9] Show dependents of the product node --- src/Tools/Production/Result/CalcHighlight.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Tools/Production/Result/CalcHighlight.ts b/src/Tools/Production/Result/CalcHighlight.ts index 8342a5b7..398352ec 100644 --- a/src/Tools/Production/Result/CalcHighlight.ts +++ b/src/Tools/Production/Result/CalcHighlight.ts @@ -47,7 +47,7 @@ export class CalcHighlight extends CalcCompleted { this.graph.highlightedNode.highlighted = 'highlighted'; - this.setHighlighted(this.graph.highlightedNode, true, this.graph.settings.showHighlightDependents); + this.setHighlighted(this.graph.highlightedNode, true, true); if (this.graph.settings.showHighlightLimits) { if (this.graph.highlightedNode instanceof RecipeNode) { @@ -102,7 +102,7 @@ export class CalcHighlight extends CalcCompleted { } } - this.setHighlighted(edge.to, false, false); + this.setHighlighted(edge.to, false, edge.to.highlighted === 'product'); } } @@ -129,7 +129,12 @@ export class CalcHighlight extends CalcCompleted { const multiplier = node.getMultiplier(diff); - if (node.highlighted === 'highlighted' || node.highlighted === 'dependent' || node.highlighted === 'dependency') { + if (node.highlighted === 'highlighted' + || node.highlighted === 'dependent' + || node.highlighted === 'dependency' + || (node.highlighted === 'product' + && this.graph.settings.showHighlightDependents)) + { for (const input of node.getInputs()) { const ingredient = node.recipeData.recipe.ingredients.find((x) => x.item === input.resource.className); if (!ingredient) { @@ -155,7 +160,8 @@ export class CalcHighlight extends CalcCompleted { } } - if (node.highlighted === 'highlighted') { + if (node.highlighted === 'highlighted' + && !this.graph.settings.showHighlightDependents) { for (const output of node.getOutputs()) { const product = node.recipeData.recipe.products.find((x) => x.item === output.resource.className); if (!product) { @@ -190,7 +196,7 @@ export class CalcHighlight extends CalcCompleted { const cache = this.cache[node.id]; cache.visited = true; - if (node.highlighted === 'product') { + if (node.highlighted === 'product' && !this.graph.settings.showHighlightDependents) { const inputsUsed = node.getInputs().map((input) => { const limit = node .getEdgesIn(input.resource.className) From 10accb1bd70f99abbe58c827a70a75fcdbb73687 Mon Sep 17 00:00:00 2001 From: Bergmann89 Date: Sun, 21 Sep 2025 22:50:19 +0200 Subject: [PATCH 9/9] Fix issue with user disabled nodes user disabled nodes did not work in the non-filtered / non-highlighted graph. --- src/Tools/Production/Result/CalcHighlight.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Tools/Production/Result/CalcHighlight.ts b/src/Tools/Production/Result/CalcHighlight.ts index 398352ec..cab87553 100644 --- a/src/Tools/Production/Result/CalcHighlight.ts +++ b/src/Tools/Production/Result/CalcHighlight.ts @@ -10,6 +10,10 @@ export class CalcHighlight extends CalcCompleted { if (this.graph.highlightedNode === node) { this.graph.highlightedNode = undefined; this.graph.highlightedLimit = undefined; + + for (const n of this.graph.nodes) { + n.userIgnore = false; + } } else { this.graph.highlightedNode = node; @@ -33,7 +37,6 @@ export class CalcHighlight extends CalcCompleted { if (!this.graph.highlightedNode) { for (const n of this.graph.nodes) { - n.userIgnore = false; n.highlighted = undefined; } } else {