Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/app/Components/Planner/ContextMenu/NodeContextMenu.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {faArrowsSplitUpAndLeft, faBan, faCheck, faCompress, faCrosshairs, faDiagramProject, faExpand, faFolderOpen, faLock, faLockOpen, faRotateLeft, faTrashCan, faXmark} from '@fortawesome/free-solid-svg-icons';
import {faArrowsSplitUpAndLeft, faBan, faCheck, faCompress, faCrosshairs, faDiagramProject, faExpand, faFolderOpen, faLock, faLockOpen, faRightToBracket, faRotateLeft, faTrashCan, faXmark} from '@fortawesome/free-solid-svg-icons';
import {ContextMenuItem} from '@src/Components/Planner/ContextMenu/ContextMenuItem';
import {NodeResizeOptions} from '@src/Components/Planner/ContextMenu/NodeResizeOptions';
import {NodeSplitOptions} from '@src/Components/Planner/ContextMenu/NodeSplitOptions';
Expand Down Expand Up @@ -130,6 +130,11 @@ export class NodeContextMenu extends PlannerContextMenu
hotkey: 'graph.disableMachine',
action: () => this.actions.requestMachineDisable(node.machine.className),
},
{
label: `Add to inputs (${node.recipe.products[0]?.item.name ?? 'main product'})`,
icon: faRightToBracket,
action: () => this.actions.requestInputAdd(node),
},
];
}

Expand Down
9 changes: 9 additions & 0 deletions src/app/Components/Planner/PlannerActionsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {SubplanScaleRequest} from '@src/Components/Planner/SubplanScaleRequest';
import {GraphEdge} from '@src/Model/Planner/Graph/GraphEdge';
import {GraphPoint} from '@src/Model/Planner/Graph/GraphPoint';
import {Node} from '@src/Model/Planner/Solver/Response/Node';
import {RecipeNode} from '@src/Model/Planner/Solver/Response/RecipeNode';

/** Panels are created via ngComponentOutlet and cannot use output bindings, so their actions reach PlannerComponent through here. */
@Injectable()
Expand Down Expand Up @@ -96,6 +97,9 @@ export class PlannerActionsService
private readonly inputRemoveSubject = new Subject<string>();
public readonly inputRemoveRequests: Observable<string> = this.inputRemoveSubject.asObservable();

private readonly inputAddSubject = new Subject<RecipeNode>();
public readonly inputAddRequests: Observable<RecipeNode> = this.inputAddSubject.asObservable();

private readonly undoSubject = new Subject<void>();
public readonly undoRequests: Observable<void> = this.undoSubject.asObservable();

Expand Down Expand Up @@ -241,6 +245,11 @@ export class PlannerActionsService
this.inputRemoveSubject.next(itemClassName);
}

public requestInputAdd(node: RecipeNode): void
{
this.inputAddSubject.next(node);
}

public requestUndo(): void
{
this.undoSubject.next();
Expand Down
27 changes: 27 additions & 0 deletions src/app/Components/Planner/PlannerComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import {ByproductNode} from '@src/Model/Planner/Solver/Response/ByproductNode';
import {InputNode} from '@src/Model/Planner/Solver/Response/InputNode';
import {Node} from '@src/Model/Planner/Solver/Response/Node';
import {ProductNode} from '@src/Model/Planner/Solver/Response/ProductNode';
import {RecipeNode} from '@src/Model/Planner/Solver/Response/RecipeNode';
import {SolverResponse} from '@src/Model/Planner/Solver/Response/SolverResponse';
import {SubplanNode} from '@src/Model/Planner/Solver/Response/SubplanNode';
import {SubplanBuildCountRequest} from '@src/Components/Planner/SubplanBuildCountRequest';
Expand Down Expand Up @@ -406,6 +407,10 @@ export class PlannerComponent implements AfterViewInit, OnDestroy, HotkeyItemSou
this.actions.inputRemoveRequests.subscribe(itemClassName => this.removeInput(itemClassName)),
);

this.subscription.add(
this.actions.inputAddRequests.subscribe(node => this.addInput(node)),
);

this.subscription.add(
toObservable(computed(() => {
const planGraph = this.planManager.activePlan()?.settings.graph;
Expand Down Expand Up @@ -1442,6 +1447,28 @@ export class PlannerComponent implements AfterViewInit, OnDestroy, HotkeyItemSou
this.planManager.setInputs(plan.id, plan.inputs.filter(input => input.itemClassName !== itemClassName));
}

/** Free, so the solver uses the input instead of making the item. The node is unlocked, or the next solve would keep it next to the input. */
private addInput(node: RecipeNode): void
{
const plan = this.planManager.activePlan();
const item = node.recipe.products[0]?.item;
if (!plan || !item) {
return;
}
const output = node.outputs
.filter(io => io.item.className === item.className)
.reduce((sum, io) => sum + io.maxAmount, 0);
// Rounded up, so solver noise (9.999999999375) reads cleanly in the Input tab and never leaves the input a sliver short.
const amount = Math.ceil(output * 10000 - 1e-6) / 10000;
if (node.locked) {
this.applyLockChange({nodeIds: [node.id], locked: false});
}
const existing = plan.inputs.find(input => input.itemClassName === item.className);
this.planManager.setInputs(plan.id, existing
? plan.inputs.map(input => input === existing ? {...input, amount: input.amount + amount} : input)
: [...plan.inputs, {itemClassName: item.className, amount, weight: 0}]);
}

/** Leaves the dirty flag as is, like manual node drags. */
private async relayoutGraph(): Promise<void>
{
Expand Down
15 changes: 8 additions & 7 deletions src/app/Model/Planner/ProductionSolverService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export class ProductionSolverService

private static readonly MAXIMISE_EPSILON = 0.001;

/** Far below any real weight (water: 0.01), far above the 1e-9 dual tolerance - it only breaks ties. */
private static readonly INPUT_TIE_BREAK = 1e-6;

public constructor(
private readonly solver: SolverService,
private readonly versionManager: VersionManager,
Expand Down Expand Up @@ -489,13 +492,11 @@ export class ProductionSolverService
}
})

if (request.optimisation.inputs) {
request.inputs.forEach(input => {
if (input.weight > 0) {
optimisation.push(input.weight + ' ' + input.item.className + '@Input');
}
});
}
// Never free, even at weight 0: byproducts cost nothing, so a free input the plan does not need could be drawn and dumped into one.
request.inputs.forEach(input => {
const weight = request.optimisation.inputs ? input.weight : 0;
optimisation.push((weight + ProductionSolverService.INPUT_TIE_BREAK) + ' ' + input.item.className + '@Input');
});

if (request.optimisation.power > 0 || request.optimisation.machines > 0) {
request.recipes.forEach(recipe => {
Expand Down