From 49d7318aae62b58e61e21b5379862f39d42456f0 Mon Sep 17 00:00:00 2001 From: Ravriely Date: Thu, 26 Mar 2026 17:52:51 +0100 Subject: [PATCH 1/7] feat: nodes stable key management --- src/Tools/Production/Result/Nodes/ByproductNode.ts | 5 +++++ src/Tools/Production/Result/Nodes/GraphNode.ts | 2 ++ src/Tools/Production/Result/Nodes/InputNode.ts | 5 +++++ src/Tools/Production/Result/Nodes/MinerNode.ts | 5 +++++ src/Tools/Production/Result/Nodes/ProductNode.ts | 5 +++++ src/Tools/Production/Result/Nodes/RecipeNode.ts | 5 +++++ src/Tools/Production/Result/Nodes/SinkNode.ts | 5 +++++ 7 files changed, 32 insertions(+) diff --git a/src/Tools/Production/Result/Nodes/ByproductNode.ts b/src/Tools/Production/Result/Nodes/ByproductNode.ts index 90b796e1..7a1ced13 100644 --- a/src/Tools/Production/Result/Nodes/ByproductNode.ts +++ b/src/Tools/Production/Result/Nodes/ByproductNode.ts @@ -39,6 +39,11 @@ export class ByproductNode extends GraphNode return null; } + public getStableKey(): string + { + return `byproduct:${this.resource.className}`; + } + public getVisNode(): IVisNode { return { diff --git a/src/Tools/Production/Result/Nodes/GraphNode.ts b/src/Tools/Production/Result/Nodes/GraphNode.ts index 8e60314d..51d208b9 100644 --- a/src/Tools/Production/Result/Nodes/GraphNode.ts +++ b/src/Tools/Production/Result/Nodes/GraphNode.ts @@ -17,6 +17,8 @@ export abstract class GraphNode public abstract getVisNode(): IVisNode; + public abstract getStableKey(): string; + public hasOutputTo(target: GraphNode): boolean { for (const edge of this.connectedEdges) { diff --git a/src/Tools/Production/Result/Nodes/InputNode.ts b/src/Tools/Production/Result/Nodes/InputNode.ts index f3ec3c91..35e0e461 100644 --- a/src/Tools/Production/Result/Nodes/InputNode.ts +++ b/src/Tools/Production/Result/Nodes/InputNode.ts @@ -39,6 +39,11 @@ export class InputNode extends GraphNode return null; } + public getStableKey(): string + { + return `input:${this.resource.className}`; + } + public getVisNode(): IVisNode { return { diff --git a/src/Tools/Production/Result/Nodes/MinerNode.ts b/src/Tools/Production/Result/Nodes/MinerNode.ts index c3283575..847dabec 100644 --- a/src/Tools/Production/Result/Nodes/MinerNode.ts +++ b/src/Tools/Production/Result/Nodes/MinerNode.ts @@ -39,6 +39,11 @@ export class MinerNode extends GraphNode return null; } + public getStableKey(): string + { + return `miner:${this.resource.className}`; + } + public getVisNode(): IVisNode { return { diff --git a/src/Tools/Production/Result/Nodes/ProductNode.ts b/src/Tools/Production/Result/Nodes/ProductNode.ts index 4773bbd4..c977ae92 100644 --- a/src/Tools/Production/Result/Nodes/ProductNode.ts +++ b/src/Tools/Production/Result/Nodes/ProductNode.ts @@ -39,6 +39,11 @@ export class ProductNode extends GraphNode return null; } + public getStableKey(): string + { + return `product:${this.resource.className}`; + } + public getVisNode(): IVisNode { return { diff --git a/src/Tools/Production/Result/Nodes/RecipeNode.ts b/src/Tools/Production/Result/Nodes/RecipeNode.ts index fd821382..9beaff36 100644 --- a/src/Tools/Production/Result/Nodes/RecipeNode.ts +++ b/src/Tools/Production/Result/Nodes/RecipeNode.ts @@ -85,6 +85,11 @@ export class RecipeNode extends GraphNode }; } + public getStableKey(): string + { + return `recipe:${this.recipeData.recipe.className}@${this.recipeData.machine.className}`; + } + private getMultiplier(): number { return this.recipeData.amount * this.recipeData.machine.metadata.manufacturingSpeed * (this.recipeData.clockSpeed / 100) * (60 / this.recipeData.recipe.time); diff --git a/src/Tools/Production/Result/Nodes/SinkNode.ts b/src/Tools/Production/Result/Nodes/SinkNode.ts index 4be9f3e5..212a2b77 100644 --- a/src/Tools/Production/Result/Nodes/SinkNode.ts +++ b/src/Tools/Production/Result/Nodes/SinkNode.ts @@ -39,6 +39,11 @@ export class SinkNode extends GraphNode return null; } + public getStableKey(): string + { + return `sink:${this.resource.className}`; + } + public getVisNode(): IVisNode { return { From 1c7c1d48eb4909b068daf19f2a10def5682b295b Mon Sep 17 00:00:00 2001 From: Ravriely Date: Thu, 26 Mar 2026 17:53:27 +0100 Subject: [PATCH 2/7] feat: edges stable key management --- src/Tools/Production/Result/Edges/GraphEdge.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Tools/Production/Result/Edges/GraphEdge.ts b/src/Tools/Production/Result/Edges/GraphEdge.ts index a9e85eee..6ff577dd 100644 --- a/src/Tools/Production/Result/Edges/GraphEdge.ts +++ b/src/Tools/Production/Result/Edges/GraphEdge.ts @@ -12,4 +12,8 @@ export class GraphEdge to.connectedEdges.push(this); } + public getStableKey(): string + { + return `edge:${this.from.getStableKey()}=>${this.to.getStableKey()}:${this.itemAmount.item}`; + } } From b8087a1075dffb4caffebc26ad3663645c56ac2b Mon Sep 17 00:00:00 2001 From: Ravriely Date: Thu, 26 Mar 2026 18:01:39 +0100 Subject: [PATCH 3/7] feat: persistent done state support through stable keys --- .../Components/VisualizationComponent.ts | 1 + .../VisualizationComponentController.ts | 250 ++++++++++++++++-- 2 files changed, 228 insertions(+), 23 deletions(-) diff --git a/src/Module/Components/VisualizationComponent.ts b/src/Module/Components/VisualizationComponent.ts index 06ff3ae2..127b41bf 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: '=', + storageKey: '@', }; } diff --git a/src/Module/Components/VisualizationComponentController.ts b/src/Module/Components/VisualizationComponentController.ts index 685d2706..f11eaeca 100644 --- a/src/Module/Components/VisualizationComponentController.ts +++ b/src/Module/Components/VisualizationComponentController.ts @@ -9,17 +9,50 @@ import {Strings} from '@src/Utils/Strings'; import model from '@src/Data/Model'; import {ProductionResult} from '@src/Tools/Production/Result/ProductionResult'; +const DONE_NODE_BORDER = 'rgba(180, 255, 180, 0.9)'; +const DONE_NODE_BORDER_HL = 'rgba(220, 255, 220, 1)'; +const DONE_NODE_BORDER_WIDTH = 3; +const DONE_NODE_BG_ALPHA = 0.35; +const DONE_FONT_ALPHA = 0.45; + +const DONE_EDGE_COLOR = 'rgba(105, 125, 145, 0.3)'; +const DONE_EDGE_COLOR_HL = 'rgba(134, 151, 167, 0.45)'; +const DONE_EDGE_FONT_COLOR = 'rgba(238, 238, 238, 0.3)'; + +function fadeRgba(rgba: string, alpha: number): string +{ + const m = rgba.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); + + if (!m) { + return rgba; + } + + return `rgba(${m[1]}, ${m[2]}, ${m[3]}, ${alpha})`; +} + +const STORAGE_KEY_PREFIX = 'doneNodes_'; + export class VisualizationComponentController implements IController { public result: ProductionResult; + public storageKey: string; + public static $inject = ['$element', '$scope', '$timeout']; private unregisterWatcherCallback: () => void; private network: Network; private fitted: boolean = false; + private doneKeys: Set = new Set(); + + private visNodes: DataSet; + private visEdges: DataSet; + + private nodeStyles: Map = new Map(); + private edgeStyles: Map = new Map(); + public constructor(private readonly $element: any, private readonly $scope: IScope, private readonly $timeout: ITimeoutService) {} @@ -37,6 +70,135 @@ export class VisualizationComponentController implements IController this.unregisterWatcherCallback(); } + private getPersistenceKey(): string + { + return STORAGE_KEY_PREFIX + (this.storageKey || 'default'); + } + + private loadDoneKeys(): void + { + try { + const raw = localStorage.getItem(this.getPersistenceKey()); + + this.doneKeys = raw ? new Set(JSON.parse(raw)) : new Set(); + } catch { + this.doneKeys = new Set(); + } + } + + private saveDoneKeys(): void + { + try { + localStorage.setItem(this.getPersistenceKey(), JSON.stringify([...this.doneKeys])); + } catch { /* storage full: silently ignore */ } + } + + private applyDoneNodeStyle(id: number, isDone: boolean): void + { + const meta = this.nodeStyles.get(id); + + if (!meta) { + return; + } + + if (isDone) { + const orig = meta.originalColor; + + this.visNodes.update({ + id, + color: { + border: DONE_NODE_BORDER, + background: fadeRgba(orig.background, DONE_NODE_BG_ALPHA), + highlight: { + border: DONE_NODE_BORDER_HL, + background: fadeRgba(orig.highlight.background, DONE_NODE_BG_ALPHA), + }, + }, + borderWidth: DONE_NODE_BORDER_WIDTH, + borderWidthSelected: DONE_NODE_BORDER_WIDTH + 1, + font: { + color: fadeRgba(meta.originalFont.color, DONE_FONT_ALPHA), + }, + } as any); + } else { + this.visNodes.update({ + id, + color: meta.originalColor, + borderWidth: 0, + borderWidthSelected: 1, + font: meta.originalFont, + } as any); + } + } + + private applyDoneEdgeStyle(id: number, isDone: boolean): void + { + const meta = this.edgeStyles.get(id); + if (!meta) { return; } + if (isDone) { + this.visEdges.update({ + id, + color: { + color: DONE_EDGE_COLOR, + highlight: DONE_EDGE_COLOR_HL, + }, + font: { + color: DONE_EDGE_FONT_COLOR, + }, + dashes: true, + } as any); + } else { + this.visEdges.update({ + id, + color: meta.originalColor, + font: meta.originalFont, + dashes: false, + } as any); + } + } + + private toggleNodeDone(visId: number): void + { + const meta = this.nodeStyles.get(visId); + + if (!meta) { + return; + } + + const key = meta.stableKey; + + if (this.doneKeys.has(key)) { + this.doneKeys.delete(key); + this.applyDoneNodeStyle(visId, false); + } else { + this.doneKeys.add(key); + this.applyDoneNodeStyle(visId, true); + } + + this.saveDoneKeys(); + } + + private toggleEdgeDone(visId: number): void + { + const meta = this.edgeStyles.get(visId); + + if (!meta) { + return; + } + + const key = meta.stableKey; + + if (this.doneKeys.has(key)) { + this.doneKeys.delete(key); + this.applyDoneEdgeStyle(visId, false); + } else { + this.doneKeys.add(key); + this.applyDoneEdgeStyle(visId, true); + } + + this.saveDoneKeys(); + } + public useCytoscape(result: ProductionResult): void { const options: cytoscape.CytoscapeOptions = { @@ -108,11 +270,24 @@ export class VisualizationComponentController implements IController public useVis(result: ProductionResult): void { - const nodes = new DataSet(); - const edges = new DataSet(); + this.loadDoneKeys(); + this.nodeStyles.clear(); + this.edgeStyles.clear(); + + this.visNodes = new DataSet(); + this.visEdges = new DataSet(); for (const node of result.graph.nodes) { - nodes.add(node.getVisNode()); + const visNode = node.getVisNode(); + + this.visNodes.add(visNode); + + // Store original style + stable key for later toggling. + this.nodeStyles.set(node.id, { + stableKey: node.getStableKey(), + originalColor: visNode.color ? { ...visNode.color, highlight: { ...visNode.color.highlight } } : undefined, + originalFont: visNode.font ? { ...visNode.font } : { color: 'rgba(238, 238, 238, 1)' }, + }); } for (const edge of result.graph.edges) { @@ -121,49 +296,78 @@ export class VisualizationComponentController implements IController }; if (edge.to.hasOutputTo(edge.from)) { - smooth.enabled = true; - smooth.type = 'curvedCW' + smooth.enabled = true; + smooth.type = 'curvedCW'; smooth.roundness = 0.2; } - edges.add({ - id: edge.id, - from: edge.from.id, - to: edge.to.id, + const edgeColor = { + color: 'rgba(105, 125, 145, 1)', + highlight: 'rgba(134, 151, 167, 1)', + }; + const edgeFont = { + color: 'rgba(238, 238, 238, 1)', + }; + + this.visEdges.add({ + 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', - color: { - color: 'rgba(105, 125, 145, 1)', - highlight: 'rgba(134, 151, 167, 1)', - }, - font: { - color: 'rgba(238, 238, 238, 1)', - }, - smooth: smooth, + color: edgeColor, + font: edgeFont, + smooth, } as any); + + this.edgeStyles.set(edge.id, { + stableKey: edge.getStableKey(), + originalColor: { ...edgeColor }, + originalFont: { ...edgeFont }, + }); } - this.network = this.drawVisualisation(nodes, edges); + // Apply persisted done state before the network is drawn. + this.nodeStyles.forEach(({ stableKey }, visId) => { + if (this.doneKeys.has(stableKey)) { + this.applyDoneNodeStyle(visId, true); + } + }); + this.edgeStyles.forEach(({ stableKey }, visId) => { + if (this.doneKeys.has(stableKey)) { + this.applyDoneEdgeStyle(visId, true); + } + }); + + this.network = this.drawVisualisation(this.visNodes, this.visEdges); + + this.network.on('doubleClick', (params) => { + if (params.nodes && params.nodes.length > 0) { + this.toggleNodeDone(params.nodes[0]); + } else if (params.edges && params.edges.length > 0) { + this.toggleEdgeDone(params.edges[0]); + } + }); this.$timeout(0).then(() => { const elkGraph: IElkGraph = { id: 'root', layoutOptions: { 'elk.algorithm': 'org.eclipse.elk.layered', - 'org.eclipse.elk.layered.nodePlacement.favorStraightEdges': true as unknown as string, // fuck off typescript + 'org.eclipse.elk.layered.nodePlacement.favorStraightEdges': true as unknown as string, 'org.eclipse.elk.spacing.nodeNode': 40 + '', }, children: [], edges: [], }; - nodes.forEach((node) => { + this.visNodes.forEach((node) => { elkGraph.children.push({ id: node.id.toString(), width: 250, height: 100, }); }); - edges.forEach((edge) => { + this.visEdges.forEach((edge) => { elkGraph.edges.push({ id: '', source: edge.from.toString(), @@ -174,12 +378,12 @@ export class VisualizationComponentController implements IController this.$timeout(0).then(() => { const elk = new ELK(); elk.layout(elkGraph).then((data) => { - nodes.forEach((node) => { + this.visNodes.forEach((node) => { const id = node.id; if (data.children) { for (const item of data.children) { if (parseInt(item.id, 10) === id) { - nodes.update({ + this.visNodes.update({ id: id, x: item.x, y: item.y, From f1d27c05f76d244de34815a7664b9bdeb8fcfb21 Mon Sep 17 00:00:00 2001 From: Ravriely Date: Thu, 26 Mar 2026 18:26:11 +0100 Subject: [PATCH 4/7] fix: node done border was too wide --- .../Components/VisualizationComponentController.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Module/Components/VisualizationComponentController.ts b/src/Module/Components/VisualizationComponentController.ts index f11eaeca..9e1bc4de 100644 --- a/src/Module/Components/VisualizationComponentController.ts +++ b/src/Module/Components/VisualizationComponentController.ts @@ -11,13 +11,13 @@ import {ProductionResult} from '@src/Tools/Production/Result/ProductionResult'; const DONE_NODE_BORDER = 'rgba(180, 255, 180, 0.9)'; const DONE_NODE_BORDER_HL = 'rgba(220, 255, 220, 1)'; -const DONE_NODE_BORDER_WIDTH = 3; +const DONE_NODE_BORDER_WIDTH = 1; const DONE_NODE_BG_ALPHA = 0.35; const DONE_FONT_ALPHA = 0.45; -const DONE_EDGE_COLOR = 'rgba(105, 125, 145, 0.3)'; -const DONE_EDGE_COLOR_HL = 'rgba(134, 151, 167, 0.45)'; -const DONE_EDGE_FONT_COLOR = 'rgba(238, 238, 238, 0.3)'; +const DONE_EDGE_COLOR = 'rgba(105, 125, 145, 0.3)'; +const DONE_EDGE_COLOR_HL = 'rgba(134, 151, 167, 0.45)'; +const DONE_EDGE_FONT_COLOR = 'rgba(238, 238, 238, 0.3)'; function fadeRgba(rgba: string, alpha: number): string { @@ -296,8 +296,8 @@ export class VisualizationComponentController implements IController }; if (edge.to.hasOutputTo(edge.from)) { - smooth.enabled = true; - smooth.type = 'curvedCW'; + smooth.enabled = true; + smooth.type = 'curvedCW'; smooth.roundness = 0.2; } From 8be9c634831d9958bf694f29288d0dca4a287c22 Mon Sep 17 00:00:00 2001 From: Ravriely Date: Thu, 26 Mar 2026 18:26:33 +0100 Subject: [PATCH 5/7] chore: bumped deprecated es5 to es2015 --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 9771680b..269aa23f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "esnext", "dom" ], - "target": "es5", + "target": "es2015", "paths": { "@src/*": ["./src/*"], "@data/*": ["./data/*"], From 8f368873d22792424b40f34085eac1c7c9f07ece Mon Sep 17 00:00:00 2001 From: Ravriely Date: Thu, 26 Mar 2026 18:52:03 +0100 Subject: [PATCH 6/7] fix: missing instruction that double clicking adds the done state --- templates/Controllers/production.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/Controllers/production.html b/templates/Controllers/production.html index 0e4a6b5a..e92d098c 100644 --- a/templates/Controllers/production.html +++ b/templates/Controllers/production.html @@ -846,7 +846,7 @@

- You can move the nodes around using drag'n'drop. + You can move the nodes around using drag'n'drop. Mark a node or an egde as "done" by double-clicking it.
From 0017207372063c82d92d07d9bba8c8dc5ba64357 Mon Sep 17 00:00:00 2001 From: Ravriely Date: Thu, 26 Mar 2026 23:36:16 +0100 Subject: [PATCH 7/7] fix: done state was propagating between tabs (added uuids to tabs) --- .../VisualizationComponentController.ts | 78 +++++++++---------- src/Tools/Production/IProductionData.ts | 1 + src/Tools/Production/ProductionTab.ts | 17 ++++ templates/Controllers/production.html | 2 +- 4 files changed, 55 insertions(+), 43 deletions(-) diff --git a/src/Module/Components/VisualizationComponentController.ts b/src/Module/Components/VisualizationComponentController.ts index 9e1bc4de..aa2cbce3 100644 --- a/src/Module/Components/VisualizationComponentController.ts +++ b/src/Module/Components/VisualizationComponentController.ts @@ -19,6 +19,8 @@ const DONE_EDGE_COLOR = 'rgba(105, 125, 145, 0.3)'; const DONE_EDGE_COLOR_HL = 'rgba(134, 151, 167, 0.45)'; const DONE_EDGE_FONT_COLOR = 'rgba(238, 238, 238, 0.3)'; +const STORAGE_KEY_PREFIX = 'doneNodes_'; + function fadeRgba(rgba: string, alpha: number): string { const m = rgba.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); @@ -30,8 +32,6 @@ function fadeRgba(rgba: string, alpha: number): string return `rgba(${m[1]}, ${m[2]}, ${m[3]}, ${alpha})`; } -const STORAGE_KEY_PREFIX = 'doneNodes_'; - export class VisualizationComponentController implements IController { @@ -59,10 +59,13 @@ export class VisualizationComponentController implements IController public $onInit(): void { this.unregisterWatcherCallback = this.$scope.$watch(() => { - return this.result; - }, (newValue) => { - this.updateData(newValue); - }); + return { + result: this.result, + storageKey: this.storageKey, + }; + }, (newValue: { result: ProductionResult; storageKey: string }) => { + this.updateData(newValue.result, newValue.storageKey); + }, true); } public $onDestroy(): void @@ -70,15 +73,10 @@ export class VisualizationComponentController implements IController this.unregisterWatcherCallback(); } - private getPersistenceKey(): string - { - return STORAGE_KEY_PREFIX + (this.storageKey || 'default'); - } - - private loadDoneKeys(): void + private loadDoneKeys(key: string): void { try { - const raw = localStorage.getItem(this.getPersistenceKey()); + const raw = localStorage.getItem(STORAGE_KEY_PREFIX + key); this.doneKeys = raw ? new Set(JSON.parse(raw)) : new Set(); } catch { @@ -89,7 +87,9 @@ export class VisualizationComponentController implements IController private saveDoneKeys(): void { try { - localStorage.setItem(this.getPersistenceKey(), JSON.stringify([...this.doneKeys])); + const key = STORAGE_KEY_PREFIX + (this.storageKey || 'default'); + + localStorage.setItem(key, JSON.stringify([...this.doneKeys])); } catch { /* storage full: silently ignore */ } } @@ -123,10 +123,10 @@ export class VisualizationComponentController implements IController } else { this.visNodes.update({ id, - color: meta.originalColor, + color: meta.originalColor, borderWidth: 0, borderWidthSelected: 1, - font: meta.originalFont, + font: meta.originalFont, } as any); } } @@ -139,7 +139,7 @@ export class VisualizationComponentController implements IController this.visEdges.update({ id, color: { - color: DONE_EDGE_COLOR, + color: DONE_EDGE_COLOR, highlight: DONE_EDGE_COLOR_HL, }, font: { @@ -151,7 +151,7 @@ export class VisualizationComponentController implements IController this.visEdges.update({ id, color: meta.originalColor, - font: meta.originalFont, + font: meta.originalFont, dashes: false, } as any); } @@ -166,15 +166,11 @@ export class VisualizationComponentController implements IController } const key = meta.stableKey; + const isDone = !this.doneKeys.has(key); - if (this.doneKeys.has(key)) { - this.doneKeys.delete(key); - this.applyDoneNodeStyle(visId, false); - } else { - this.doneKeys.add(key); - this.applyDoneNodeStyle(visId, true); - } + isDone ? this.doneKeys.add(key) : this.doneKeys.delete(key); + this.applyDoneNodeStyle(visId, isDone); this.saveDoneKeys(); } @@ -187,15 +183,11 @@ export class VisualizationComponentController implements IController } const key = meta.stableKey; + const isDone = !this.doneKeys.has(key); - if (this.doneKeys.has(key)) { - this.doneKeys.delete(key); - this.applyDoneEdgeStyle(visId, false); - } else { - this.doneKeys.add(key); - this.applyDoneEdgeStyle(visId, true); - } + isDone ? this.doneKeys.add(key) : this.doneKeys.delete(key); + this.applyDoneEdgeStyle(visId, isDone); this.saveDoneKeys(); } @@ -268,9 +260,11 @@ export class VisualizationComponentController implements IController const cy = cytoscape(options as any); } - public useVis(result: ProductionResult): void + public useVis(result: ProductionResult, storageKey: string): void { - this.loadDoneKeys(); + this.storageKey = storageKey; + this.loadDoneKeys(storageKey); + this.nodeStyles.clear(); this.edgeStyles.clear(); @@ -284,9 +278,9 @@ export class VisualizationComponentController implements IController // Store original style + stable key for later toggling. this.nodeStyles.set(node.id, { - stableKey: node.getStableKey(), + stableKey: node.getStableKey(), originalColor: visNode.color ? { ...visNode.color, highlight: { ...visNode.color.highlight } } : undefined, - originalFont: visNode.font ? { ...visNode.font } : { color: 'rgba(238, 238, 238, 1)' }, + originalFont: visNode.font ? { ...visNode.font } : { color: 'rgba(238, 238, 238, 1)' }, }); } @@ -302,7 +296,7 @@ export class VisualizationComponentController implements IController } const edgeColor = { - color: 'rgba(105, 125, 145, 1)', + color: 'rgba(105, 125, 145, 1)', highlight: 'rgba(134, 151, 167, 1)', }; const edgeFont = { @@ -320,9 +314,9 @@ export class VisualizationComponentController implements IController } as any); this.edgeStyles.set(edge.id, { - stableKey: edge.getStableKey(), + stableKey: edge.getStableKey(), originalColor: { ...edgeColor }, - originalFont: { ...edgeFont }, + originalFont: { ...edgeFont }, }); } @@ -403,7 +397,7 @@ export class VisualizationComponentController implements IController }); } - public updateData(result: ProductionResult|undefined): void + public updateData(result: ProductionResult|undefined, storageKey?: string): void { if (!result) { return; @@ -411,13 +405,14 @@ export class VisualizationComponentController implements IController this.fitted = false; + const key = storageKey || this.storageKey || 'default'; let use; use = 'vis'; if (use === 'cytoscape') { this.useCytoscape(result); } else { - this.useVis(result); + this.useVis(result, key); } } @@ -440,7 +435,6 @@ export class VisualizationComponentController implements IController nodes: { labelHighlightBold: false, font: { - // align: 'left', size: 14, multi: 'html', }, diff --git a/src/Tools/Production/IProductionData.ts b/src/Tools/Production/IProductionData.ts index 3c0393c3..f8eed6e7 100644 --- a/src/Tools/Production/IProductionData.ts +++ b/src/Tools/Production/IProductionData.ts @@ -13,6 +13,7 @@ export interface IProductionDataMetadata icon: string|null; schemaVersion: number; gameVersion: string; + tabId?: string; } diff --git a/src/Tools/Production/ProductionTab.ts b/src/Tools/Production/ProductionTab.ts index c0436bb1..12e6e5a9 100644 --- a/src/Tools/Production/ProductionTab.ts +++ b/src/Tools/Production/ProductionTab.ts @@ -50,6 +50,11 @@ export class ProductionTab { if (productionData) { this.data = productionData; + + // Back-fill tabId for tabs that were saved before this feature existed. + if (!this.data.metadata.tabId) { + this.data.metadata.tabId = ProductionTab.generateTabId(); + } } else { this.resetData(); this.addEmptyProduct(); @@ -187,6 +192,7 @@ export class ProductionTab icon: null, schemaVersion: 1, gameVersion: '0', + tabId: ProductionTab.generateTabId(), }, request: { allowedAlternateRecipes: [], @@ -202,6 +208,17 @@ export class ProductionTab }; } + private static generateTabId(): string + { + // crypto.randomUUID is available in all modern browsers + if (typeof crypto !== 'undefined' && crypto.randomUUID) { + return crypto.randomUUID(); + } + + // Fallback for older environments + return 'tab-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2); + } + get icon(): string|null { if (this.data.metadata.icon) { diff --git a/templates/Controllers/production.html b/templates/Controllers/production.html index e92d098c..5c0a615b 100644 --- a/templates/Controllers/production.html +++ b/templates/Controllers/production.html @@ -848,7 +848,7 @@

You can move the nodes around using drag'n'drop. Mark a node or an egde as "done" by double-clicking it. - +