# ============================================================================= # Copyright (C) 2025 # # This file is part of pyfa. # # pyfa is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # pyfa is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with pyfa. If not, see . # ============================================================================= # noinspection PyPackageRequirements import wx from collections import Counter from eos.const import FittingSlot, FittingModuleState from service.fit import Fit as svcFit from service.port.eft import exportEft, importEft, _importPrepare from service.const import PortEftOptions _t = wx.GetTranslation class FitDiffFrame(wx.Frame): """A frame to display differences between two fits.""" def __init__(self, parent, fitID): super().__init__( parent, title=_t("Fit Diff"), style=wx.DEFAULT_FRAME_STYLE | wx.RESIZE_BORDER, size=(1000, 600) ) self.parent = parent self.fitID = fitID self.sFit = svcFit.getInstance() # EFT export options (same as CTRL-C) self.eftOptions = { PortEftOptions.LOADED_CHARGES: True, PortEftOptions.MUTATIONS: True, PortEftOptions.IMPLANTS: True, PortEftOptions.BOOSTERS: True, PortEftOptions.CARGO: True, } self.initUI() self.Centre() self.Show() def initUI(self): panel = wx.Panel(self) mainSizer = wx.BoxSizer(wx.VERTICAL) # Instructions at the top instructions = wx.StaticText( panel, label=_t("Paste fits in EFT format to compare") ) mainSizer.Add(instructions, 0, wx.ALL | wx.EXPAND, 5) # Three panes: Fit 1 | Diff | Fit 2 panesSizer = wx.BoxSizer(wx.HORIZONTAL) # Pane 1: Fit 1 (editable) fit1Box = wx.StaticBox(panel, label=_t("Fit 1")) fit1Sizer = wx.StaticBoxSizer(fit1Box, wx.VERTICAL) self.fit1Text = wx.TextCtrl( panel, style=wx.TE_MULTILINE | wx.TE_DONTWRAP ) fit1Sizer.Add(self.fit1Text, 1, wx.EXPAND) panesSizer.Add(fit1Sizer, 1, wx.ALL | wx.EXPAND, 5) # Bind text changed event to update diff self.fit1Text.Bind(wx.EVT_TEXT, self.onFitChanged) # Pane 2: Diff (simple text format) diffBox = wx.StaticBox(panel, label=_t("Differences")) diffSizer = wx.StaticBoxSizer(diffBox, wx.VERTICAL) self.diffText = wx.TextCtrl( panel, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_DONTWRAP ) diffSizer.Add(self.diffText, 1, wx.EXPAND) panesSizer.Add(diffSizer, 1, wx.ALL | wx.EXPAND, 5) # Pane 3: Fit 2 (user input) fit2Box = wx.StaticBox(panel, label=_t("Fit 2")) fit2Sizer = wx.StaticBoxSizer(fit2Box, wx.VERTICAL) self.fit2Text = wx.TextCtrl( panel, style=wx.TE_MULTILINE | wx.TE_DONTWRAP ) fit2Sizer.Add(self.fit2Text, 1, wx.EXPAND) # Bind text changed event to update diff self.fit2Text.Bind(wx.EVT_TEXT, self.onFitChanged) panesSizer.Add(fit2Sizer, 1, wx.ALL | wx.EXPAND, 5) mainSizer.Add(panesSizer, 1, wx.EXPAND | wx.ALL, 5) panel.SetSizer(mainSizer) # Load current fit into pane 1 self.loadFit1() def loadFit1(self): """Load the current fit into pane 1 as EFT format.""" fit = self.sFit.getFit(self.fitID) if fit: eftText = exportEft(fit, self.eftOptions, callback=None) self.fit1Text.SetValue(eftText) def onFitChanged(self, event): """Handle text change in either fit pane - update diff.""" self.updateDiff() event.Skip() def updateDiff(self): """Calculate and display the differences between the two fits.""" self.diffText.Clear() fit1Text = self.fit1Text.GetValue().strip() fit2Text = self.fit2Text.GetValue().strip() if not fit1Text or not fit2Text: return # Parse both fits fit1 = self.parsePastedFit(fit1Text) fit2 = self.parsePastedFit(fit2Text) if fit1 is None: self.diffText.SetValue(_t("Error: Fit 1 has invalid EFT format")) return if fit2 is None: self.diffText.SetValue(_t("Error: Fit 2 has invalid EFT format")) return # Calculate differences and format as simple text list diffLines = self.calculateDiff(fit1, fit2) self.diffText.SetValue('\n'.join(diffLines)) def parsePastedFit(self, text): """Parse pasted EFT text into a fit object.""" try: lines = _importPrepare(text.splitlines()) if not lines: return None return importEft(lines) except Exception: return None def calculateDiff(self, fit1, fit2): """Calculate items needed to transform fit1 into fit2. Returns a list of strings in the format: " " Only shows items that need to be added (no negative values). """ diffLines = [] # Get module counts by type for each fit (grouped by slot type) fit1_modules = self.getModuleCounts(fit1) fit2_modules = self.getModuleCounts(fit2) # Diff modules - only show items needed to add all_module_types = set(fit1_modules.keys()) | set(fit2_modules.keys()) for module_type in sorted(all_module_types): count1 = fit1_modules.get(module_type, 0) count2 = fit2_modules.get(module_type, 0) if count2 > count1: diffLines.append(f"{module_type} {count2 - count1}") # Get drone counts fit1_drones = self.getDroneCounts(fit1) fit2_drones = self.getDroneCounts(fit2) all_drone_types = set(fit1_drones.keys()) | set(fit2_drones.keys()) for drone_type in sorted(all_drone_types): count1 = fit1_drones.get(drone_type, 0) count2 = fit2_drones.get(drone_type, 0) if count2 > count1: diffLines.append(f"{drone_type} {count2 - count1}") # Get fighter counts fit1_fighters = self.getFighterCounts(fit1) fit2_fighters = self.getFighterCounts(fit2) all_fighter_types = set(fit1_fighters.keys()) | set(fit2_fighters.keys()) for fighter_type in sorted(all_fighter_types): count1 = fit1_fighters.get(fighter_type, 0) count2 = fit2_fighters.get(fighter_type, 0) if count2 > count1: diffLines.append(f"{fighter_type} {count2 - count1}") # Get cargo counts fit1_cargo = self.getCargoCounts(fit1) fit2_cargo = self.getCargoCounts(fit2) all_cargo_types = set(fit1_cargo.keys()) | set(fit2_cargo.keys()) for cargo_type in sorted(all_cargo_types): count1 = fit1_cargo.get(cargo_type, 0) count2 = fit2_cargo.get(cargo_type, 0) if count2 > count1: diffLines.append(f"{cargo_type} {count2 - count1}") # Get implants fit1_implants = self.getImplantNames(fit1) fit2_implants = self.getImplantNames(fit2) for implant in sorted(fit2_implants - fit1_implants): diffLines.append(f"{implant} 1") # Get boosters fit1_boosters = self.getBoosterNames(fit1) fit2_boosters = self.getBoosterNames(fit2) for booster in sorted(fit2_boosters - fit1_boosters): diffLines.append(f"{booster} 1") return diffLines def getModuleCounts(self, fit): """Get a counter of module types for a fit. Position doesn't matter, just counts by module name. """ counts = Counter() for module in fit.modules: if module.isEmpty: continue # Use item type name for comparison name = module.item.typeName if module.item else "" # Add charge info if present if module.charge: name += f", {module.charge.typeName}" # Add offline suffix if offline if module.state == FittingModuleState.OFFLINE: name += " /offline" counts[name] += 1 return counts def getDroneCounts(self, fit): """Get a counter of drone types for a fit.""" counts = Counter() for drone in fit.drones: if drone.item: counts[drone.item.typeName] += drone.amount return counts def getFighterCounts(self, fit): """Get a counter of fighter types for a fit.""" counts = Counter() for fighter in fit.fighters: if fighter.item: counts[fighter.item.typeName] += fighter.amount return counts def getCargoCounts(self, fit): """Get a counter of cargo items for a fit.""" counts = Counter() for cargo in fit.cargo: if cargo.item: counts[cargo.item.typeName] += cargo.amount return counts def getImplantNames(self, fit): """Get a set of implant names for a fit.""" names = set() for implant in fit.implants: if implant.item: names.add(implant.item.typeName) return names def getBoosterNames(self, fit): """Get a set of booster names for a fit.""" names = set() for booster in fit.boosters: if booster.item: names.add(booster.item.typeName) return names