# ============================================================================= # 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 import re from service.fit import Fit as svcFit from service.port.eft import exportEft from service.const import PortEftOptions _t = wx.GetTranslation # Regex for parsing items: itemName x? quantity?, ,? chargeName? ITEM_REGEX = re.compile( r"^(?P[-\'\w\s]+?)x?\s*(?P\d+)?\s*(?:,\s*(?P[-\'\w\s]+))?$" ) 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 and flip button at the top topSizer = wx.BoxSizer(wx.HORIZONTAL) instructions = wx.StaticText( panel, label=_t("Paste fits in EFT format to compare") ) topSizer.Add(instructions, 1, wx.ALL | wx.EXPAND, 5) flipButton = wx.Button(panel, label=_t("Flip")) flipButton.Bind(wx.EVT_BUTTON, self.onFlip) topSizer.Add(flipButton, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) mainSizer.Add(topSizer, 0, wx.EXPAND) # 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 onFlip(self, event): """Swap Fit 1 and Fit 2.""" fit1Value = self.fit1Text.GetValue() fit2Value = self.fit2Text.GetValue() self.fit1Text.SetValue(fit2Value) self.fit2Text.SetValue(fit1Value) 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 map of item name to count.""" items = {} for line in text.splitlines(): line = line.strip() if not line or line.startswith("["): continue match = ITEM_REGEX.match(line) if match: item_name = match.group("itemName").strip() quantity = match.group("quantity") count = int(quantity) if quantity else 1 if item_name not in items: items[item_name] = 0 items[item_name] += count return items def calculateDiff(self, fit1_items, fit2_items): """Calculate items needed to transform fit1 into fit2. Returns a list of strings showing additions and extra items. """ diffLines = [] all_items = set(fit1_items.keys()) | set(fit2_items.keys()) additions = [] extras = [] for item in sorted(all_items): count1 = fit1_items.get(item, 0) count2 = fit2_items.get(item, 0) if count2 > count1: additions.append(f"{item} x{count2 - count1}") elif count1 > count2: extras.append(f"{item} x-{count1 - count2}") diffLines.extend(additions) if additions and extras: diffLines.extend(["", ""]) diffLines.extend(extras) return diffLines