From c6aa72a3e37af7a3d98fa6a17199b450fb696496 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 29 May 2019 15:29:24 +0300 Subject: [PATCH 01/93] Re-enable target list in graph panel --- gui/graphFrame.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gui/graphFrame.py b/gui/graphFrame.py index ee4107968..b2f59c57f 100644 --- a/gui/graphFrame.py +++ b/gui/graphFrame.py @@ -120,9 +120,9 @@ class GraphFrame(wx.Frame): self.fitList.SetMinSize((270, -1)) self.fitList.fitList.update(self.fits) self.targets = [] - # self.targetList = TargetList(self) - # self.targetList.SetMinSize((270, -1)) - # self.targetList.targetList.update(self.targets) + self.targetList = TargetList(self) + self.targetList.SetMinSize((270, -1)) + self.targetList.targetList.update(self.targets) self.graphSelection = wx.Choice(self, wx.ID_ANY, style=0) self.mainSizer.Add(self.graphSelection, 0, wx.EXPAND) @@ -180,7 +180,7 @@ class GraphFrame(wx.Frame): fitSizer = wx.BoxSizer(wx.HORIZONTAL) fitSizer.Add(self.fitList, 1, wx.EXPAND) - #fitSizer.Add(self.targetList, 1, wx.EXPAND) + fitSizer.Add(self.targetList, 1, wx.EXPAND) self.mainSizer.Add(fitSizer, 0, wx.EXPAND) From c85b6e4a361c8f6242097b2b6ac286cccfe75756 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 13 Jun 2019 13:20:04 +0300 Subject: [PATCH 02/93] Add vector class --- gui/graphFrame.py | 275 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 239 insertions(+), 36 deletions(-) diff --git a/gui/graphFrame.py b/gui/graphFrame.py index b2f59c57f..0c25c1a23 100644 --- a/gui/graphFrame.py +++ b/gui/graphFrame.py @@ -17,9 +17,9 @@ # along with pyfa. If not, see . # ============================================================================= +import math import os import traceback -from itertools import chain # noinspection PyPackageRequirements import wx @@ -52,7 +52,7 @@ try: graphFrame_enabled = True mplImported = True except ImportError as e: - pyfalog.warning("Matplotlib failed to import. Likely missing or incompatible version.") + pyfalog.warning('Matplotlib failed to import. Likely missing or incompatible version.') mpl_version = -1 Patch = mpl = Canvas = Figure = None graphFrame_enabled = False @@ -60,7 +60,7 @@ except ImportError as e: except Exception: # We can get exceptions deep within matplotlib. Catch those. See GH #1046 tb = traceback.format_exc() - pyfalog.critical("Exception when importing Matplotlib. Continuing without importing.") + pyfalog.critical('Exception when importing Matplotlib. Continuing without importing.') pyfalog.critical(tb) mpl_version = -1 Patch = mpl = Canvas = Figure = None @@ -79,13 +79,13 @@ class GraphFrame(wx.Frame): self.legendFix = False if not graphFrame_enabled: - pyfalog.warning("Matplotlib is not enabled. Skipping initialization.") + pyfalog.warning('Matplotlib is not enabled. Skipping initialization.') return try: cache_dir = mpl._get_cachedir() except: - cache_dir = os.path.expanduser(os.path.join("~", ".matplotlib")) + cache_dir = os.path.expanduser(os.path.join('~', '.matplotlib')) cache_file = os.path.join(cache_dir, 'fontList.cache') @@ -97,15 +97,15 @@ class GraphFrame(wx.Frame): graphFrame_enabled = True if int(mpl.__version__[0]) < 1: - pyfalog.warning("pyfa: Found matplotlib version {} - activating OVER9000 workarounds".format(mpl.__version__)) - pyfalog.warning("pyfa: Recommended minimum matplotlib version is 1.0.0") + pyfalog.warning('pyfa: Found matplotlib version {} - activating OVER9000 workarounds'.format(mpl.__version__)) + pyfalog.warning('pyfa: Recommended minimum matplotlib version is 1.0.0') self.legendFix = True mplImported = True - wx.Frame.__init__(self, parent, title="pyfa: Graph Generator", style=style, size=(520, 390)) + wx.Frame.__init__(self, parent, title='pyfa: Graph Generator', style=style, size=(520, 390)) - i = wx.Icon(BitmapLoader.getBitmap("graphs_small", "gui")) + i = wx.Icon(BitmapLoader.getBitmap('graphs_small', 'gui')) self.SetIcon(i) self.mainFrame = gui.mainFrame.MainFrame.getInstance() self.CreateStatusBar() @@ -153,7 +153,7 @@ class GraphFrame(wx.Frame): ctrlPanelSizer = wx.BoxSizer(wx.HORIZONTAL) viewOptSizer = wx.BoxSizer(wx.VERTICAL) - self.showY0Cb = wx.CheckBox(self.graphCtrlPanel, wx.ID_ANY, "Always show Y = 0", wx.DefaultPosition, wx.DefaultSize, 0) + self.showY0Cb = wx.CheckBox(self.graphCtrlPanel, wx.ID_ANY, 'Always show Y = 0', wx.DefaultPosition, wx.DefaultSize, 0) self.showY0Cb.SetValue(self.showY0) self.showY0Cb.Bind(wx.EVT_CHECKBOX, self.OnShowY0Update) viewOptSizer.Add(self.showY0Cb, 0, wx.LEFT | wx.TOP | wx.RIGHT | wx.EXPAND, 5) @@ -203,7 +203,7 @@ class GraphFrame(wx.Frame): self.SetMinSize(self.GetSize()) def handleDrag(self, type, fitID): - if type == "fit": + if type == 'fit': self.AppendFitToList(fitID) def closeEvent(self, event): @@ -321,15 +321,15 @@ class GraphFrame(wx.Frame): if fieldDef.inputDefault is not None: inputDefault = fieldDef.inputDefault if not isinstance(inputDefault, str): - inputDefault = ("%f" % inputDefault).rstrip("0") - if inputDefault[-1:] == ".": - inputDefault += "0" + inputDefault = ('%f' % inputDefault).rstrip('0') + if inputDefault[-1:] == '.': + inputDefault += '0' textBox.ChangeValue(inputDefault) imgLabelSizer = wx.BoxSizer(wx.HORIZONTAL) if fieldDef.inputIconID: - icon = BitmapLoader.getBitmap(fieldDef.inputIconID, "icons") + icon = BitmapLoader.getBitmap(fieldDef.inputIconID, 'icons') if icon is not None: static = wx.StaticBitmap(self.graphCtrlPanel) static.SetBitmap(icon) @@ -343,7 +343,7 @@ class GraphFrame(wx.Frame): def delayedDraw(self, event=None): self.drawTimer.Stop() - self.drawTimer.Start(Fit.getInstance().serviceFittingOptions["marketSearchDelay"], True) + self.drawTimer.Start(Fit.getInstance().serviceFittingOptions['marketSearchDelay'], True) def draw(self, event=None): global mpl_version @@ -356,7 +356,7 @@ class GraphFrame(wx.Frame): # todo: FIX THIS, see #1430. draw() is not being unbound properly when the window closes, this is an easy fix, # but not a proper solution if not self: - pyfalog.warning("GraphFrame handled event, however GraphFrame no longer exists. Ignoring event") + pyfalog.warning('GraphFrame handled event, however GraphFrame no longer exists. Ignoring event') return values = self.getValues() @@ -396,8 +396,8 @@ class GraphFrame(wx.Frame): self.subplot.plot(xs, ys) legend.append('{} ({})'.format(fit.name, fit.ship.item.getShortName())) except Exception as ex: - pyfalog.warning("Invalid values in '{0}'", fit.name) - self.SetStatusText("Invalid values in '%s'" % fit.name) + pyfalog.warning('Invalid values in "{0}"', fit.name) + self.SetStatusText('Invalid values in "%s"' % fit.name) self.canvas.draw() return @@ -414,7 +414,7 @@ class GraphFrame(wx.Frame): if mpl_version < 2: if self.legendFix and len(legend) > 0: - leg = self.subplot.legend(tuple(legend), "upper right", shadow=False) + leg = self.subplot.legend(tuple(legend), 'upper right', shadow=False) for t in leg.get_texts(): t.set_fontsize('small') @@ -422,7 +422,7 @@ class GraphFrame(wx.Frame): l.set_linewidth(1) elif not self.legendFix and len(legend) > 0: - leg = self.subplot.legend(tuple(legend), "upper right", shadow=False, frameon=False) + leg = self.subplot.legend(tuple(legend), 'upper right', shadow=False, frameon=False) for t in leg.get_texts(): t.set_fontsize('small') @@ -431,14 +431,14 @@ class GraphFrame(wx.Frame): elif mpl_version >= 2: legend2 = [] legend_colors = { - 0: "blue", - 1: "orange", - 2: "green", - 3: "red", - 4: "purple", - 5: "brown", - 6: "pink", - 7: "grey", + 0: 'blue', + 1: 'orange', + 2: 'green', + 3: 'red', + 4: 'purple', + 5: 'brown', + 6: 'pink', + 7: 'grey', } for i, i_name in enumerate(legend): @@ -457,7 +457,7 @@ class GraphFrame(wx.Frame): l.set_linewidth(1) self.canvas.draw() - self.SetStatusText("") + self.SetStatusText('') self.Refresh() def onFieldChanged(self, event): @@ -516,13 +516,13 @@ class FitList(wx.Panel): self.fitList = FitDisplay(self) self.mainSizer.Add(self.fitList, 1, wx.EXPAND) - fitToolTip = wx.ToolTip("Drag a fit into this list to graph it") + fitToolTip = wx.ToolTip('Drag a fit into this list to graph it') self.fitList.SetToolTip(fitToolTip) class FitDisplay(gui.display.Display): - DEFAULT_COLS = ["Base Icon", - "Base Name"] + DEFAULT_COLS = ['Base Icon', + 'Base Name'] def __init__(self, parent): gui.display.Display.__init__(self, parent) @@ -537,13 +537,216 @@ class TargetList(wx.Panel): self.targetList = TargetDisplay(self) self.mainSizer.Add(self.targetList, 1, wx.EXPAND) - fitToolTip = wx.ToolTip("Drag a fit into this list to graph it") + fitToolTip = wx.ToolTip('Drag a fit into this list to graph it') self.targetList.SetToolTip(fitToolTip) class TargetDisplay(gui.display.Display): - DEFAULT_COLS = ["Base Icon", - "Base Name"] + DEFAULT_COLS = ['Base Icon', + 'Base Name'] def __init__(self, parent): gui.display.Display.__init__(self, parent) + + +# class VectorEvent(wx.PyCommandEvent): +# def __init__(self, evtType, id): +# wx.PyCommandEvent.__init__(self, evtType, id) +# self._angle = 0 +# self._length = 0 +# +# def GetValue(self): +# return self._angle, self._length +# +# def GetAngle(self): +# return self._angle +# +# def GetLength(self): +# return self._length + + +class VectorPicker(wx.Control): + + myEVT_VECTOR_CHANGED = wx.NewEventType() + EVT_VECTOR_CHANGED = wx.PyEventBinder(myEVT_VECTOR_CHANGED, 1) + + def __init__(self, *args, **kwargs): + self._label = str(kwargs.pop('label', '')) + self._labelpos = int(kwargs.pop('labelpos', 0)) + self._offset = float(kwargs.pop('offset', 0)) + self._size = max(0, float(kwargs.pop('size', 50))) + self._fontsize = max(1, float(kwargs.pop('fontsize', 8))) + wx.Control.__init__(self, *args, **kwargs) + self._font = wx.Font(self._fontsize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) + self._angle = 0 + self._length = 1 + self._left = False + self._right = False + self._tooltip = 'Click to set angle and velocity, right-click for increments; mouse wheel for velocity only' + self.SetToolTip(wx.ToolTip(self._tooltip)) + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) + self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel) + + def DoGetBestSize(self): + return wx.Size(self._size, self._size) + + def AcceptsFocusFromKeyboard(self): + return False + + def GetValue(self): + return self._angle, self._length + + def GetAngle(self): + return self._angle + + def GetLength(self): + return self._length + + def SetValue(self, angle=None, length=None): + if angle is not None: + self._angle = min(max(angle, -180), 180) + if length is not None: + self._length = min(max(length, 0), 1) + self.Refresh() + + def SetAngle(self, angle): + self.SetValue(angle, None) + + def SetLength(self, length): + self.SetValue(None, length) + + def OnPaint(self, event): + dc = wx.BufferedPaintDC(self) + self.Draw(dc) + + def Draw(self, dc): + width, height = self.GetClientSize() + if not width or not height: + return + + dc.SetBackground(wx.Brush(self.GetBackgroundColour(), wx.BRUSHSTYLE_SOLID)) + dc.Clear() + dc.SetTextForeground(wx.Colour(0)) + dc.SetFont(self._font) + + radius = min(width, height) / 2 - 2 + dc.SetBrush(wx.WHITE_BRUSH) + dc.DrawCircle(radius + 2, radius + 2, radius) + a = math.radians(self._angle + self._offset) + x = math.sin(a) * radius + y = math.cos(a) * radius + dc.DrawLine(radius + 2, radius + 2, radius + 2 + x * self._length, radius + 2 - y * self._length) + dc.SetBrush(wx.BLACK_BRUSH) + dc.DrawCircle(radius + 2 + x * self._length, radius + 2 - y * self._length, 2) + + if self._label: + labelText = self._label + labelTextW, labelTextH = dc.GetTextExtent(labelText) + labelTextX = (radius * 2 + 4 - labelTextW) if (self._labelpos & 1) else 0 + labelTextY = (radius * 2 + 4 - labelTextH) if (self._labelpos & 2) else 0 + dc.DrawText(labelText, labelTextX, labelTextY) + + lengthText = '%d%%' % (100 * self._length,) + lengthTextW, lengthTextH = dc.GetTextExtent(lengthText) + lengthTextX = radius + 2 + x / 2 - y / 3 - lengthTextW / 2 + lengthTextY = radius + 2 - y / 2 - x / 3 - lengthTextH / 2 + dc.DrawText(lengthText, lengthTextX, lengthTextY) + + angleText = '%d\u00B0' % (self._angle,) + angleTextW, angleTextH = dc.GetTextExtent(angleText) + angleTextX = radius + 2 - x / 2 - angleTextW / 2 + angleTextY = radius + 2 + y / 2 - angleTextH / 2 + dc.DrawText(angleText, angleTextX, angleTextY) + + def OnEraseBackground(self, event): + pass + + def OnLeftDown(self, event): + self._left = True + self.SetToolTip(None) + self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) + self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnLeftUp) + if not self._right: + self.Bind(wx.EVT_MOTION, self.OnMotion) + if not self.HasCapture(): + self.CaptureMouse() + self.HandleMouseEvent(event) + + def OnRightDown(self, event): + self._right = True + self.SetToolTip(None) + self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) + self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnRightUp) + if not self._left: + self.Bind(wx.EVT_MOTION, self.OnMotion) + if not self.HasCapture(): + self.CaptureMouse() + self.HandleMouseEvent(event) + + def OnLeftUp(self, event): + self.HandleMouseEvent(event) + self.Unbind(wx.EVT_LEFT_UP, handler=self.OnLeftUp) + self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST, handler=self.OnLeftUp) + self._left = False + if not self._right: + self.Unbind(wx.EVT_MOTION, handler=self.OnMotion) + self.SendChangeEvent() + self.SetToolTip(wx.ToolTip(self._tooltip)) + if self.HasCapture(): + self.ReleaseMouse() + + def OnRightUp(self, event): + self.HandleMouseEvent(event) + self.Unbind(wx.EVT_RIGHT_UP, handler=self.OnRightUp) + self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST, handler=self.OnRightUp) + self._right = False + if not self._left: + self.Unbind(wx.EVT_MOTION, handler=self.OnMotion) + self.SendChangeEvent() + self.SetToolTip(wx.ToolTip(self._tooltip)) + if self.HasCapture(): + self.ReleaseMouse() + + def OnMotion(self, event): + self.HandleMouseEvent(event) + event.Skip() + + def OnWheel(self, event): + amount = 0.1 * event.GetWheelRotation() / event.GetWheelDelta() + self._length = min(max(self._length + amount, 0.0), 1.0) + self.Refresh() + self.SendChangeEvent() + + def HandleMouseEvent(self, event): + width, height = self.GetClientSize() + if width and height: + center = min(width, height) / 2 + x, y = event.GetPositionTuple() + x = x - center + y = center - y + angle = self._angle + length = min((x * x + y * y) ** 0.5 / (center - 2), 1.0) + if length < 0.01: + length = 0 + else: + angle = ((math.degrees(math.atan2(x, y)) - self._offset + 180) % 360) - 180 + if (self._right and not self._left) or event.ShiftDown(): + angle = round(angle / 15.0) * 15.0 + # floor() for length to make it easier to hit 0%, can still hit 100% outside the circle + length = math.floor(length / 0.05) * 0.05 + if (angle != self._angle) or (length != self._length): + self._angle = angle + self._length = length + self.Refresh() + if self._right and not self._left: + self.SendChangeEvent() + + def SendChangeEvent(self): + changeEvent = wx.CommandEvent(self.myEVT_VECTOR_CHANGED, self.GetId()) + changeEvent._object = self + changeEvent._angle = self._angle + changeEvent._length = self._length + self.GetEventHandler().ProcessEvent(changeEvent) From 3c0b8643f64b34f517424697ac2675e68162ee7d Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 13 Jun 2019 13:28:04 +0300 Subject: [PATCH 03/93] Move graph file into graph package --- gui/{graph.py => builtinGraphs/base.py} | 0 gui/builtinGraphs/fitCapAmountVsTime.py | 2 +- gui/builtinGraphs/fitCapRegenVsCapPerc.py | 2 +- gui/builtinGraphs/fitDmgVsTime.py | 2 +- gui/builtinGraphs/fitDpsVsRange.py | 2 +- gui/builtinGraphs/fitMobilityVsTime.py | 2 +- gui/builtinGraphs/fitShieldAmountVsTime.py | 2 +- gui/builtinGraphs/fitShieldRegenVsShieldPerc.py | 2 +- gui/builtinGraphs/fitWarpTimeVsDistance.py | 2 +- gui/graphFrame.py | 2 +- 10 files changed, 9 insertions(+), 9 deletions(-) rename gui/{graph.py => builtinGraphs/base.py} (100%) diff --git a/gui/graph.py b/gui/builtinGraphs/base.py similarity index 100% rename from gui/graph.py rename to gui/builtinGraphs/base.py diff --git a/gui/builtinGraphs/fitCapAmountVsTime.py b/gui/builtinGraphs/fitCapAmountVsTime.py index e2aa9bbf6..f29573175 100644 --- a/gui/builtinGraphs/fitCapAmountVsTime.py +++ b/gui/builtinGraphs/fitCapAmountVsTime.py @@ -21,7 +21,7 @@ from collections import OrderedDict from eos.graph.fitCapAmountVsTime import FitCapAmountVsTimeGraph as EosGraph -from gui.graph import Graph, XDef, YDef +from .base import Graph, XDef, YDef class FitCapAmountVsTimeGraph(Graph): diff --git a/gui/builtinGraphs/fitCapRegenVsCapPerc.py b/gui/builtinGraphs/fitCapRegenVsCapPerc.py index bf6731b8a..ad4ae639e 100644 --- a/gui/builtinGraphs/fitCapRegenVsCapPerc.py +++ b/gui/builtinGraphs/fitCapRegenVsCapPerc.py @@ -21,7 +21,7 @@ from collections import OrderedDict from eos.graph.fitCapRegenVsCapPerc import FitCapRegenVsCapPercGraph as EosGraph -from gui.graph import Graph, XDef, YDef +from .base import Graph, XDef, YDef class FitCapRegenVsCapPercGraph(Graph): diff --git a/gui/builtinGraphs/fitDmgVsTime.py b/gui/builtinGraphs/fitDmgVsTime.py index 4dc22dc22..e9b1b14a6 100644 --- a/gui/builtinGraphs/fitDmgVsTime.py +++ b/gui/builtinGraphs/fitDmgVsTime.py @@ -22,7 +22,7 @@ from collections import OrderedDict from eos.graph.fitDmgVsTime import FitDmgVsTimeGraph as EosGraphDmg from eos.graph.fitDpsVsTime import FitDpsVsTimeGraph as EosGraphDps -from gui.graph import Graph, XDef, YDef +from .base import Graph, XDef, YDef class FitDmgVsTimeGraph(Graph): diff --git a/gui/builtinGraphs/fitDpsVsRange.py b/gui/builtinGraphs/fitDpsVsRange.py index a17cf067a..3a2e7326c 100644 --- a/gui/builtinGraphs/fitDpsVsRange.py +++ b/gui/builtinGraphs/fitDpsVsRange.py @@ -21,7 +21,7 @@ from collections import OrderedDict from eos.graph.fitDpsVsRange import FitDpsVsRangeGraph as EosGraph -from gui.graph import Graph, XDef, YDef, ExtraInput +from .base import Graph, XDef, YDef, ExtraInput class FitDpsVsRangeGraph(Graph): diff --git a/gui/builtinGraphs/fitMobilityVsTime.py b/gui/builtinGraphs/fitMobilityVsTime.py index 1d55f6061..6f002bce9 100644 --- a/gui/builtinGraphs/fitMobilityVsTime.py +++ b/gui/builtinGraphs/fitMobilityVsTime.py @@ -22,7 +22,7 @@ from collections import OrderedDict from eos.graph.fitDistanceVsTime import FitDistanceVsTimeGraph as EosGraphDistance from eos.graph.fitSpeedVsTime import FitSpeedVsTimeGraph as EosGraphSpeed -from gui.graph import Graph, XDef, YDef +from .base import Graph, XDef, YDef class FitMobilityVsTimeGraph(Graph): diff --git a/gui/builtinGraphs/fitShieldAmountVsTime.py b/gui/builtinGraphs/fitShieldAmountVsTime.py index d3c58dddd..ae44178f6 100644 --- a/gui/builtinGraphs/fitShieldAmountVsTime.py +++ b/gui/builtinGraphs/fitShieldAmountVsTime.py @@ -22,7 +22,7 @@ from collections import OrderedDict import gui.mainFrame from eos.graph.fitShieldAmountVsTime import FitShieldAmountVsTimeGraph as EosGraph -from gui.graph import Graph, XDef, YDef +from .base import Graph, XDef, YDef class FitShieldAmountVsTimeGraph(Graph): diff --git a/gui/builtinGraphs/fitShieldRegenVsShieldPerc.py b/gui/builtinGraphs/fitShieldRegenVsShieldPerc.py index e684688ba..e86ce5aef 100644 --- a/gui/builtinGraphs/fitShieldRegenVsShieldPerc.py +++ b/gui/builtinGraphs/fitShieldRegenVsShieldPerc.py @@ -22,7 +22,7 @@ from collections import OrderedDict import gui.mainFrame from eos.graph.fitShieldRegenVsShieldPerc import FitShieldRegenVsShieldPercGraph as EosGraph -from gui.graph import Graph, XDef, YDef +from .base import Graph, XDef, YDef class FitShieldRegenVsShieldPercGraph(Graph): diff --git a/gui/builtinGraphs/fitWarpTimeVsDistance.py b/gui/builtinGraphs/fitWarpTimeVsDistance.py index b3f479a2c..c7bce5c1d 100644 --- a/gui/builtinGraphs/fitWarpTimeVsDistance.py +++ b/gui/builtinGraphs/fitWarpTimeVsDistance.py @@ -21,7 +21,7 @@ from collections import OrderedDict from eos.graph.fitWarpTimeVsDistance import FitWarpTimeVsDistanceGraph as EosGraph -from gui.graph import Graph, XDef, YDef +from .base import Graph, XDef, YDef class FitWarpTimeVsDistanceGraph(Graph): diff --git a/gui/graphFrame.py b/gui/graphFrame.py index 0c25c1a23..8df2272f8 100644 --- a/gui/graphFrame.py +++ b/gui/graphFrame.py @@ -29,7 +29,7 @@ import gui.display import gui.globalEvents as GE import gui.mainFrame from gui.bitmap_loader import BitmapLoader -from gui.graph import Graph +from gui.builtinGraphs.base import Graph from service.fit import Fit From bbcc32c8cfcad00be450c45518d071562aa052c9 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 14 Jun 2019 13:17:11 +0300 Subject: [PATCH 04/93] Split graph frame into multiple files --- gui/graphFrame/__init__.py | 21 ++ gui/graphFrame/events.py | 24 ++ gui/{graphFrame.py => graphFrame/frame.py} | 359 +++------------------ gui/graphFrame/lists.py | 68 ++++ gui/graphFrame/panel.py | 70 ++++ gui/graphFrame/vector.py | 211 ++++++++++++ gui/mainFrame.py | 4 +- gui/mainMenuBar.py | 2 +- 8 files changed, 439 insertions(+), 320 deletions(-) create mode 100644 gui/graphFrame/__init__.py create mode 100644 gui/graphFrame/events.py rename gui/{graphFrame.py => graphFrame/frame.py} (53%) create mode 100644 gui/graphFrame/lists.py create mode 100644 gui/graphFrame/panel.py create mode 100644 gui/graphFrame/vector.py diff --git a/gui/graphFrame/__init__.py b/gui/graphFrame/__init__.py new file mode 100644 index 000000000..10c552c17 --- /dev/null +++ b/gui/graphFrame/__init__.py @@ -0,0 +1,21 @@ +# ============================================================================= +# Copyright (C) 2010 Diego Duclos +# +# 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 . +# ============================================================================= + + +from .frame import GraphFrame diff --git a/gui/graphFrame/events.py b/gui/graphFrame/events.py new file mode 100644 index 000000000..2adab83b1 --- /dev/null +++ b/gui/graphFrame/events.py @@ -0,0 +1,24 @@ +# ============================================================================= +# Copyright (C) 2010 Diego Duclos +# +# 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 . +# ============================================================================= + + +import wx.lib.newevent + + +RefreshGraph, REFRESH_GRAPH = wx.lib.newevent.NewEvent() diff --git a/gui/graphFrame.py b/gui/graphFrame/frame.py similarity index 53% rename from gui/graphFrame.py rename to gui/graphFrame/frame.py index 8df2272f8..6849afa90 100644 --- a/gui/graphFrame.py +++ b/gui/graphFrame/frame.py @@ -17,9 +17,10 @@ # along with pyfa. If not, see . # ============================================================================= -import math + import os import traceback +from itertools import chain # noinspection PyPackageRequirements import wx @@ -31,6 +32,7 @@ import gui.mainFrame from gui.bitmap_loader import BitmapLoader from gui.builtinGraphs.base import Graph from service.fit import Fit +from .panel import GraphControlPanel pyfalog = Logger(__name__) @@ -113,19 +115,9 @@ class GraphFrame(wx.Frame): self.mainSizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(self.mainSizer) - sFit = Fit.getInstance() - fit = sFit.getFit(self.mainFrame.getActiveFit()) - self.fits = [fit] if fit is not None else [] - self.fitList = FitList(self) - self.fitList.SetMinSize((270, -1)) - self.fitList.fitList.update(self.fits) - self.targets = [] - self.targetList = TargetList(self) - self.targetList.SetMinSize((270, -1)) - self.targetList.targetList.update(self.targets) - self.graphSelection = wx.Choice(self, wx.ID_ANY, style=0) self.mainSizer.Add(self.graphSelection, 0, wx.EXPAND) + self.selectedYRbMap = {} self.figure = Figure(figsize=(5, 3), tight_layout={'pad': 1.08}) @@ -144,26 +136,9 @@ class GraphFrame(wx.Frame): self.mainSizer.Add(wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, wx.EXPAND) - self.graphCtrlPanel = wx.Panel(self) - self.mainSizer.Add(self.graphCtrlPanel, 0, wx.EXPAND | wx.ALL, 0) - self.showY0 = True - self.selectedY = None - self.selectedYRbMap = {} - - ctrlPanelSizer = wx.BoxSizer(wx.HORIZONTAL) - viewOptSizer = wx.BoxSizer(wx.VERTICAL) - self.showY0Cb = wx.CheckBox(self.graphCtrlPanel, wx.ID_ANY, 'Always show Y = 0', wx.DefaultPosition, wx.DefaultSize, 0) - self.showY0Cb.SetValue(self.showY0) - self.showY0Cb.Bind(wx.EVT_CHECKBOX, self.OnShowY0Update) - viewOptSizer.Add(self.showY0Cb, 0, wx.LEFT | wx.TOP | wx.RIGHT | wx.EXPAND, 5) - self.graphSubselSizer = wx.BoxSizer(wx.VERTICAL) - viewOptSizer.Add(self.graphSubselSizer, 0, wx.ALL | wx.EXPAND, 5) - ctrlPanelSizer.Add(viewOptSizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.BOTTOM, 5) - self.inputsSizer = wx.FlexGridSizer(0, 4, 0, 0) - self.inputsSizer.AddGrowableCol(1) - ctrlPanelSizer.Add(self.inputsSizer, 1, wx.EXPAND | wx.RIGHT | wx.TOP | wx.BOTTOM, 5) - self.graphCtrlPanel.SetSizer(ctrlPanelSizer) + self.ctrlPanel = GraphControlPanel(self, self) + self.mainSizer.Add(self.ctrlPanel, 0, wx.EXPAND | wx.ALL, 0) self.drawTimer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self.draw, self.drawTimer) @@ -178,14 +153,10 @@ class GraphFrame(wx.Frame): self.sl1 = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) self.mainSizer.Add(self.sl1, 0, wx.EXPAND) - fitSizer = wx.BoxSizer(wx.HORIZONTAL) - fitSizer.Add(self.fitList, 1, wx.EXPAND) - fitSizer.Add(self.targetList, 1, wx.EXPAND) - self.mainSizer.Add(fitSizer, 0, wx.EXPAND) - - self.fitList.fitList.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick) - self.fitList.fitList.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu) + self.ctrlPanel.fitList.fitList.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick) + self.ctrlPanel.fitList.fitList.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu) + self.ctrlPanel.showY0Cb.Bind(wx.EVT_CHECKBOX, self.OnNonDestructiveControlsUpdate) self.mainFrame.Bind(GE.FIT_CHANGED, self.OnFitChanged) self.mainFrame.Bind(GE.FIT_REMOVED, self.OnFitRemoved) self.Bind(wx.EVT_CLOSE, self.closeEvent) @@ -217,7 +188,7 @@ class GraphFrame(wx.Frame): self.closeWindow() return elif keycode == 65 and mstate.GetModifiers() == wx.MOD_CONTROL: - self.fitList.fitList.selectAll() + self.ctrlPanel.fitList.fitList.selectAll() elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and mstate.GetModifiers() == wx.MOD_NONE: self.removeFits(self.getSelectedFits()) event.Skip() @@ -247,18 +218,18 @@ class GraphFrame(wx.Frame): def OnFitRemoved(self, event): event.Skip() - fit = next((f for f in self.fits if f.ID == event.fitID), None) + fit = next((f for f in self.ctrlPanel.fits if f.ID == event.fitID), None) if fit is not None: self.removeFits([fit]) def graphChanged(self, event): - self.selectedY = None + self.ctrlPanel.selectedY = None self.updateGraphWidgets() event.Skip() def closeWindow(self): from gui.builtinStatsViews.resistancesViewFull import EFFECTIVE_HP_TOGGLED # Grr gons - self.fitList.fitList.Unbind(wx.EVT_LEFT_DCLICK, handler=self.OnLeftDClick) + self.ctrlPanel.fitList.fitList.Unbind(wx.EVT_LEFT_DCLICK, handler=self.OnLeftDClick) self.mainFrame.Unbind(GE.FIT_CHANGED, handler=self.OnFitChanged) self.mainFrame.Unbind(GE.FIT_REMOVED, handler=self.OnFitRemoved) self.mainFrame.Unbind(EFFECTIVE_HP_TOGGLED, handler=self.OnEhpToggled) @@ -274,25 +245,24 @@ class GraphFrame(wx.Frame): return values - def OnShowY0Update(self, event): + def OnNonDestructiveControlsUpdate(self, event): event.Skip() - self.showY0 = self.showY0Cb.GetValue() self.draw() def OnYTypeUpdate(self, event): event.Skip() obj = event.GetEventObject() formatName = obj.GetLabel() - self.selectedY = self.selectedYRbMap[formatName] + self.ctrlPanel.selectedY = self.selectedYRbMap[formatName] self.draw() def updateGraphWidgets(self): view = self.getView() view.clearCache() - self.graphSubselSizer.Clear() - self.inputsSizer.Clear() - for child in self.graphCtrlPanel.Children: - if child is not self.showY0Cb: + self.ctrlPanel.graphSubselSizer.Clear() + self.ctrlPanel.inputsSizer.Clear() + for child in self.ctrlPanel.Children: + if child not in (self.ctrlPanel.showY0Cb, self.ctrlPanel.fitList, self.ctrlPanel.targetList): child.Destroy() self.fields.clear() @@ -302,22 +272,22 @@ class GraphFrame(wx.Frame): i = 0 for yAlias, yDef in view.yDefs.items(): if i == 0: - rdo = wx.RadioButton(self.graphCtrlPanel, wx.ID_ANY, yDef.switchLabel, style=wx.RB_GROUP) + rdo = wx.RadioButton(self.ctrlPanel, wx.ID_ANY, yDef.switchLabel, style=wx.RB_GROUP) else: - rdo = wx.RadioButton(self.graphCtrlPanel, wx.ID_ANY, yDef.switchLabel) + rdo = wx.RadioButton(self.ctrlPanel, wx.ID_ANY, yDef.switchLabel) rdo.Bind(wx.EVT_RADIOBUTTON, self.OnYTypeUpdate) - if i == (self.selectedY or 0): + if i == (self.ctrlPanel.selectedY or 0): rdo.SetValue(True) - self.graphSubselSizer.Add(rdo, 0, wx.ALL | wx.EXPAND, 0) + self.ctrlPanel.graphSubselSizer.Add(rdo, 0, wx.ALL | wx.EXPAND, 0) self.selectedYRbMap[yDef.switchLabel] = i i += 1 # Setup inputs for fieldHandle, fieldDef in (('x', view.xDef), *view.extraInputs.items()): - textBox = wx.TextCtrl(self.graphCtrlPanel, wx.ID_ANY, style=0) + textBox = wx.TextCtrl(self.ctrlPanel, wx.ID_ANY, style=0) self.fields[fieldHandle] = textBox textBox.Bind(wx.EVT_TEXT, self.onFieldChanged) - self.inputsSizer.Add(textBox, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 3) + self.ctrlPanel.inputsSizer.Add(textBox, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 3) if fieldDef.inputDefault is not None: inputDefault = fieldDef.inputDefault if not isinstance(inputDefault, str): @@ -331,13 +301,13 @@ class GraphFrame(wx.Frame): if fieldDef.inputIconID: icon = BitmapLoader.getBitmap(fieldDef.inputIconID, 'icons') if icon is not None: - static = wx.StaticBitmap(self.graphCtrlPanel) + static = wx.StaticBitmap(self.ctrlPanel) static.SetBitmap(icon) imgLabelSizer.Add(static, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 1) - imgLabelSizer.Add(wx.StaticText(self.graphCtrlPanel, wx.ID_ANY, fieldDef.inputLabel), 0, + imgLabelSizer.Add(wx.StaticText(self.ctrlPanel, wx.ID_ANY, fieldDef.inputLabel), 0, wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 3) - self.inputsSizer.Add(imgLabelSizer, 0, wx.ALIGN_CENTER_VERTICAL) + self.ctrlPanel.inputsSizer.Add(imgLabelSizer, 0, wx.ALIGN_CENTER_VERTICAL) self.Layout() self.draw() @@ -365,19 +335,19 @@ class GraphFrame(wx.Frame): self.subplot.grid(True) legend = [] - min_y = 0 if self.showY0 else None - max_y = 0 if self.showY0 else None + min_y = 0 if self.ctrlPanel.showY0 else None + max_y = 0 if self.ctrlPanel.showY0 else None xRange = values['x'] extraInputs = {ih: values[ih] for ih in view.extraInputs} try: - chosenY = [i for i in view.yDefs.keys()][self.selectedY or 0] + chosenY = [i for i in view.yDefs.keys()][self.ctrlPanel.selectedY or 0] except IndexError: chosenY = [i for i in view.yDefs.keys()][0] self.subplot.set(xlabel=view.xDef.axisLabel, ylabel=view.yDefs[chosenY].axisLabel) - for fit in self.fits: + for fit in self.ctrlPanel.fits: try: xs, ys = view.getPlotPoints(fit, extraInputs, xRange, 100, chosenY) @@ -468,29 +438,29 @@ class GraphFrame(wx.Frame): def AppendFitToList(self, fitID): sFit = Fit.getInstance() fit = sFit.getFit(fitID) - if fit not in self.fits: - self.fits.append(fit) + if fit not in self.ctrlPanel.fits: + self.ctrlPanel.fits.append(fit) - self.fitList.fitList.update(self.fits) + self.ctrlPanel.fitList.fitList.update(self.ctrlPanel.fits) self.draw() def OnLeftDClick(self, event): - row, _ = self.fitList.fitList.HitTest(event.Position) + row, _ = self.ctrlPanel.fitList.fitList.HitTest(event.Position) if row != -1: try: - fit = self.fits[row] + fit = self.ctrlPanel.fits[row] except IndexError: pass else: self.removeFits([fit]) def removeFits(self, fits): - toRemove = [f for f in fits if f in self.fits] + toRemove = [f for f in fits if f in self.ctrlPanel.fits] if not toRemove: return for fit in toRemove: - self.fits.remove(fit) - self.fitList.fitList.update(self.fits) + self.ctrlPanel.fits.remove(fit) + self.ctrlPanel.fitList.fitList.update(self.ctrlPanel.fits) view = self.getView() for fit in fits: view.clearCache(key=fit.ID) @@ -498,255 +468,10 @@ class GraphFrame(wx.Frame): def getSelectedFits(self): fits = [] - for row in self.fitList.fitList.getSelectedRows(): + for row in self.ctrlPanel.fitList.fitList.getSelectedRows(): try: - fit = self.fits[row] + fit = self.ctrlPanel.fits[row] except IndexError: continue fits.append(fit) return fits - - -class FitList(wx.Panel): - - def __init__(self, parent): - wx.Panel.__init__(self, parent) - self.mainSizer = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(self.mainSizer) - - self.fitList = FitDisplay(self) - self.mainSizer.Add(self.fitList, 1, wx.EXPAND) - fitToolTip = wx.ToolTip('Drag a fit into this list to graph it') - self.fitList.SetToolTip(fitToolTip) - - -class FitDisplay(gui.display.Display): - DEFAULT_COLS = ['Base Icon', - 'Base Name'] - - def __init__(self, parent): - gui.display.Display.__init__(self, parent) - - -class TargetList(wx.Panel): - - def __init__(self, parent): - wx.Panel.__init__(self, parent) - self.mainSizer = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(self.mainSizer) - - self.targetList = TargetDisplay(self) - self.mainSizer.Add(self.targetList, 1, wx.EXPAND) - fitToolTip = wx.ToolTip('Drag a fit into this list to graph it') - self.targetList.SetToolTip(fitToolTip) - - -class TargetDisplay(gui.display.Display): - DEFAULT_COLS = ['Base Icon', - 'Base Name'] - - def __init__(self, parent): - gui.display.Display.__init__(self, parent) - - -# class VectorEvent(wx.PyCommandEvent): -# def __init__(self, evtType, id): -# wx.PyCommandEvent.__init__(self, evtType, id) -# self._angle = 0 -# self._length = 0 -# -# def GetValue(self): -# return self._angle, self._length -# -# def GetAngle(self): -# return self._angle -# -# def GetLength(self): -# return self._length - - -class VectorPicker(wx.Control): - - myEVT_VECTOR_CHANGED = wx.NewEventType() - EVT_VECTOR_CHANGED = wx.PyEventBinder(myEVT_VECTOR_CHANGED, 1) - - def __init__(self, *args, **kwargs): - self._label = str(kwargs.pop('label', '')) - self._labelpos = int(kwargs.pop('labelpos', 0)) - self._offset = float(kwargs.pop('offset', 0)) - self._size = max(0, float(kwargs.pop('size', 50))) - self._fontsize = max(1, float(kwargs.pop('fontsize', 8))) - wx.Control.__init__(self, *args, **kwargs) - self._font = wx.Font(self._fontsize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) - self._angle = 0 - self._length = 1 - self._left = False - self._right = False - self._tooltip = 'Click to set angle and velocity, right-click for increments; mouse wheel for velocity only' - self.SetToolTip(wx.ToolTip(self._tooltip)) - self.Bind(wx.EVT_PAINT, self.OnPaint) - self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) - self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) - self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) - self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel) - - def DoGetBestSize(self): - return wx.Size(self._size, self._size) - - def AcceptsFocusFromKeyboard(self): - return False - - def GetValue(self): - return self._angle, self._length - - def GetAngle(self): - return self._angle - - def GetLength(self): - return self._length - - def SetValue(self, angle=None, length=None): - if angle is not None: - self._angle = min(max(angle, -180), 180) - if length is not None: - self._length = min(max(length, 0), 1) - self.Refresh() - - def SetAngle(self, angle): - self.SetValue(angle, None) - - def SetLength(self, length): - self.SetValue(None, length) - - def OnPaint(self, event): - dc = wx.BufferedPaintDC(self) - self.Draw(dc) - - def Draw(self, dc): - width, height = self.GetClientSize() - if not width or not height: - return - - dc.SetBackground(wx.Brush(self.GetBackgroundColour(), wx.BRUSHSTYLE_SOLID)) - dc.Clear() - dc.SetTextForeground(wx.Colour(0)) - dc.SetFont(self._font) - - radius = min(width, height) / 2 - 2 - dc.SetBrush(wx.WHITE_BRUSH) - dc.DrawCircle(radius + 2, radius + 2, radius) - a = math.radians(self._angle + self._offset) - x = math.sin(a) * radius - y = math.cos(a) * radius - dc.DrawLine(radius + 2, radius + 2, radius + 2 + x * self._length, radius + 2 - y * self._length) - dc.SetBrush(wx.BLACK_BRUSH) - dc.DrawCircle(radius + 2 + x * self._length, radius + 2 - y * self._length, 2) - - if self._label: - labelText = self._label - labelTextW, labelTextH = dc.GetTextExtent(labelText) - labelTextX = (radius * 2 + 4 - labelTextW) if (self._labelpos & 1) else 0 - labelTextY = (radius * 2 + 4 - labelTextH) if (self._labelpos & 2) else 0 - dc.DrawText(labelText, labelTextX, labelTextY) - - lengthText = '%d%%' % (100 * self._length,) - lengthTextW, lengthTextH = dc.GetTextExtent(lengthText) - lengthTextX = radius + 2 + x / 2 - y / 3 - lengthTextW / 2 - lengthTextY = radius + 2 - y / 2 - x / 3 - lengthTextH / 2 - dc.DrawText(lengthText, lengthTextX, lengthTextY) - - angleText = '%d\u00B0' % (self._angle,) - angleTextW, angleTextH = dc.GetTextExtent(angleText) - angleTextX = radius + 2 - x / 2 - angleTextW / 2 - angleTextY = radius + 2 + y / 2 - angleTextH / 2 - dc.DrawText(angleText, angleTextX, angleTextY) - - def OnEraseBackground(self, event): - pass - - def OnLeftDown(self, event): - self._left = True - self.SetToolTip(None) - self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) - self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnLeftUp) - if not self._right: - self.Bind(wx.EVT_MOTION, self.OnMotion) - if not self.HasCapture(): - self.CaptureMouse() - self.HandleMouseEvent(event) - - def OnRightDown(self, event): - self._right = True - self.SetToolTip(None) - self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) - self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnRightUp) - if not self._left: - self.Bind(wx.EVT_MOTION, self.OnMotion) - if not self.HasCapture(): - self.CaptureMouse() - self.HandleMouseEvent(event) - - def OnLeftUp(self, event): - self.HandleMouseEvent(event) - self.Unbind(wx.EVT_LEFT_UP, handler=self.OnLeftUp) - self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST, handler=self.OnLeftUp) - self._left = False - if not self._right: - self.Unbind(wx.EVT_MOTION, handler=self.OnMotion) - self.SendChangeEvent() - self.SetToolTip(wx.ToolTip(self._tooltip)) - if self.HasCapture(): - self.ReleaseMouse() - - def OnRightUp(self, event): - self.HandleMouseEvent(event) - self.Unbind(wx.EVT_RIGHT_UP, handler=self.OnRightUp) - self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST, handler=self.OnRightUp) - self._right = False - if not self._left: - self.Unbind(wx.EVT_MOTION, handler=self.OnMotion) - self.SendChangeEvent() - self.SetToolTip(wx.ToolTip(self._tooltip)) - if self.HasCapture(): - self.ReleaseMouse() - - def OnMotion(self, event): - self.HandleMouseEvent(event) - event.Skip() - - def OnWheel(self, event): - amount = 0.1 * event.GetWheelRotation() / event.GetWheelDelta() - self._length = min(max(self._length + amount, 0.0), 1.0) - self.Refresh() - self.SendChangeEvent() - - def HandleMouseEvent(self, event): - width, height = self.GetClientSize() - if width and height: - center = min(width, height) / 2 - x, y = event.GetPositionTuple() - x = x - center - y = center - y - angle = self._angle - length = min((x * x + y * y) ** 0.5 / (center - 2), 1.0) - if length < 0.01: - length = 0 - else: - angle = ((math.degrees(math.atan2(x, y)) - self._offset + 180) % 360) - 180 - if (self._right and not self._left) or event.ShiftDown(): - angle = round(angle / 15.0) * 15.0 - # floor() for length to make it easier to hit 0%, can still hit 100% outside the circle - length = math.floor(length / 0.05) * 0.05 - if (angle != self._angle) or (length != self._length): - self._angle = angle - self._length = length - self.Refresh() - if self._right and not self._left: - self.SendChangeEvent() - - def SendChangeEvent(self): - changeEvent = wx.CommandEvent(self.myEVT_VECTOR_CHANGED, self.GetId()) - changeEvent._object = self - changeEvent._angle = self._angle - changeEvent._length = self._length - self.GetEventHandler().ProcessEvent(changeEvent) diff --git a/gui/graphFrame/lists.py b/gui/graphFrame/lists.py new file mode 100644 index 000000000..4a89650ec --- /dev/null +++ b/gui/graphFrame/lists.py @@ -0,0 +1,68 @@ +# ============================================================================= +# Copyright (C) 2010 Diego Duclos +# +# 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 gui.display + + +class FitList(wx.Panel): + + def __init__(self, parent): + wx.Panel.__init__(self, parent) + self.mainSizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self.mainSizer) + + self.fitList = FitDisplay(self) + self.mainSizer.Add(self.fitList, 1, wx.EXPAND) + fitToolTip = wx.ToolTip('Drag a fit into this list to graph it') + self.fitList.SetToolTip(fitToolTip) + + +class FitDisplay(gui.display.Display): + + DEFAULT_COLS = ['Base Icon', + 'Base Name'] + + def __init__(self, parent): + gui.display.Display.__init__(self, parent) + + +class TargetList(wx.Panel): + + def __init__(self, parent): + wx.Panel.__init__(self, parent) + self.mainSizer = wx.BoxSizer(wx.VERTICAL) + self.SetSizer(self.mainSizer) + + self.targetList = TargetDisplay(self) + self.mainSizer.Add(self.targetList, 1, wx.EXPAND) + fitToolTip = wx.ToolTip('Drag a fit into this list to graph it') + self.targetList.SetToolTip(fitToolTip) + + +class TargetDisplay(gui.display.Display): + + DEFAULT_COLS = ['Base Icon', + 'Base Name'] + + def __init__(self, parent): + gui.display.Display.__init__(self, parent) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py new file mode 100644 index 000000000..e7db62034 --- /dev/null +++ b/gui/graphFrame/panel.py @@ -0,0 +1,70 @@ +# ============================================================================= +# Copyright (C) 2010 Diego Duclos +# +# 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 service.fit import Fit +from .lists import FitList, TargetList + + +class GraphControlPanel(wx.Panel): + + def __init__(self, graphFrame, parent): + super().__init__(parent) + self.graphFrame = graphFrame + + self.selectedY = None + + mainSizer = wx.BoxSizer(wx.VERTICAL) + + paramSizer = wx.BoxSizer(wx.HORIZONTAL) + viewOptSizer = wx.BoxSizer(wx.VERTICAL) + self.showY0Cb = wx.CheckBox(self, wx.ID_ANY, 'Always show Y = 0', wx.DefaultPosition, wx.DefaultSize, 0) + self.showY0Cb.SetValue(True) + viewOptSizer.Add(self.showY0Cb, 0, wx.LEFT | wx.TOP | wx.RIGHT | wx.EXPAND, 5) + self.graphSubselSizer = wx.BoxSizer(wx.VERTICAL) + viewOptSizer.Add(self.graphSubselSizer, 0, wx.ALL | wx.EXPAND, 5) + paramSizer.Add(viewOptSizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.BOTTOM, 5) + self.inputsSizer = wx.FlexGridSizer(0, 4, 0, 0) + self.inputsSizer.AddGrowableCol(1) + paramSizer.Add(self.inputsSizer, 1, wx.EXPAND | wx.RIGHT | wx.TOP | wx.BOTTOM, 5) + mainSizer.Add(paramSizer, 0, wx.EXPAND | wx.ALL, 0) + + srcTgtSizer = wx.BoxSizer(wx.HORIZONTAL) + fit = Fit.getInstance().getFit(self.graphFrame.mainFrame.getActiveFit()) + self.fits = [fit] if fit is not None else [] + self.fitList = FitList(self) + self.fitList.SetMinSize((270, -1)) + self.fitList.fitList.update(self.fits) + srcTgtSizer.Add(self.fitList, 1, wx.EXPAND) + self.targets = [] + self.targetList = TargetList(self) + self.targetList.SetMinSize((270, -1)) + self.targetList.targetList.update(self.targets) + srcTgtSizer.Add(self.targetList, 1, wx.EXPAND) + mainSizer.Add(srcTgtSizer, 1, wx.EXPAND | wx.ALL, 0) + + self.SetSizer(mainSizer) + + @property + def showY0(self): + return self.showY0Cb.GetValue() + diff --git a/gui/graphFrame/vector.py b/gui/graphFrame/vector.py new file mode 100644 index 000000000..ba99a3cda --- /dev/null +++ b/gui/graphFrame/vector.py @@ -0,0 +1,211 @@ +# ============================================================================= +# Copyright (C) 2010 Diego Duclos +# +# 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 . +# ============================================================================= + + +import math + +# noinspection PyPackageRequirements +import wx + + +class VectorPicker(wx.Control): + + myEVT_VECTOR_CHANGED = wx.NewEventType() + EVT_VECTOR_CHANGED = wx.PyEventBinder(myEVT_VECTOR_CHANGED, 1) + + def __init__(self, *args, **kwargs): + self._label = str(kwargs.pop('label', '')) + self._labelpos = int(kwargs.pop('labelpos', 0)) + self._offset = float(kwargs.pop('offset', 0)) + self._size = max(0, float(kwargs.pop('size', 50))) + self._fontsize = max(1, float(kwargs.pop('fontsize', 8))) + wx.Control.__init__(self, *args, **kwargs) + self._font = wx.Font(self._fontsize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) + self._angle = 0 + self._length = 1 + self._left = False + self._right = False + self._tooltip = 'Click to set angle and velocity, right-click for increments; mouse wheel for velocity only' + self.SetToolTip(wx.ToolTip(self._tooltip)) + self.Bind(wx.EVT_PAINT, self.OnPaint) + self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) + self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown) + self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) + self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel) + + def DoGetBestSize(self): + return wx.Size(self._size, self._size) + + def AcceptsFocusFromKeyboard(self): + return False + + def GetValue(self): + return self._angle, self._length + + def GetAngle(self): + return self._angle + + def GetLength(self): + return self._length + + def SetValue(self, angle=None, length=None): + if angle is not None: + self._angle = min(max(angle, -180), 180) + if length is not None: + self._length = min(max(length, 0), 1) + self.Refresh() + + def SetAngle(self, angle): + self.SetValue(angle, None) + + def SetLength(self, length): + self.SetValue(None, length) + + def OnPaint(self, event): + dc = wx.BufferedPaintDC(self) + self.Draw(dc) + + def Draw(self, dc): + width, height = self.GetClientSize() + if not width or not height: + return + + dc.SetBackground(wx.Brush(self.GetBackgroundColour(), wx.BRUSHSTYLE_SOLID)) + dc.Clear() + dc.SetTextForeground(wx.Colour(0)) + dc.SetFont(self._font) + + radius = min(width, height) / 2 - 2 + dc.SetBrush(wx.WHITE_BRUSH) + dc.DrawCircle(radius + 2, radius + 2, radius) + a = math.radians(self._angle + self._offset) + x = math.sin(a) * radius + y = math.cos(a) * radius + dc.DrawLine(radius + 2, radius + 2, radius + 2 + x * self._length, radius + 2 - y * self._length) + dc.SetBrush(wx.BLACK_BRUSH) + dc.DrawCircle(radius + 2 + x * self._length, radius + 2 - y * self._length, 2) + + if self._label: + labelText = self._label + labelTextW, labelTextH = dc.GetTextExtent(labelText) + labelTextX = (radius * 2 + 4 - labelTextW) if (self._labelpos & 1) else 0 + labelTextY = (radius * 2 + 4 - labelTextH) if (self._labelpos & 2) else 0 + dc.DrawText(labelText, labelTextX, labelTextY) + + lengthText = '%d%%' % (100 * self._length,) + lengthTextW, lengthTextH = dc.GetTextExtent(lengthText) + lengthTextX = radius + 2 + x / 2 - y / 3 - lengthTextW / 2 + lengthTextY = radius + 2 - y / 2 - x / 3 - lengthTextH / 2 + dc.DrawText(lengthText, lengthTextX, lengthTextY) + + angleText = '%d\u00B0' % (self._angle,) + angleTextW, angleTextH = dc.GetTextExtent(angleText) + angleTextX = radius + 2 - x / 2 - angleTextW / 2 + angleTextY = radius + 2 + y / 2 - angleTextH / 2 + dc.DrawText(angleText, angleTextX, angleTextY) + + def OnEraseBackground(self, event): + pass + + def OnLeftDown(self, event): + self._left = True + self.SetToolTip(None) + self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp) + self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnLeftUp) + if not self._right: + self.Bind(wx.EVT_MOTION, self.OnMotion) + if not self.HasCapture(): + self.CaptureMouse() + self.HandleMouseEvent(event) + + def OnRightDown(self, event): + self._right = True + self.SetToolTip(None) + self.Bind(wx.EVT_RIGHT_UP, self.OnRightUp) + self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnRightUp) + if not self._left: + self.Bind(wx.EVT_MOTION, self.OnMotion) + if not self.HasCapture(): + self.CaptureMouse() + self.HandleMouseEvent(event) + + def OnLeftUp(self, event): + self.HandleMouseEvent(event) + self.Unbind(wx.EVT_LEFT_UP, handler=self.OnLeftUp) + self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST, handler=self.OnLeftUp) + self._left = False + if not self._right: + self.Unbind(wx.EVT_MOTION, handler=self.OnMotion) + self.SendChangeEvent() + self.SetToolTip(wx.ToolTip(self._tooltip)) + if self.HasCapture(): + self.ReleaseMouse() + + def OnRightUp(self, event): + self.HandleMouseEvent(event) + self.Unbind(wx.EVT_RIGHT_UP, handler=self.OnRightUp) + self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST, handler=self.OnRightUp) + self._right = False + if not self._left: + self.Unbind(wx.EVT_MOTION, handler=self.OnMotion) + self.SendChangeEvent() + self.SetToolTip(wx.ToolTip(self._tooltip)) + if self.HasCapture(): + self.ReleaseMouse() + + def OnMotion(self, event): + self.HandleMouseEvent(event) + event.Skip() + + def OnWheel(self, event): + amount = 0.1 * event.GetWheelRotation() / event.GetWheelDelta() + self._length = min(max(self._length + amount, 0.0), 1.0) + self.Refresh() + self.SendChangeEvent() + + def HandleMouseEvent(self, event): + width, height = self.GetClientSize() + if width and height: + center = min(width, height) / 2 + x, y = event.GetPositionTuple() + x = x - center + y = center - y + angle = self._angle + length = min((x * x + y * y) ** 0.5 / (center - 2), 1.0) + if length < 0.01: + length = 0 + else: + angle = ((math.degrees(math.atan2(x, y)) - self._offset + 180) % 360) - 180 + if (self._right and not self._left) or event.ShiftDown(): + angle = round(angle / 15.0) * 15.0 + # floor() for length to make it easier to hit 0%, can still hit 100% outside the circle + length = math.floor(length / 0.05) * 0.05 + if (angle != self._angle) or (length != self._length): + self._angle = angle + self._length = length + self.Refresh() + if self._right and not self._left: + self.SendChangeEvent() + + def SendChangeEvent(self): + changeEvent = wx.CommandEvent(self.myEVT_VECTOR_CHANGED, self.GetId()) + changeEvent._object = self + changeEvent._angle = self._angle + changeEvent._length = self._length + self.GetEventHandler().ProcessEvent(changeEvent) diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 6ca90d031..35f8e7a82 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -962,9 +962,9 @@ class MainFrame(wx.Frame): if not self.graphFrame: self.graphFrame = GraphFrame(self) - if graphFrame.graphFrame_enabled: + if graphFrame.frame.graphFrame_enabled: self.graphFrame.Show() - elif graphFrame.graphFrame_enabled: + elif graphFrame.frame.graphFrame_enabled: self.graphFrame.SetFocus() def openWXInspectTool(self, event): diff --git a/gui/mainMenuBar.py b/gui/mainMenuBar.py index 0d8193c01..2da10be26 100644 --- a/gui/mainMenuBar.py +++ b/gui/mainMenuBar.py @@ -102,7 +102,7 @@ class MainMenuBar(wx.MenuBar): graphFrameItem = wx.MenuItem(fitMenu, self.graphFrameId, "&Graphs\tCTRL+G") graphFrameItem.SetBitmap(BitmapLoader.getBitmap("graphs_small", "gui")) fitMenu.Append(graphFrameItem) - if not gui.graphFrame.graphFrame_enabled: + if not gui.graphFrame.frame.graphFrame_enabled: self.Enable(self.graphFrameId, False) self.ignoreRestrictionItem = fitMenu.Append(self.toggleIgnoreRestrictionID, "Disable Fitting Re&strictions") From b224196b05bdbcce5afe528c95d7d23d35110800 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 14 Jun 2019 14:43:42 +0300 Subject: [PATCH 05/93] Get rid of logic which handles legacy versions of matplotlib --- gui/graphFrame/frame.py | 94 ++++++++++++----------------------------- 1 file changed, 26 insertions(+), 68 deletions(-) diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index 6849afa90..a2329977c 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -20,7 +20,6 @@ import os import traceback -from itertools import chain # noinspection PyPackageRequirements import wx @@ -43,31 +42,22 @@ try: mpl_version = int(mpl.__version__[0]) or -1 if mpl_version >= 2: mpl.use('wxagg') - mplImported = True + graphFrame_enabled = True else: - mplImported = False - from matplotlib.patches import Patch + graphFrame_enabled = False + from matplotlib.patches import Patch from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as Canvas from matplotlib.figure import Figure - - graphFrame_enabled = True - mplImported = True except ImportError as e: pyfalog.warning('Matplotlib failed to import. Likely missing or incompatible version.') - mpl_version = -1 - Patch = mpl = Canvas = Figure = None graphFrame_enabled = False - mplImported = False except Exception: # We can get exceptions deep within matplotlib. Catch those. See GH #1046 tb = traceback.format_exc() pyfalog.critical('Exception when importing Matplotlib. Continuing without importing.') pyfalog.critical(tb) - mpl_version = -1 - Patch = mpl = Canvas = Figure = None graphFrame_enabled = False - mplImported = False class GraphFrame(wx.Frame): @@ -75,11 +65,6 @@ class GraphFrame(wx.Frame): def __init__(self, parent, style=wx.DEFAULT_FRAME_STYLE | wx.NO_FULL_REPAINT_ON_RESIZE | wx.FRAME_FLOAT_ON_PARENT): global graphFrame_enabled - global mplImported - global mpl_version - - self.legendFix = False - if not graphFrame_enabled: pyfalog.warning('Matplotlib is not enabled. Skipping initialization.') return @@ -94,16 +79,6 @@ class GraphFrame(wx.Frame): if os.access(cache_dir, os.W_OK | os.X_OK) and os.path.isfile(cache_file): # remove matplotlib font cache, see #234 os.remove(cache_file) - if not mplImported: - mpl.use('wxagg') - - graphFrame_enabled = True - if int(mpl.__version__[0]) < 1: - pyfalog.warning('pyfa: Found matplotlib version {} - activating OVER9000 workarounds'.format(mpl.__version__)) - pyfalog.warning('pyfa: Recommended minimum matplotlib version is 1.0.0') - self.legendFix = True - - mplImported = True wx.Frame.__init__(self, parent, title='pyfa: Graph Generator', style=style, size=(520, 390)) @@ -382,49 +357,32 @@ class GraphFrame(wx.Frame): max_y += 5 self.subplot.set_ylim(bottom=min_y, top=max_y) - if mpl_version < 2: - if self.legendFix and len(legend) > 0: - leg = self.subplot.legend(tuple(legend), 'upper right', shadow=False) - for t in leg.get_texts(): - t.set_fontsize('small') + legend2 = [] + legend_colors = { + 0: 'blue', + 1: 'orange', + 2: 'green', + 3: 'red', + 4: 'purple', + 5: 'brown', + 6: 'pink', + 7: 'grey', + } - for l in leg.get_lines(): - l.set_linewidth(1) + for i, i_name in enumerate(legend): + try: + selected_color = legend_colors[i] + except: + selected_color = None + legend2.append(Patch(color=selected_color, label=i_name), ) - elif not self.legendFix and len(legend) > 0: - leg = self.subplot.legend(tuple(legend), 'upper right', shadow=False, frameon=False) - for t in leg.get_texts(): - t.set_fontsize('small') + if len(legend2) > 0: + leg = self.subplot.legend(handles=legend2) + for t in leg.get_texts(): + t.set_fontsize('small') - for l in leg.get_lines(): - l.set_linewidth(1) - elif mpl_version >= 2: - legend2 = [] - legend_colors = { - 0: 'blue', - 1: 'orange', - 2: 'green', - 3: 'red', - 4: 'purple', - 5: 'brown', - 6: 'pink', - 7: 'grey', - } - - for i, i_name in enumerate(legend): - try: - selected_color = legend_colors[i] - except: - selected_color = None - legend2.append(Patch(color=selected_color, label=i_name), ) - - if len(legend2) > 0: - leg = self.subplot.legend(handles=legend2) - for t in leg.get_texts(): - t.set_fontsize('small') - - for l in leg.get_lines(): - l.set_linewidth(1) + for l in leg.get_lines(): + l.set_linewidth(1) self.canvas.draw() self.SetStatusText('') From 738d7f687d83d38d531f67b321819107f948091b Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 14 Jun 2019 16:38:41 +0300 Subject: [PATCH 06/93] Move more code to control panel file --- gui/graphFrame/frame.py | 38 ++++++++++++++++---------------------- gui/graphFrame/panel.py | 1 + 2 files changed, 17 insertions(+), 22 deletions(-) diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index a2329977c..621a8e55f 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -69,15 +69,13 @@ class GraphFrame(wx.Frame): pyfalog.warning('Matplotlib is not enabled. Skipping initialization.') return + # Remove matplotlib font cache, see #234 try: cache_dir = mpl._get_cachedir() except: cache_dir = os.path.expanduser(os.path.join('~', '.matplotlib')) - cache_file = os.path.join(cache_dir, 'fontList.cache') - if os.access(cache_dir, os.W_OK | os.X_OK) and os.path.isfile(cache_file): - # remove matplotlib font cache, see #234 os.remove(cache_file) wx.Frame.__init__(self, parent, title='pyfa: Graph Generator', style=style, size=(520, 390)) @@ -85,35 +83,32 @@ class GraphFrame(wx.Frame): i = wx.Icon(BitmapLoader.getBitmap('graphs_small', 'gui')) self.SetIcon(i) self.mainFrame = gui.mainFrame.MainFrame.getInstance() - self.CreateStatusBar() - self.mainSizer = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(self.mainSizer) + mainSizer = wx.BoxSizer(wx.VERTICAL) + # Graph selector self.graphSelection = wx.Choice(self, wx.ID_ANY, style=0) - self.mainSizer.Add(self.graphSelection, 0, wx.EXPAND) - self.selectedYRbMap = {} + mainSizer.Add(self.graphSelection, 0, wx.EXPAND) + # Plot area self.figure = Figure(figsize=(5, 3), tight_layout={'pad': 1.08}) - rgbtuple = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE).Get() clr = [c / 255. for c in rgbtuple] self.figure.set_facecolor(clr) self.figure.set_edgecolor(clr) - self.canvas = Canvas(self, -1, self.figure) self.canvas.SetBackgroundColour(wx.Colour(*rgbtuple)) - self.subplot = self.figure.add_subplot(111) self.subplot.grid(True) + mainSizer.Add(self.canvas, 1, wx.EXPAND) - self.mainSizer.Add(self.canvas, 1, wx.EXPAND) - self.mainSizer.Add(wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, - wx.EXPAND) - + mainSizer.Add(wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, wx.EXPAND) + # Graph control panel self.ctrlPanel = GraphControlPanel(self, self) - self.mainSizer.Add(self.ctrlPanel, 0, wx.EXPAND | wx.ALL, 0) + mainSizer.Add(self.ctrlPanel, 0, wx.EXPAND | wx.ALL, 0) + + self.drawTimer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self.draw, self.drawTimer) @@ -126,7 +121,7 @@ class GraphFrame(wx.Frame): self.fields = {} self.updateGraphWidgets() self.sl1 = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) - self.mainSizer.Add(self.sl1, 0, wx.EXPAND) + mainSizer.Add(self.sl1, 0, wx.EXPAND) self.ctrlPanel.fitList.fitList.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick) @@ -145,6 +140,7 @@ class GraphFrame(wx.Frame): self.contextMenu.Append(removeItem) self.contextMenu.Bind(wx.EVT_MENU, self.ContextMenuHandler, removeItem) + self.SetSizer(mainSizer) self.Fit() self.SetMinSize(self.GetSize()) @@ -228,7 +224,7 @@ class GraphFrame(wx.Frame): event.Skip() obj = event.GetEventObject() formatName = obj.GetLabel() - self.ctrlPanel.selectedY = self.selectedYRbMap[formatName] + self.ctrlPanel.selectedY = self.ctrlPanel.selectedYRbMap[formatName] self.draw() def updateGraphWidgets(self): @@ -242,7 +238,7 @@ class GraphFrame(wx.Frame): self.fields.clear() # Setup view options - self.selectedYRbMap.clear() + self.ctrlPanel.selectedYRbMap.clear() if len(view.yDefs) > 1: i = 0 for yAlias, yDef in view.yDefs.items(): @@ -254,7 +250,7 @@ class GraphFrame(wx.Frame): if i == (self.ctrlPanel.selectedY or 0): rdo.SetValue(True) self.ctrlPanel.graphSubselSizer.Add(rdo, 0, wx.ALL | wx.EXPAND, 0) - self.selectedYRbMap[yDef.switchLabel] = i + self.ctrlPanel.selectedYRbMap[yDef.switchLabel] = i i += 1 # Setup inputs @@ -342,7 +338,6 @@ class GraphFrame(wx.Frame): legend.append('{} ({})'.format(fit.name, fit.ship.item.getShortName())) except Exception as ex: pyfalog.warning('Invalid values in "{0}"', fit.name) - self.SetStatusText('Invalid values in "%s"' % fit.name) self.canvas.draw() return @@ -385,7 +380,6 @@ class GraphFrame(wx.Frame): l.set_linewidth(1) self.canvas.draw() - self.SetStatusText('') self.Refresh() def onFieldChanged(self, event): diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index e7db62034..82d887cec 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -32,6 +32,7 @@ class GraphControlPanel(wx.Panel): self.graphFrame = graphFrame self.selectedY = None + self.selectedYRbMap = {} mainSizer = wx.BoxSizer(wx.VERTICAL) From 3359d8cb88cd60af01c0e3677ec4c9b5e22bf420 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 14 Jun 2019 18:26:21 +0300 Subject: [PATCH 07/93] More code to control panel --- gui/graphFrame/frame.py | 78 +++-------------------------------------- gui/graphFrame/panel.py | 77 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 73 deletions(-) diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index 621a8e55f..5040d17d4 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -31,6 +31,7 @@ import gui.mainFrame from gui.bitmap_loader import BitmapLoader from gui.builtinGraphs.base import Graph from service.fit import Fit +from .events import REFRESH_GRAPH from .panel import GraphControlPanel @@ -108,17 +109,11 @@ class GraphFrame(wx.Frame): self.ctrlPanel = GraphControlPanel(self, self) mainSizer.Add(self.ctrlPanel, 0, wx.EXPAND | wx.ALL, 0) - - - self.drawTimer = wx.Timer(self) - self.Bind(wx.EVT_TIMER, self.draw, self.drawTimer) - for view in Graph.views: view = view() self.graphSelection.Append(view.name, view) self.graphSelection.SetSelection(0) - self.fields = {} self.updateGraphWidgets() self.sl1 = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) mainSizer.Add(self.sl1, 0, wx.EXPAND) @@ -126,6 +121,7 @@ class GraphFrame(wx.Frame): self.ctrlPanel.fitList.fitList.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick) self.ctrlPanel.fitList.fitList.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu) + self.Bind(REFRESH_GRAPH, self.OnRefreshGraph) self.ctrlPanel.showY0Cb.Bind(wx.EVT_CHECKBOX, self.OnNonDestructiveControlsUpdate) self.mainFrame.Bind(GE.FIT_CHANGED, self.OnFitChanged) self.mainFrame.Bind(GE.FIT_REMOVED, self.OnFitRemoved) @@ -211,7 +207,7 @@ class GraphFrame(wx.Frame): def getValues(self): values = {} - for fieldHandle, field in self.fields.items(): + for fieldHandle, field in self.ctrlPanel.fields.items(): values[fieldHandle] = field.GetValue() return values @@ -220,71 +216,14 @@ class GraphFrame(wx.Frame): event.Skip() self.draw() - def OnYTypeUpdate(self, event): - event.Skip() - obj = event.GetEventObject() - formatName = obj.GetLabel() - self.ctrlPanel.selectedY = self.ctrlPanel.selectedYRbMap[formatName] + def OnRefreshGraph(self, event): self.draw() def updateGraphWidgets(self): view = self.getView() - view.clearCache() - self.ctrlPanel.graphSubselSizer.Clear() - self.ctrlPanel.inputsSizer.Clear() - for child in self.ctrlPanel.Children: - if child not in (self.ctrlPanel.showY0Cb, self.ctrlPanel.fitList, self.ctrlPanel.targetList): - child.Destroy() - self.fields.clear() - - # Setup view options - self.ctrlPanel.selectedYRbMap.clear() - if len(view.yDefs) > 1: - i = 0 - for yAlias, yDef in view.yDefs.items(): - if i == 0: - rdo = wx.RadioButton(self.ctrlPanel, wx.ID_ANY, yDef.switchLabel, style=wx.RB_GROUP) - else: - rdo = wx.RadioButton(self.ctrlPanel, wx.ID_ANY, yDef.switchLabel) - rdo.Bind(wx.EVT_RADIOBUTTON, self.OnYTypeUpdate) - if i == (self.ctrlPanel.selectedY or 0): - rdo.SetValue(True) - self.ctrlPanel.graphSubselSizer.Add(rdo, 0, wx.ALL | wx.EXPAND, 0) - self.ctrlPanel.selectedYRbMap[yDef.switchLabel] = i - i += 1 - - # Setup inputs - for fieldHandle, fieldDef in (('x', view.xDef), *view.extraInputs.items()): - textBox = wx.TextCtrl(self.ctrlPanel, wx.ID_ANY, style=0) - self.fields[fieldHandle] = textBox - textBox.Bind(wx.EVT_TEXT, self.onFieldChanged) - self.ctrlPanel.inputsSizer.Add(textBox, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 3) - if fieldDef.inputDefault is not None: - inputDefault = fieldDef.inputDefault - if not isinstance(inputDefault, str): - inputDefault = ('%f' % inputDefault).rstrip('0') - if inputDefault[-1:] == '.': - inputDefault += '0' - - textBox.ChangeValue(inputDefault) - - imgLabelSizer = wx.BoxSizer(wx.HORIZONTAL) - if fieldDef.inputIconID: - icon = BitmapLoader.getBitmap(fieldDef.inputIconID, 'icons') - if icon is not None: - static = wx.StaticBitmap(self.ctrlPanel) - static.SetBitmap(icon) - imgLabelSizer.Add(static, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 1) - - imgLabelSizer.Add(wx.StaticText(self.ctrlPanel, wx.ID_ANY, fieldDef.inputLabel), 0, - wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 3) - self.ctrlPanel.inputsSizer.Add(imgLabelSizer, 0, wx.ALIGN_CENTER_VERTICAL) - self.Layout() + self.ctrlPanel.updateControlsForView(view) self.draw() - def delayedDraw(self, event=None): - self.drawTimer.Stop() - self.drawTimer.Start(Fit.getInstance().serviceFittingOptions['marketSearchDelay'], True) def draw(self, event=None): global mpl_version @@ -292,8 +231,6 @@ class GraphFrame(wx.Frame): if event is not None: event.Skip() - self.drawTimer.Stop() - # todo: FIX THIS, see #1430. draw() is not being unbound properly when the window closes, this is an easy fix, # but not a proper solution if not self: @@ -382,11 +319,6 @@ class GraphFrame(wx.Frame): self.canvas.draw() self.Refresh() - def onFieldChanged(self, event): - view = self.getView() - view.clearCache() - self.delayedDraw() - def AppendFitToList(self, fitID): sFit = Fit.getInstance() fit = sFit.getFit(fitID) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 82d887cec..a37a380b7 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -21,7 +21,9 @@ # noinspection PyPackageRequirements import wx +from gui.bitmap_loader import BitmapLoader from service.fit import Fit +from .events import RefreshGraph from .lists import FitList, TargetList @@ -31,8 +33,11 @@ class GraphControlPanel(wx.Panel): super().__init__(parent) self.graphFrame = graphFrame + self.fields = {} self.selectedY = None self.selectedYRbMap = {} + self.drawTimer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self.redrawRequest, self.drawTimer) mainSizer = wx.BoxSizer(wx.VERTICAL) @@ -69,3 +74,75 @@ class GraphControlPanel(wx.Panel): def showY0(self): return self.showY0Cb.GetValue() + def updateControlsForView(self, view): + view.clearCache() + self.graphSubselSizer.Clear() + self.inputsSizer.Clear() + for child in self.Children: + if child not in (self.showY0Cb, self.fitList, self.targetList): + child.Destroy() + self.fields.clear() + + # Setup view options + self.selectedYRbMap.clear() + if len(view.yDefs) > 1: + i = 0 + for yAlias, yDef in view.yDefs.items(): + if i == 0: + rdo = wx.RadioButton(self, wx.ID_ANY, yDef.switchLabel, style=wx.RB_GROUP) + else: + rdo = wx.RadioButton(self, wx.ID_ANY, yDef.switchLabel) + rdo.Bind(wx.EVT_RADIOBUTTON, self.OnYTypeUpdate) + if i == (self.selectedY or 0): + rdo.SetValue(True) + self.graphSubselSizer.Add(rdo, 0, wx.ALL | wx.EXPAND, 0) + self.selectedYRbMap[yDef.switchLabel] = i + i += 1 + + # Setup inputs + for fieldHandle, fieldDef in (('x', view.xDef), *view.extraInputs.items()): + textBox = wx.TextCtrl(self, wx.ID_ANY, style=0) + self.fields[fieldHandle] = textBox + textBox.Bind(wx.EVT_TEXT, self.onFieldChanged) + self.inputsSizer.Add(textBox, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 3) + if fieldDef.inputDefault is not None: + inputDefault = fieldDef.inputDefault + if not isinstance(inputDefault, str): + inputDefault = ('%f' % inputDefault).rstrip('0') + if inputDefault[-1:] == '.': + inputDefault += '0' + + textBox.ChangeValue(inputDefault) + + imgLabelSizer = wx.BoxSizer(wx.HORIZONTAL) + if fieldDef.inputIconID: + icon = BitmapLoader.getBitmap(fieldDef.inputIconID, 'icons') + if icon is not None: + static = wx.StaticBitmap(self) + static.SetBitmap(icon) + imgLabelSizer.Add(static, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 1) + + imgLabelSizer.Add(wx.StaticText(self, wx.ID_ANY, fieldDef.inputLabel), 0, + wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 3) + self.inputsSizer.Add(imgLabelSizer, 0, wx.ALIGN_CENTER_VERTICAL) + self.Layout() + + def OnYTypeUpdate(self, event): + event.Skip() + obj = event.GetEventObject() + formatName = obj.GetLabel() + self.selectedY = self.selectedYRbMap[formatName] + self.redrawRequest() + + def redrawRequest(self, event=None): + self.drawTimer.Stop() + wx.PostEvent(self.graphFrame, RefreshGraph()) + + def delayedDraw(self, event=None): + self.drawTimer.Stop() + self.drawTimer.Start(Fit.getInstance().serviceFittingOptions['marketSearchDelay'], True) + + def onFieldChanged(self, event): + view = self.graphFrame.getView() + view.clearCache() + self.delayedDraw() From 4ca3f10bc947d34347c76ce40de683072fca28a0 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 18 Jun 2019 16:12:27 +0300 Subject: [PATCH 08/93] Move more stuff away from the frame --- gui/graphFrame/events.py | 24 ------ gui/graphFrame/frame.py | 154 +++++++-------------------------------- gui/graphFrame/lists.py | 131 +++++++++++++++++++++++++-------- gui/graphFrame/panel.py | 51 +++++++------ 4 files changed, 158 insertions(+), 202 deletions(-) delete mode 100644 gui/graphFrame/events.py diff --git a/gui/graphFrame/events.py b/gui/graphFrame/events.py deleted file mode 100644 index 2adab83b1..000000000 --- a/gui/graphFrame/events.py +++ /dev/null @@ -1,24 +0,0 @@ -# ============================================================================= -# Copyright (C) 2010 Diego Duclos -# -# 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 . -# ============================================================================= - - -import wx.lib.newevent - - -RefreshGraph, REFRESH_GRAPH = wx.lib.newevent.NewEvent() diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index 5040d17d4..da8853b83 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -30,8 +30,6 @@ import gui.globalEvents as GE import gui.mainFrame from gui.bitmap_loader import BitmapLoader from gui.builtinGraphs.base import Graph -from service.fit import Fit -from .events import REFRESH_GRAPH from .panel import GraphControlPanel @@ -70,6 +68,11 @@ class GraphFrame(wx.Frame): pyfalog.warning('Matplotlib is not enabled. Skipping initialization.') return + wx.Frame.__init__(self, parent, title='pyfa: Graph Generator', style=style, size=(520, 390)) + self.mainFrame = gui.mainFrame.MainFrame.getInstance() + + self.SetIcon(wx.Icon(BitmapLoader.getBitmap('graphs_small', 'gui'))) + # Remove matplotlib font cache, see #234 try: cache_dir = mpl._get_cachedir() @@ -79,19 +82,14 @@ class GraphFrame(wx.Frame): if os.access(cache_dir, os.W_OK | os.X_OK) and os.path.isfile(cache_file): os.remove(cache_file) - wx.Frame.__init__(self, parent, title='pyfa: Graph Generator', style=style, size=(520, 390)) - - i = wx.Icon(BitmapLoader.getBitmap('graphs_small', 'gui')) - self.SetIcon(i) - self.mainFrame = gui.mainFrame.MainFrame.getInstance() - mainSizer = wx.BoxSizer(wx.VERTICAL) - # Graph selector + # Layout - graph selector self.graphSelection = wx.Choice(self, wx.ID_ANY, style=0) + self.graphSelection.Bind(wx.EVT_CHOICE, self.OnGraphSwitched) mainSizer.Add(self.graphSelection, 0, wx.EXPAND) - # Plot area + # Layout - plot area self.figure = Figure(figsize=(5, 3), tight_layout={'pad': 1.08}) rgbtuple = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE).Get() clr = [c / 255. for c in rgbtuple] @@ -105,45 +103,29 @@ class GraphFrame(wx.Frame): mainSizer.Add(wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL), 0, wx.EXPAND) - # Graph control panel + # Layout - graph control panel self.ctrlPanel = GraphControlPanel(self, self) mainSizer.Add(self.ctrlPanel, 0, wx.EXPAND | wx.ALL, 0) + # Setup - graph selector for view in Graph.views: - view = view() - self.graphSelection.Append(view.name, view) - + self.graphSelection.Append(view.name, view()) self.graphSelection.SetSelection(0) - self.updateGraphWidgets() - self.sl1 = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) - mainSizer.Add(self.sl1, 0, wx.EXPAND) + self.ctrlPanel.updateControlsForView(self.getView()) - - self.ctrlPanel.fitList.fitList.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick) - self.ctrlPanel.fitList.fitList.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu) - self.Bind(REFRESH_GRAPH, self.OnRefreshGraph) - self.ctrlPanel.showY0Cb.Bind(wx.EVT_CHECKBOX, self.OnNonDestructiveControlsUpdate) - self.mainFrame.Bind(GE.FIT_CHANGED, self.OnFitChanged) - self.mainFrame.Bind(GE.FIT_REMOVED, self.OnFitRemoved) + # Event bindings self.Bind(wx.EVT_CLOSE, self.closeEvent) self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent) - self.Bind(wx.EVT_CHOICE, self.graphChanged) + self.mainFrame.Bind(GE.FIT_CHANGED, self.OnFitChanged) from gui.builtinStatsViews.resistancesViewFull import EFFECTIVE_HP_TOGGLED # Grr crclar gons self.mainFrame.Bind(EFFECTIVE_HP_TOGGLED, self.OnEhpToggled) - self.contextMenu = wx.Menu() - removeItem = wx.MenuItem(self.contextMenu, 1, 'Remove Fit') - self.contextMenu.Append(removeItem) - self.contextMenu.Bind(wx.EVT_MENU, self.ContextMenuHandler, removeItem) - self.SetSizer(mainSizer) + + self.draw() self.Fit() self.SetMinSize(self.GetSize()) - def handleDrag(self, type, fitID): - if type == 'fit': - self.AppendFitToList(fitID) - def closeEvent(self, event): self.closeWindow() event.Skip() @@ -154,22 +136,8 @@ class GraphFrame(wx.Frame): if keycode == wx.WXK_ESCAPE and mstate.GetModifiers() == wx.MOD_NONE: self.closeWindow() return - elif keycode == 65 and mstate.GetModifiers() == wx.MOD_CONTROL: - self.ctrlPanel.fitList.fitList.selectAll() - elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and mstate.GetModifiers() == wx.MOD_NONE: - self.removeFits(self.getSelectedFits()) event.Skip() - def OnContextMenu(self, event): - if self.getSelectedFits(): - self.PopupMenu(self.contextMenu) - - def ContextMenuHandler(self, event): - selectedMenuItem = event.GetId() - if selectedMenuItem == 1: # Copy was chosen - fits = self.getSelectedFits() - self.removeFits(fits) - def OnEhpToggled(self, event): event.Skip() view = self.getView() @@ -179,65 +147,38 @@ class GraphFrame(wx.Frame): def OnFitChanged(self, event): event.Skip() - view = self.getView() - view.clearCache(key=event.fitID) + self.getView().clearCache(key=event.fitID) self.draw() - def OnFitRemoved(self, event): - event.Skip() - fit = next((f for f in self.ctrlPanel.fits if f.ID == event.fitID), None) - if fit is not None: - self.removeFits([fit]) - - def graphChanged(self, event): - self.ctrlPanel.selectedY = None - self.updateGraphWidgets() + def OnGraphSwitched(self, event): + self.clearCache() + self.ctrlPanel.updateControlsForView(self.getView()) + self.draw() event.Skip() def closeWindow(self): from gui.builtinStatsViews.resistancesViewFull import EFFECTIVE_HP_TOGGLED # Grr gons - self.ctrlPanel.fitList.fitList.Unbind(wx.EVT_LEFT_DCLICK, handler=self.OnLeftDClick) self.mainFrame.Unbind(GE.FIT_CHANGED, handler=self.OnFitChanged) - self.mainFrame.Unbind(GE.FIT_REMOVED, handler=self.OnFitRemoved) self.mainFrame.Unbind(EFFECTIVE_HP_TOGGLED, handler=self.OnEhpToggled) + self.ctrlPanel.unbindExternalEvents() self.Destroy() def getView(self): return self.graphSelection.GetClientData(self.graphSelection.GetSelection()) - def getValues(self): - values = {} - for fieldHandle, field in self.ctrlPanel.fields.items(): - values[fieldHandle] = field.GetValue() + def clearCache(self, key=None): + self.getView().clearCache(key=key) - return values - - def OnNonDestructiveControlsUpdate(self, event): - event.Skip() - self.draw() - - def OnRefreshGraph(self, event): - self.draw() - - def updateGraphWidgets(self): - view = self.getView() - self.ctrlPanel.updateControlsForView(view) - self.draw() - - - def draw(self, event=None): + def draw(self): global mpl_version - if event is not None: - event.Skip() - # todo: FIX THIS, see #1430. draw() is not being unbound properly when the window closes, this is an easy fix, # but not a proper solution if not self: pyfalog.warning('GraphFrame handled event, however GraphFrame no longer exists. Ignoring event') return - values = self.getValues() + values = self.ctrlPanel.getValues() view = self.getView() self.subplot.clear() self.subplot.grid(True) @@ -255,7 +196,7 @@ class GraphFrame(wx.Frame): self.subplot.set(xlabel=view.xDef.axisLabel, ylabel=view.yDefs[chosenY].axisLabel) - for fit in self.ctrlPanel.fits: + for fit in self.ctrlPanel.fitList.fits: try: xs, ys = view.getPlotPoints(fit, extraInputs, xRange, 100, chosenY) @@ -318,44 +259,3 @@ class GraphFrame(wx.Frame): self.canvas.draw() self.Refresh() - - def AppendFitToList(self, fitID): - sFit = Fit.getInstance() - fit = sFit.getFit(fitID) - if fit not in self.ctrlPanel.fits: - self.ctrlPanel.fits.append(fit) - - self.ctrlPanel.fitList.fitList.update(self.ctrlPanel.fits) - self.draw() - - def OnLeftDClick(self, event): - row, _ = self.ctrlPanel.fitList.fitList.HitTest(event.Position) - if row != -1: - try: - fit = self.ctrlPanel.fits[row] - except IndexError: - pass - else: - self.removeFits([fit]) - - def removeFits(self, fits): - toRemove = [f for f in fits if f in self.ctrlPanel.fits] - if not toRemove: - return - for fit in toRemove: - self.ctrlPanel.fits.remove(fit) - self.ctrlPanel.fitList.fitList.update(self.ctrlPanel.fits) - view = self.getView() - for fit in fits: - view.clearCache(key=fit.ID) - self.draw() - - def getSelectedFits(self): - fits = [] - for row in self.ctrlPanel.fitList.fitList.getSelectedRows(): - try: - fit = self.ctrlPanel.fits[row] - except IndexError: - continue - fits.append(fit) - return fits diff --git a/gui/graphFrame/lists.py b/gui/graphFrame/lists.py index 4a89650ec..4956f23ab 100644 --- a/gui/graphFrame/lists.py +++ b/gui/graphFrame/lists.py @@ -22,47 +22,118 @@ import wx import gui.display +import gui.globalEvents as GE +from service.fit import Fit -class FitList(wx.Panel): +class FitList(gui.display.Display): - def __init__(self, parent): - wx.Panel.__init__(self, parent) - self.mainSizer = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(self.mainSizer) + DEFAULT_COLS = ( + 'Base Icon', + 'Base Name') + + def __init__(self, graphFrame, parent): + super().__init__(parent) + self.graphFrame = graphFrame + self.fits = [] - self.fitList = FitDisplay(self) - self.mainSizer.Add(self.fitList, 1, wx.EXPAND) fitToolTip = wx.ToolTip('Drag a fit into this list to graph it') - self.fitList.SetToolTip(fitToolTip) + self.SetToolTip(fitToolTip) + + fit = Fit.getInstance().getFit(self.graphFrame.mainFrame.getActiveFit()) + if fit is not None: + self.fits.append(fit) + self.update(self.fits) + + self.contextMenu = wx.Menu() + removeItem = wx.MenuItem(self.contextMenu, 1, 'Remove Fit') + self.contextMenu.Append(removeItem) + self.contextMenu.Bind(wx.EVT_MENU, self.ContextMenuHandler, removeItem) + + self.graphFrame.mainFrame.Bind(GE.FIT_REMOVED, self.OnFitRemoved) + self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick) + self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent) + self.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu) -class FitDisplay(gui.display.Display): + def kbEvent(self, event): + keycode = event.GetKeyCode() + mstate = wx.GetMouseState() + if keycode == 65 and mstate.GetModifiers() == wx.MOD_CONTROL: + self.selectAll() + elif keycode in (wx.WXK_DELETE, wx.WXK_NUMPAD_DELETE) and mstate.GetModifiers() == wx.MOD_NONE: + self.removeFits(self.getSelectedFits()) + event.Skip() - DEFAULT_COLS = ['Base Icon', - 'Base Name'] + def OnLeftDClick(self, event): + row, _ = self.HitTest(event.Position) + if row != -1: + try: + fit = self.fits[row] + except IndexError: + pass + else: + self.removeFits([fit]) - def __init__(self, parent): - gui.display.Display.__init__(self, parent) + def OnContextMenu(self, event): + if self.getSelectedFits(): + self.PopupMenu(self.contextMenu) + + def ContextMenuHandler(self, event): + selectedMenuItem = event.GetId() + if selectedMenuItem == 1: + fits = self.getSelectedFits() + self.removeFits(fits) + + def OnFitRemoved(self, event): + event.Skip() + fit = next((f for f in self.fits if f.ID == event.fitID), None) + if fit is not None: + self.removeFits([fit]) + + def getSelectedFits(self): + fits = [] + for row in self.getSelectedRows(): + try: + fit = self.fits[row] + except IndexError: + continue + fits.append(fit) + return fits + + def removeFits(self, fits): + toRemove = [f for f in fits if f in self.fits] + if not toRemove: + return + for fit in toRemove: + self.fits.remove(fit) + self.update(self.fits) + for fit in fits: + self.graphFrame.clearCache(key=fit.ID) + self.graphFrame.draw() + + def unbindExternalEvents(self): + self.graphFrame.mainFrame.Unbind(GE.FIT_REMOVED, handler=self.OnFitRemoved) + + def handleDrag(self, type, fitID): + if type == 'fit': + sFit = Fit.getInstance() + fit = sFit.getFit(fitID) + if fit not in self.fits: + self.fits.append(fit) + self.update(self.fits) + self.graphFrame.draw() -class TargetList(wx.Panel): +class TargetList(gui.display.Display): - def __init__(self, parent): - wx.Panel.__init__(self, parent) - self.mainSizer = wx.BoxSizer(wx.VERTICAL) - self.SetSizer(self.mainSizer) + DEFAULT_COLS = ( + 'Base Icon', + 'Base Name') - self.targetList = TargetDisplay(self) - self.mainSizer.Add(self.targetList, 1, wx.EXPAND) + def __init__(self, graphFrame, parent): + super().__init__(parent) + self.graphFrame = graphFrame + self.targetFits = [] fitToolTip = wx.ToolTip('Drag a fit into this list to graph it') - self.targetList.SetToolTip(fitToolTip) - - -class TargetDisplay(gui.display.Display): - - DEFAULT_COLS = ['Base Icon', - 'Base Name'] - - def __init__(self, parent): - gui.display.Display.__init__(self, parent) + self.SetToolTip(fitToolTip) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index a37a380b7..3e9c46c2b 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -23,7 +23,6 @@ import wx from gui.bitmap_loader import BitmapLoader from service.fit import Fit -from .events import RefreshGraph from .lists import FitList, TargetList @@ -36,8 +35,6 @@ class GraphControlPanel(wx.Panel): self.fields = {} self.selectedY = None self.selectedYRbMap = {} - self.drawTimer = wx.Timer(self) - self.Bind(wx.EVT_TIMER, self.redrawRequest, self.drawTimer) mainSizer = wx.BoxSizer(wx.VERTICAL) @@ -45,6 +42,7 @@ class GraphControlPanel(wx.Panel): viewOptSizer = wx.BoxSizer(wx.VERTICAL) self.showY0Cb = wx.CheckBox(self, wx.ID_ANY, 'Always show Y = 0', wx.DefaultPosition, wx.DefaultSize, 0) self.showY0Cb.SetValue(True) + self.showY0Cb.Bind(wx.EVT_CHECKBOX, self.OnShowY0Change) viewOptSizer.Add(self.showY0Cb, 0, wx.LEFT | wx.TOP | wx.RIGHT | wx.EXPAND, 5) self.graphSubselSizer = wx.BoxSizer(wx.VERTICAL) viewOptSizer.Add(self.graphSubselSizer, 0, wx.ALL | wx.EXPAND, 5) @@ -55,27 +53,33 @@ class GraphControlPanel(wx.Panel): mainSizer.Add(paramSizer, 0, wx.EXPAND | wx.ALL, 0) srcTgtSizer = wx.BoxSizer(wx.HORIZONTAL) - fit = Fit.getInstance().getFit(self.graphFrame.mainFrame.getActiveFit()) - self.fits = [fit] if fit is not None else [] - self.fitList = FitList(self) + self.fitList = FitList(graphFrame, self) self.fitList.SetMinSize((270, -1)) - self.fitList.fitList.update(self.fits) srcTgtSizer.Add(self.fitList, 1, wx.EXPAND) self.targets = [] - self.targetList = TargetList(self) + self.targetList = TargetList(graphFrame, self) self.targetList.SetMinSize((270, -1)) - self.targetList.targetList.update(self.targets) + self.targetList.update(self.targets) srcTgtSizer.Add(self.targetList, 1, wx.EXPAND) mainSizer.Add(srcTgtSizer, 1, wx.EXPAND | wx.ALL, 0) self.SetSizer(mainSizer) + self.drawTimer = wx.Timer(self) + self.Bind(wx.EVT_TIMER, self.OnDrawTimer, self.drawTimer) + + def getValues(self): + values = {} + for fieldHandle, field in self.fields.items(): + values[fieldHandle] = field.GetValue() + return values + @property def showY0(self): return self.showY0Cb.GetValue() def updateControlsForView(self, view): - view.clearCache() + self.selectedY = None self.graphSubselSizer.Clear() self.inputsSizer.Clear() for child in self.Children: @@ -103,7 +107,7 @@ class GraphControlPanel(wx.Panel): for fieldHandle, fieldDef in (('x', view.xDef), *view.extraInputs.items()): textBox = wx.TextCtrl(self, wx.ID_ANY, style=0) self.fields[fieldHandle] = textBox - textBox.Bind(wx.EVT_TEXT, self.onFieldChanged) + textBox.Bind(wx.EVT_TEXT, self.OnFieldChanged) self.inputsSizer.Add(textBox, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 3) if fieldDef.inputDefault is not None: inputDefault = fieldDef.inputDefault @@ -127,22 +131,27 @@ class GraphControlPanel(wx.Panel): self.inputsSizer.Add(imgLabelSizer, 0, wx.ALIGN_CENTER_VERTICAL) self.Layout() + def OnShowY0Change(self, event): + event.Skip() + self.graphFrame.draw() + def OnYTypeUpdate(self, event): event.Skip() obj = event.GetEventObject() formatName = obj.GetLabel() self.selectedY = self.selectedYRbMap[formatName] - self.redrawRequest() + self.graphFrame.draw() - def redrawRequest(self, event=None): - self.drawTimer.Stop() - wx.PostEvent(self.graphFrame, RefreshGraph()) - - def delayedDraw(self, event=None): + def OnFieldChanged(self, event): + event.Skip() self.drawTimer.Stop() self.drawTimer.Start(Fit.getInstance().serviceFittingOptions['marketSearchDelay'], True) - def onFieldChanged(self, event): - view = self.graphFrame.getView() - view.clearCache() - self.delayedDraw() + def OnDrawTimer(self, event): + event.Skip() + self.graphFrame.clearCache() + self.graphFrame.draw() + + def unbindExternalEvents(self): + self.fitList.unbindExternalEvents() + From 30d03f0ab5bccac0e0725124b96c764d4ee0ea97 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 18 Jun 2019 16:38:10 +0300 Subject: [PATCH 09/93] Do not crash with Show y = 0 disabled and no fits --- gui/graphFrame/frame.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index da8853b83..1c6f419eb 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -107,21 +107,22 @@ class GraphFrame(wx.Frame): self.ctrlPanel = GraphControlPanel(self, self) mainSizer.Add(self.ctrlPanel, 0, wx.EXPAND | wx.ALL, 0) + self.SetSizer(mainSizer) + # Setup - graph selector for view in Graph.views: self.graphSelection.Append(view.name, view()) self.graphSelection.SetSelection(0) self.ctrlPanel.updateControlsForView(self.getView()) - # Event bindings + # Event bindings - local events self.Bind(wx.EVT_CLOSE, self.closeEvent) self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent) + # Event bindings - external events self.mainFrame.Bind(GE.FIT_CHANGED, self.OnFitChanged) from gui.builtinStatsViews.resistancesViewFull import EFFECTIVE_HP_TOGGLED # Grr crclar gons self.mainFrame.Bind(EFFECTIVE_HP_TOGGLED, self.OnEhpToggled) - self.SetSizer(mainSizer) - self.draw() self.Fit() self.SetMinSize(self.GetSize()) @@ -172,8 +173,8 @@ class GraphFrame(wx.Frame): def draw(self): global mpl_version - # todo: FIX THIS, see #1430. draw() is not being unbound properly when the window closes, this is an easy fix, - # but not a proper solution + # Eee #1430. draw() is not being unbound properly when the window closes. + # This is an easy fix, but not a proper solution if not self: pyfalog.warning('GraphFrame handled event, however GraphFrame no longer exists. Ignoring event') return @@ -219,6 +220,12 @@ class GraphFrame(wx.Frame): self.canvas.draw() return + # Special case for when we do not show Y = 0 and have no fits + if min_y is None: + min_y = 0 + if max_y is None: + max_y = 0 + # Extend range a little for some visual space y_range = max_y - min_y min_y -= y_range * 0.05 max_y += y_range * 0.05 From 2c1905f04112ca5a73947690dd12248a642f1df6 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 20 Jun 2019 00:02:02 +0300 Subject: [PATCH 10/93] Add vectors to panel (not yet functional) --- gui/graphFrame/panel.py | 7 ++++++- gui/graphFrame/vector.py | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 3e9c46c2b..a28f95e3f 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -24,6 +24,7 @@ import wx from gui.bitmap_loader import BitmapLoader from service.fit import Fit from .lists import FitList, TargetList +from .vector import VectorPicker class GraphControlPanel(wx.Panel): @@ -56,6 +57,10 @@ class GraphControlPanel(wx.Panel): self.fitList = FitList(graphFrame, self) self.fitList.SetMinSize((270, -1)) srcTgtSizer.Add(self.fitList, 1, wx.EXPAND) + self.srcVector = VectorPicker(self, style=wx.NO_BORDER, size=60, offset=90, label='Src', labelpos=2) + srcTgtSizer.Add(self.srcVector, flag=wx.SHAPED | wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL) + self.tgtVector = VectorPicker(self, style=wx.NO_BORDER, size=60, offset=-90, label='Tgt', labelpos=3) + srcTgtSizer.Add(self.tgtVector, flag=wx.SHAPED | wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL) self.targets = [] self.targetList = TargetList(graphFrame, self) self.targetList.SetMinSize((270, -1)) @@ -83,7 +88,7 @@ class GraphControlPanel(wx.Panel): self.graphSubselSizer.Clear() self.inputsSizer.Clear() for child in self.Children: - if child not in (self.showY0Cb, self.fitList, self.targetList): + if child not in (self.showY0Cb, self.fitList, self.targetList, self.srcVector, self.tgtVector): child.Destroy() self.fields.clear() diff --git a/gui/graphFrame/vector.py b/gui/graphFrame/vector.py index ba99a3cda..b93ab68f9 100644 --- a/gui/graphFrame/vector.py +++ b/gui/graphFrame/vector.py @@ -24,7 +24,7 @@ import math import wx -class VectorPicker(wx.Control): +class VectorPicker(wx.Window): myEVT_VECTOR_CHANGED = wx.NewEventType() EVT_VECTOR_CHANGED = wx.PyEventBinder(myEVT_VECTOR_CHANGED, 1) @@ -35,7 +35,7 @@ class VectorPicker(wx.Control): self._offset = float(kwargs.pop('offset', 0)) self._size = max(0, float(kwargs.pop('size', 50))) self._fontsize = max(1, float(kwargs.pop('fontsize', 8))) - wx.Control.__init__(self, *args, **kwargs) + wx.Window.__init__(self, *args, **kwargs) self._font = wx.Font(self._fontsize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) self._angle = 0 self._length = 1 @@ -183,7 +183,7 @@ class VectorPicker(wx.Control): width, height = self.GetClientSize() if width and height: center = min(width, height) / 2 - x, y = event.GetPositionTuple() + x, y = event.GetPosition() x = x - center y = center - y angle = self._angle From 28db388fa0196234e192b2fb02748d07edca07e2 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 20 Jun 2019 15:59:59 +0300 Subject: [PATCH 11/93] Add subclass to specify direction only --- gui/graphFrame/panel.py | 4 ++-- gui/graphFrame/vector.py | 30 +++++++++++++++++++++++------- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index a28f95e3f..cc661901b 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -24,7 +24,7 @@ import wx from gui.bitmap_loader import BitmapLoader from service.fit import Fit from .lists import FitList, TargetList -from .vector import VectorPicker +from .vector import VectorPicker, DirectionPicker class GraphControlPanel(wx.Panel): @@ -59,7 +59,7 @@ class GraphControlPanel(wx.Panel): srcTgtSizer.Add(self.fitList, 1, wx.EXPAND) self.srcVector = VectorPicker(self, style=wx.NO_BORDER, size=60, offset=90, label='Src', labelpos=2) srcTgtSizer.Add(self.srcVector, flag=wx.SHAPED | wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL) - self.tgtVector = VectorPicker(self, style=wx.NO_BORDER, size=60, offset=-90, label='Tgt', labelpos=3) + self.tgtVector = DirectionPicker(self, style=wx.NO_BORDER, size=60, offset=-90, label='Tgt', labelpos=3) srcTgtSizer.Add(self.tgtVector, flag=wx.SHAPED | wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL) self.targets = [] self.targetList = TargetList(graphFrame, self) diff --git a/gui/graphFrame/vector.py b/gui/graphFrame/vector.py index b93ab68f9..047cb1821 100644 --- a/gui/graphFrame/vector.py +++ b/gui/graphFrame/vector.py @@ -28,6 +28,8 @@ class VectorPicker(wx.Window): myEVT_VECTOR_CHANGED = wx.NewEventType() EVT_VECTOR_CHANGED = wx.PyEventBinder(myEVT_VECTOR_CHANGED, 1) + _tooltip = 'Click to set angle and velocity\nRight-click to snap to 15% angle/5% speed increments\nMouse wheel to change velocity only' + _lengthLabel = True def __init__(self, *args, **kwargs): self._label = str(kwargs.pop('label', '')) @@ -35,13 +37,12 @@ class VectorPicker(wx.Window): self._offset = float(kwargs.pop('offset', 0)) self._size = max(0, float(kwargs.pop('size', 50))) self._fontsize = max(1, float(kwargs.pop('fontsize', 8))) - wx.Window.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) self._font = wx.Font(self._fontsize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) self._angle = 0 self._length = 1 self._left = False self._right = False - self._tooltip = 'Click to set angle and velocity, right-click for increments; mouse wheel for velocity only' self.SetToolTip(wx.ToolTip(self._tooltip)) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground) @@ -108,11 +109,12 @@ class VectorPicker(wx.Window): labelTextY = (radius * 2 + 4 - labelTextH) if (self._labelpos & 2) else 0 dc.DrawText(labelText, labelTextX, labelTextY) - lengthText = '%d%%' % (100 * self._length,) - lengthTextW, lengthTextH = dc.GetTextExtent(lengthText) - lengthTextX = radius + 2 + x / 2 - y / 3 - lengthTextW / 2 - lengthTextY = radius + 2 - y / 2 - x / 3 - lengthTextH / 2 - dc.DrawText(lengthText, lengthTextX, lengthTextY) + if self._lengthLabel: + lengthText = '%d%%' % (100 * self._length,) + lengthTextW, lengthTextH = dc.GetTextExtent(lengthText) + lengthTextX = radius + 2 + x / 2 - y / 3 - lengthTextW / 2 + lengthTextY = radius + 2 - y / 2 - x / 3 - lengthTextH / 2 + dc.DrawText(lengthText, lengthTextX, lengthTextY) angleText = '%d\u00B0' % (self._angle,) angleTextW, angleTextH = dc.GetTextExtent(angleText) @@ -209,3 +211,17 @@ class VectorPicker(wx.Window): changeEvent._angle = self._angle changeEvent._length = self._length self.GetEventHandler().ProcessEvent(changeEvent) + + +class DirectionPicker(VectorPicker): + + _tooltip = 'Click to set angle\nRight-click to snap to 15% angle' + _lengthLabel = False + + @property + def _length(self): + return 1 + + @_length.setter + def _length(self, length): + pass From 4cf07c4b764cde67f67edaa27a39037431287bf0 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 21 Jun 2019 09:10:55 +0300 Subject: [PATCH 12/93] Slap shit together and commit --- gui/builtinGraphs/__init__.py | 16 ++++----- gui/builtinGraphs/base.py | 34 +++++++++++-------- .../{fitDpsVsRange.py => fitDamageStats.py} | 21 +++++++----- 3 files changed, 39 insertions(+), 32 deletions(-) rename gui/builtinGraphs/{fitDpsVsRange.py => fitDamageStats.py} (60%) diff --git a/gui/builtinGraphs/__init__.py b/gui/builtinGraphs/__init__.py index d943dcf04..de2446088 100644 --- a/gui/builtinGraphs/__init__.py +++ b/gui/builtinGraphs/__init__.py @@ -1,11 +1,11 @@ # noinspection PyUnresolvedReferences from gui.builtinGraphs import ( # noqa: E402,F401 - fitDpsVsRange, - fitDmgVsTime, - fitShieldRegenVsShieldPerc, - fitShieldAmountVsTime, - fitCapRegenVsCapPerc, - fitCapAmountVsTime, - fitMobilityVsTime, - fitWarpTimeVsDistance + fitDamageStats, + # fitDmgVsTime, + # fitShieldRegenVsShieldPerc, + # fitShieldAmountVsTime, + # fitCapRegenVsCapPerc, + # fitCapAmountVsTime, + # fitMobilityVsTime, + # fitWarpTimeVsDistance ) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index 9129bf334..4ebc8616d 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -40,26 +40,30 @@ class Graph(metaclass=ABCMeta): def name(self): raise NotImplementedError - @property - @abstractmethod - def xDef(self): - raise NotImplementedError - - @property - def extraInputs(self): - return {} - @property @abstractmethod def yDefs(self): raise NotImplementedError @property - def hasTargets(self): - return False + @abstractmethod + def xDefs(self): + raise NotImplementedError @property - def hasVectors(self): + def inputs(self): + raise NotImplementedError + + @property + def srcVectorHandles(self): + return None, None + + @property + def tgtVectorHandles(self): + return None, None + + @property + def hasTargets(self): return False @property @@ -98,9 +102,9 @@ class Graph(metaclass=ABCMeta): getattr(self, yDef.eosGraph).clearCache(key=key) -XDef = namedtuple('XDef', ('inputDefault', 'inputLabel', 'inputIconID', 'axisLabel')) -YDef = namedtuple('YDef', ('switchLabel', 'axisLabel', 'eosGraph')) -ExtraInput = namedtuple('ExtraInput', ('inputDefault', 'inputLabel', 'inputIconID')) +XDef = namedtuple('XDef', ('handle', 'label', 'unit', 'mainInputHandle')) +YDef = namedtuple('YDef', ('handle', 'label', 'unit', 'eosGraph')) +Input = namedtuple('Input', ('handle', 'label', 'unit', 'iconID', 'defaultValue', 'defaultRange')) # noinspection PyUnresolvedReferences diff --git a/gui/builtinGraphs/fitDpsVsRange.py b/gui/builtinGraphs/fitDamageStats.py similarity index 60% rename from gui/builtinGraphs/fitDpsVsRange.py rename to gui/builtinGraphs/fitDamageStats.py index 3a2e7326c..121af4aaa 100644 --- a/gui/builtinGraphs/fitDpsVsRange.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -21,19 +21,19 @@ from collections import OrderedDict from eos.graph.fitDpsVsRange import FitDpsVsRangeGraph as EosGraph -from .base import Graph, XDef, YDef, ExtraInput +from .base import Graph, XDef, YDef, Input -class FitDpsVsRangeGraph(Graph): +class FitDamageStatsGraph(Graph): - name = 'DPS vs Range' + name = 'Damage Stats' def __init__(self): super().__init__() self.eosGraph = EosGraph() @property - def xDef(self): + def xDefs(self): return XDef(inputDefault='0-100', inputLabel='Distance to target (km)', inputIconID=1391, axisLabel='Distance to target, km') @property @@ -41,11 +41,14 @@ class FitDpsVsRangeGraph(Graph): return OrderedDict([('dps', YDef(switchLabel='DPS', axisLabel='DPS', eosGraph='eosGraph'))]) @property - def extraInputs(self): + def inputs(self): return OrderedDict([ - ('speed', ExtraInput(inputDefault=0, inputLabel='Target speed (m/s)', inputIconID=1389)), - ('signatureRadius', ExtraInput(inputDefault=None, inputLabel='Target signature radius (m)', inputIconID=1390)), - ('angle', ExtraInput(inputDefault=0, inputLabel='Target angle (degrees)', inputIconID=1389))]) + ('time', Input(handle='time', label='Time', unit='s', iconID=1392, defaultValue=None, defaultRange=(0, 80))), + ('atkSpeed', Input(handle='atkSpeed', label=None, unit=None, iconID=None, defaultValue=None, defaultRange=None)), + ('atkAngle', Input(handle='atkAngle', label=None, unit=None, iconID=None, defaultValue=None, defaultRange=None)), + ('tgtSpeed', Input(handle='tgtSpeed', label='Target speed', unit='%', iconID=1389, defaultValue=100, defaultRange=(0, 100))), + ('tgtAngle', Input(handle='tgtAngle', label=None, unit=None, iconID=None, defaultValue=None, defaultRange=None)), + ('tgtSigRad', Input(handle='tgtSigRad', label='Target signature radius', unit='%', iconID=1390, defaultValue=100, defaultRange=(100, 200)))]) @property def hasTargets(self): @@ -56,4 +59,4 @@ class FitDpsVsRangeGraph(Graph): return True -FitDpsVsRangeGraph.register() +FitDamageStatsGraph.register() From 3c0d87940bfb9d9a93a92aa7e93435611d75e961 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 21 Jun 2019 20:10:38 +0300 Subject: [PATCH 13/93] Change damage stats graph definition --- gui/builtinGraphs/base.py | 32 +++++++++++++++++------ gui/builtinGraphs/fitDamageStats.py | 39 ++++++++++++++++++++++++----- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index 4ebc8616d..f002b6c64 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -54,18 +54,34 @@ class Graph(metaclass=ABCMeta): def inputs(self): raise NotImplementedError - @property - def srcVectorHandles(self): - return None, None - - @property - def tgtVectorHandles(self): - return None, None - @property def hasTargets(self): return False + @property + def hasSrcVector(self): + return False + + @property + def srcVectorLengthHandle(self): + return None + + @property + def srcVectorAngleHandle(self): + return None + + @property + def hasTgtVector(self): + return False + + @property + def tgtVectorLengthHandle(self): + return None + + @property + def tgtVectorAngleHandle(self): + return None + @property def redrawOnEffectiveChange(self): return False diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 121af4aaa..12afb9fe7 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -34,20 +34,27 @@ class FitDamageStatsGraph(Graph): @property def xDefs(self): - return XDef(inputDefault='0-100', inputLabel='Distance to target (km)', inputIconID=1391, axisLabel='Distance to target, km') + return OrderedDict([ + ('distance', XDef(handle='distance', label='Distance', unit='km', mainInputHandle='distance')), + ('time', XDef(handle='time', label='Time', unit='s', mainInputHandle='time')), + ('tgtSpeedAbs', XDef(handle='tgtSpeedAbs', label='Target speed', unit='m/s', mainInputHandle='tgtSpeed')), + ('tgtSpeedRel', XDef(handle='tgtSpeedRel', label='Target speed', unit='%', mainInputHandle='tgtSpeed')), + ('tgtSigRadAbs', XDef(handle='tgtSigRadAbs', label='Target signature radius', unit='m', mainInputHandle='tgtSigRad')), + ('tgtSigRadRel', XDef(handle='tgtSigRadRel', label='Target signature radius', unit='%', mainInputHandle='tgtSigRad'))]) @property def yDefs(self): - return OrderedDict([('dps', YDef(switchLabel='DPS', axisLabel='DPS', eosGraph='eosGraph'))]) + return OrderedDict([ + ('dps', YDef(handle='dps', label='DPS', unit=None, eosGraph='eosGraph')), + ('volley', YDef(handle='volley', label='Volley', unit=None, eosGraph='eosGraph')), + ('damage', YDef(handle='damage', label='Damage inflicted', unit=None, eosGraph='eosGraph'))]) @property def inputs(self): return OrderedDict([ ('time', Input(handle='time', label='Time', unit='s', iconID=1392, defaultValue=None, defaultRange=(0, 80))), - ('atkSpeed', Input(handle='atkSpeed', label=None, unit=None, iconID=None, defaultValue=None, defaultRange=None)), - ('atkAngle', Input(handle='atkAngle', label=None, unit=None, iconID=None, defaultValue=None, defaultRange=None)), + ('distance', Input(handle='distance', label='Distance', unit='km', iconID=1391, defaultValue=50, defaultRange=(0, 100))), ('tgtSpeed', Input(handle='tgtSpeed', label='Target speed', unit='%', iconID=1389, defaultValue=100, defaultRange=(0, 100))), - ('tgtAngle', Input(handle='tgtAngle', label=None, unit=None, iconID=None, defaultValue=None, defaultRange=None)), ('tgtSigRad', Input(handle='tgtSigRad', label='Target signature radius', unit='%', iconID=1390, defaultValue=100, defaultRange=(100, 200)))]) @property @@ -55,8 +62,28 @@ class FitDamageStatsGraph(Graph): return True @property - def hasVectors(self): + def hasSrcVector(self): return True + @property + def srcVectorLengthHandle(self): + return 'atkSpeed' + + @property + def srcVectorAngleHandle(self): + return 'atkAngle' + + @property + def hasTgtVector(self): + return True + + @property + def tgtVectorLengthHandle(self): + return 'tgtSpeed' + + @property + def tgtVectorAngleHandle(self): + return 'tgtAngle' + FitDamageStatsGraph.register() From 5e7fcc32b6c66d5f912fb4da82900b7ab2ae0819 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Mon, 24 Jun 2019 10:33:59 +0300 Subject: [PATCH 14/93] Start adding code which uses new graph definition for layout --- gui/graphFrame/panel.py | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index cc661901b..c4f921f9c 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -34,8 +34,6 @@ class GraphControlPanel(wx.Panel): self.graphFrame = graphFrame self.fields = {} - self.selectedY = None - self.selectedYRbMap = {} mainSizer = wx.BoxSizer(wx.VERTICAL) @@ -84,7 +82,6 @@ class GraphControlPanel(wx.Panel): return self.showY0Cb.GetValue() def updateControlsForView(self, view): - self.selectedY = None self.graphSubselSizer.Clear() self.inputsSizer.Clear() for child in self.Children: @@ -92,24 +89,21 @@ class GraphControlPanel(wx.Panel): child.Destroy() self.fields.clear() - # Setup view options - self.selectedYRbMap.clear() - if len(view.yDefs) > 1: - i = 0 - for yAlias, yDef in view.yDefs.items(): - if i == 0: - rdo = wx.RadioButton(self, wx.ID_ANY, yDef.switchLabel, style=wx.RB_GROUP) - else: - rdo = wx.RadioButton(self, wx.ID_ANY, yDef.switchLabel) - rdo.Bind(wx.EVT_RADIOBUTTON, self.OnYTypeUpdate) - if i == (self.selectedY or 0): - rdo.SetValue(True) - self.graphSubselSizer.Add(rdo, 0, wx.ALL | wx.EXPAND, 0) - self.selectedYRbMap[yDef.switchLabel] = i - i += 1 + selectedXRb = wx.RadioBox(self, -1, 'Axis X', wx.DefaultPosition, wx.DefaultSize, [x.label for x in view.xDefs], 1, wx.RA_SPECIFY_COLS) + self.graphSubselSizer.Add(selectedXRb, 0, wx.ALL | wx.EXPAND, 0) + selectedYRb = wx.RadioBox(self, -1, 'Axis Y', wx.DefaultPosition, wx.DefaultSize, [y.label for y in view.yDefs], 1, wx.RA_SPECIFY_COLS) + self.graphSubselSizer.Add(selectedYRb, 0, wx.ALL | wx.EXPAND, 0) + + vectorHandles = set() + if view.hasSrcVector: + vectorHandles.add(view.srcVectorLengthHandle) + vectorHandles.add(view.srcVectorAngleHandle) + if view.hasTgtVector: + vectorHandles.add(view.tgtVectorLengthHandle) + vectorHandles.add(view.tgtVectorAngleHandle) # Setup inputs - for fieldHandle, fieldDef in (('x', view.xDef), *view.extraInputs.items()): + for fieldHandle, fieldDef in ((view.xDefs[0].handle, view.xDefs[0]), *view.extraInputs.items()): textBox = wx.TextCtrl(self, wx.ID_ANY, style=0) self.fields[fieldHandle] = textBox textBox.Bind(wx.EVT_TEXT, self.OnFieldChanged) @@ -144,7 +138,6 @@ class GraphControlPanel(wx.Panel): event.Skip() obj = event.GetEventObject() formatName = obj.GetLabel() - self.selectedY = self.selectedYRbMap[formatName] self.graphFrame.draw() def OnFieldChanged(self, event): From 03183827a62a0b79d53b3e17ec27f301e3093c77 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Mon, 24 Jun 2019 16:15:35 +0300 Subject: [PATCH 15/93] Show all the needed controls on the panel --- gui/builtinGraphs/base.py | 14 ++- gui/builtinGraphs/fitDamageStats.py | 34 +++--- gui/graphFrame/frame.py | 168 ++++++++++++++-------------- gui/graphFrame/panel.py | 44 ++++---- 4 files changed, 138 insertions(+), 122 deletions(-) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index f002b6c64..f3f31a70c 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -20,7 +20,7 @@ import re from abc import ABCMeta, abstractmethod -from collections import namedtuple +from collections import OrderedDict, namedtuple class Graph(metaclass=ABCMeta): @@ -45,15 +45,27 @@ class Graph(metaclass=ABCMeta): def yDefs(self): raise NotImplementedError + @property + def yDefMap(self): + return OrderedDict((y.handle, y) for y in self.yDefs) + @property @abstractmethod def xDefs(self): raise NotImplementedError + @property + def xDefMap(self): + return OrderedDict((x.handle, x) for x in self.xDefs) + @property def inputs(self): raise NotImplementedError + @property + def inputMap(self): + return OrderedDict((i.handle, i) for i in self.inputs) + @property def hasTargets(self): return False diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 12afb9fe7..f0a80717d 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -18,8 +18,6 @@ # ============================================================================= -from collections import OrderedDict - from eos.graph.fitDpsVsRange import FitDpsVsRangeGraph as EosGraph from .base import Graph, XDef, YDef, Input @@ -34,28 +32,28 @@ class FitDamageStatsGraph(Graph): @property def xDefs(self): - return OrderedDict([ - ('distance', XDef(handle='distance', label='Distance', unit='km', mainInputHandle='distance')), - ('time', XDef(handle='time', label='Time', unit='s', mainInputHandle='time')), - ('tgtSpeedAbs', XDef(handle='tgtSpeedAbs', label='Target speed', unit='m/s', mainInputHandle='tgtSpeed')), - ('tgtSpeedRel', XDef(handle='tgtSpeedRel', label='Target speed', unit='%', mainInputHandle='tgtSpeed')), - ('tgtSigRadAbs', XDef(handle='tgtSigRadAbs', label='Target signature radius', unit='m', mainInputHandle='tgtSigRad')), - ('tgtSigRadRel', XDef(handle='tgtSigRadRel', label='Target signature radius', unit='%', mainInputHandle='tgtSigRad'))]) + return [ + XDef(handle='distance', label='Distance', unit='km', mainInputHandle='distance'), + XDef(handle='time', label='Time', unit='s', mainInputHandle='time'), + XDef(handle='tgtSpeedAbs', label='Target speed', unit='m/s', mainInputHandle='tgtSpeed'), + XDef(handle='tgtSpeedRel', label='Target speed', unit='%', mainInputHandle='tgtSpeed'), + XDef(handle='tgtSigRadAbs', label='Target signature radius', unit='m', mainInputHandle='tgtSigRad'), + XDef(handle='tgtSigRadRel', label='Target signature radius', unit='%', mainInputHandle='tgtSigRad')] @property def yDefs(self): - return OrderedDict([ - ('dps', YDef(handle='dps', label='DPS', unit=None, eosGraph='eosGraph')), - ('volley', YDef(handle='volley', label='Volley', unit=None, eosGraph='eosGraph')), - ('damage', YDef(handle='damage', label='Damage inflicted', unit=None, eosGraph='eosGraph'))]) + return [ + YDef(handle='dps', label='DPS', unit=None, eosGraph='eosGraph'), + YDef(handle='volley', label='Volley', unit=None, eosGraph='eosGraph'), + YDef(handle='damage', label='Damage inflicted', unit=None, eosGraph='eosGraph')] @property def inputs(self): - return OrderedDict([ - ('time', Input(handle='time', label='Time', unit='s', iconID=1392, defaultValue=None, defaultRange=(0, 80))), - ('distance', Input(handle='distance', label='Distance', unit='km', iconID=1391, defaultValue=50, defaultRange=(0, 100))), - ('tgtSpeed', Input(handle='tgtSpeed', label='Target speed', unit='%', iconID=1389, defaultValue=100, defaultRange=(0, 100))), - ('tgtSigRad', Input(handle='tgtSigRad', label='Target signature radius', unit='%', iconID=1390, defaultValue=100, defaultRange=(100, 200)))]) + return [ + Input(handle='time', label='Time', unit='s', iconID=1392, defaultValue=None, defaultRange=(0, 80)), + Input(handle='distance', label='Distance', unit='km', iconID=1391, defaultValue=50, defaultRange=(0, 100)), + Input(handle='tgtSpeed', label='Target speed', unit='%', iconID=1389, defaultValue=100, defaultRange=(0, 100)), + Input(handle='tgtSigRad', label='Target signature radius', unit='%', iconID=1390, defaultValue=100, defaultRange=(100, 200))] @property def hasTargets(self): diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index 1c6f419eb..0ae0b39f3 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -179,90 +179,90 @@ class GraphFrame(wx.Frame): pyfalog.warning('GraphFrame handled event, however GraphFrame no longer exists. Ignoring event') return - values = self.ctrlPanel.getValues() - view = self.getView() - self.subplot.clear() - self.subplot.grid(True) - legend = [] - - min_y = 0 if self.ctrlPanel.showY0 else None - max_y = 0 if self.ctrlPanel.showY0 else None - - xRange = values['x'] - extraInputs = {ih: values[ih] for ih in view.extraInputs} - try: - chosenY = [i for i in view.yDefs.keys()][self.ctrlPanel.selectedY or 0] - except IndexError: - chosenY = [i for i in view.yDefs.keys()][0] - - self.subplot.set(xlabel=view.xDef.axisLabel, ylabel=view.yDefs[chosenY].axisLabel) - - for fit in self.ctrlPanel.fitList.fits: - try: - xs, ys = view.getPlotPoints(fit, extraInputs, xRange, 100, chosenY) - - # Figure out min and max Y - min_y_this = min(ys, default=None) - if min_y is None: - min_y = min_y_this - elif min_y_this is not None: - min_y = min(min_y, min_y_this) - max_y_this = max(ys, default=None) - if max_y is None: - max_y = max_y_this - elif max_y_this is not None: - max_y = max(max_y, max_y_this) - - self.subplot.plot(xs, ys) - legend.append('{} ({})'.format(fit.name, fit.ship.item.getShortName())) - except Exception as ex: - pyfalog.warning('Invalid values in "{0}"', fit.name) - self.canvas.draw() - return - - # Special case for when we do not show Y = 0 and have no fits - if min_y is None: - min_y = 0 - if max_y is None: - max_y = 0 - # Extend range a little for some visual space - y_range = max_y - min_y - min_y -= y_range * 0.05 - max_y += y_range * 0.05 - if min_y == max_y: - min_y -= min_y * 0.05 - max_y += min_y * 0.05 - if min_y == max_y: - min_y -= 5 - max_y += 5 - self.subplot.set_ylim(bottom=min_y, top=max_y) - - legend2 = [] - legend_colors = { - 0: 'blue', - 1: 'orange', - 2: 'green', - 3: 'red', - 4: 'purple', - 5: 'brown', - 6: 'pink', - 7: 'grey', - } - - for i, i_name in enumerate(legend): - try: - selected_color = legend_colors[i] - except: - selected_color = None - legend2.append(Patch(color=selected_color, label=i_name), ) - - if len(legend2) > 0: - leg = self.subplot.legend(handles=legend2) - for t in leg.get_texts(): - t.set_fontsize('small') - - for l in leg.get_lines(): - l.set_linewidth(1) + # values = self.ctrlPanel.getValues() + # view = self.getView() + # self.subplot.clear() + # self.subplot.grid(True) + # legend = [] + # + # min_y = 0 if self.ctrlPanel.showY0 else None + # max_y = 0 if self.ctrlPanel.showY0 else None + # + # xRange = values['x'] + # extraInputs = {ih: values[ih] for ih in view.extraInputs} + # try: + # chosenY = [i for i in view.yDefs.keys()][self.ctrlPanel.selectedY or 0] + # except IndexError: + # chosenY = [i for i in view.yDefs.keys()][0] + # + # self.subplot.set(xlabel=view.xDef.axisLabel, ylabel=view.yDefs[chosenY].axisLabel) + # + # for fit in self.ctrlPanel.fitList.fits: + # try: + # xs, ys = view.getPlotPoints(fit, extraInputs, xRange, 100, chosenY) + # + # # Figure out min and max Y + # min_y_this = min(ys, default=None) + # if min_y is None: + # min_y = min_y_this + # elif min_y_this is not None: + # min_y = min(min_y, min_y_this) + # max_y_this = max(ys, default=None) + # if max_y is None: + # max_y = max_y_this + # elif max_y_this is not None: + # max_y = max(max_y, max_y_this) + # + # self.subplot.plot(xs, ys) + # legend.append('{} ({})'.format(fit.name, fit.ship.item.getShortName())) + # except Exception as ex: + # pyfalog.warning('Invalid values in "{0}"', fit.name) + # self.canvas.draw() + # return + # + # # Special case for when we do not show Y = 0 and have no fits + # if min_y is None: + # min_y = 0 + # if max_y is None: + # max_y = 0 + # # Extend range a little for some visual space + # y_range = max_y - min_y + # min_y -= y_range * 0.05 + # max_y += y_range * 0.05 + # if min_y == max_y: + # min_y -= min_y * 0.05 + # max_y += min_y * 0.05 + # if min_y == max_y: + # min_y -= 5 + # max_y += 5 + # self.subplot.set_ylim(bottom=min_y, top=max_y) + # + # legend2 = [] + # legend_colors = { + # 0: 'blue', + # 1: 'orange', + # 2: 'green', + # 3: 'red', + # 4: 'purple', + # 5: 'brown', + # 6: 'pink', + # 7: 'grey', + # } + # + # for i, i_name in enumerate(legend): + # try: + # selected_color = legend_colors[i] + # except: + # selected_color = None + # legend2.append(Patch(color=selected_color, label=i_name), ) + # + # if len(legend2) > 0: + # leg = self.subplot.legend(handles=legend2) + # for t in leg.get_texts(): + # t.set_fontsize('small') + # + # for l in leg.get_lines(): + # l.set_linewidth(1) self.canvas.draw() self.Refresh() diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index c4f921f9c..4df2ce34d 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -89,43 +89,49 @@ class GraphControlPanel(wx.Panel): child.Destroy() self.fields.clear() - selectedXRb = wx.RadioBox(self, -1, 'Axis X', wx.DefaultPosition, wx.DefaultSize, [x.label for x in view.xDefs], 1, wx.RA_SPECIFY_COLS) + + def formatAxisLabel(axisDef): + if axisDef.unit is None: + return axisDef.label + return '{}, {}'.format(axisDef.label, axisDef.unit) + + selectedXRb = wx.RadioBox(self, -1, 'Axis X', wx.DefaultPosition, wx.DefaultSize, [formatAxisLabel(x) for x in view.xDefs], 1, wx.RA_SPECIFY_COLS) self.graphSubselSizer.Add(selectedXRb, 0, wx.ALL | wx.EXPAND, 0) - selectedYRb = wx.RadioBox(self, -1, 'Axis Y', wx.DefaultPosition, wx.DefaultSize, [y.label for y in view.yDefs], 1, wx.RA_SPECIFY_COLS) + selectedYRb = wx.RadioBox(self, -1, 'Axis Y', wx.DefaultPosition, wx.DefaultSize, [formatAxisLabel(y) for y in view.yDefs], 1, wx.RA_SPECIFY_COLS) self.graphSubselSizer.Add(selectedYRb, 0, wx.ALL | wx.EXPAND, 0) - vectorHandles = set() - if view.hasSrcVector: - vectorHandles.add(view.srcVectorLengthHandle) - vectorHandles.add(view.srcVectorAngleHandle) - if view.hasTgtVector: - vectorHandles.add(view.tgtVectorLengthHandle) - vectorHandles.add(view.tgtVectorAngleHandle) - # Setup inputs - for fieldHandle, fieldDef in ((view.xDefs[0].handle, view.xDefs[0]), *view.extraInputs.items()): + shownHandles = set() + if view.hasSrcVector: + shownHandles.add(view.srcVectorLengthHandle) + shownHandles.add(view.srcVectorAngleHandle) + if view.hasTgtVector: + shownHandles.add(view.tgtVectorLengthHandle) + shownHandles.add(view.tgtVectorAngleHandle) + for inputHandle in (view.xDefs[0].mainInputHandle, *(i.handle for i in view.inputs)): + if inputHandle in shownHandles: + continue + shownHandles.add(inputHandle) + inputDef = view.inputMap[inputHandle] textBox = wx.TextCtrl(self, wx.ID_ANY, style=0) - self.fields[fieldHandle] = textBox textBox.Bind(wx.EVT_TEXT, self.OnFieldChanged) self.inputsSizer.Add(textBox, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 3) - if fieldDef.inputDefault is not None: - inputDefault = fieldDef.inputDefault + if inputDef.defaultValue is not None: + inputDefault = inputDef.defaultValue if not isinstance(inputDefault, str): inputDefault = ('%f' % inputDefault).rstrip('0') if inputDefault[-1:] == '.': inputDefault += '0' - textBox.ChangeValue(inputDefault) - imgLabelSizer = wx.BoxSizer(wx.HORIZONTAL) - if fieldDef.inputIconID: - icon = BitmapLoader.getBitmap(fieldDef.inputIconID, 'icons') + if inputDef.iconID is not None: + icon = BitmapLoader.getBitmap(inputDef.iconID, 'icons') if icon is not None: static = wx.StaticBitmap(self) static.SetBitmap(icon) imgLabelSizer.Add(static, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 1) - imgLabelSizer.Add(wx.StaticText(self, wx.ID_ANY, fieldDef.inputLabel), 0, + imgLabelSizer.Add(wx.StaticText(self, wx.ID_ANY, inputDef.label), 0, wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 3) self.inputsSizer.Add(imgLabelSizer, 0, wx.ALIGN_CENTER_VERTICAL) self.Layout() From 5ffd644ad982538b76e7254a30c9e068feb38ed0 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Mon, 24 Jun 2019 20:24:19 +0300 Subject: [PATCH 16/93] Rework subgraph options --- gui/graphFrame/panel.py | 48 ++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 4df2ce34d..1cb185930 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -33,16 +33,33 @@ class GraphControlPanel(wx.Panel): super().__init__(parent) self.graphFrame = graphFrame - self.fields = {} - mainSizer = wx.BoxSizer(wx.VERTICAL) - paramSizer = wx.BoxSizer(wx.HORIZONTAL) - viewOptSizer = wx.BoxSizer(wx.VERTICAL) + commonOptsSizer = wx.BoxSizer(wx.HORIZONTAL) + ySubSelectionSizer = wx.BoxSizer(wx.HORIZONTAL) + yText = wx.StaticText(self, wx.ID_ANY, 'Axis Y:') + ySubSelectionSizer.Add(yText, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) + self.ySubSelection = wx.Choice(self, wx.ID_ANY) + ySubSelectionSizer.Add(self.ySubSelection, 1, wx.EXPAND | wx.ALL, 0) + commonOptsSizer.Add(ySubSelectionSizer, 1, wx.EXPAND | wx.RIGHT, 3) + + xSubSelectionSizer = wx.BoxSizer(wx.HORIZONTAL) + xText = wx.StaticText(self, wx.ID_ANY, 'Axis X:') + xSubSelectionSizer.Add(xText, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) + self.xSubSelection = wx.Choice(self, wx.ID_ANY) + xSubSelectionSizer.Add(self.xSubSelection, 1, wx.EXPAND | wx.ALL, 0) + commonOptsSizer.Add(xSubSelectionSizer, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 3) + + commonOptsMiscSizer = wx.BoxSizer(wx.VERTICAL) self.showY0Cb = wx.CheckBox(self, wx.ID_ANY, 'Always show Y = 0', wx.DefaultPosition, wx.DefaultSize, 0) self.showY0Cb.SetValue(True) self.showY0Cb.Bind(wx.EVT_CHECKBOX, self.OnShowY0Change) - viewOptSizer.Add(self.showY0Cb, 0, wx.LEFT | wx.TOP | wx.RIGHT | wx.EXPAND, 5) + commonOptsMiscSizer.Add(self.showY0Cb, 1, wx.EXPAND | wx.ALL, 0) + commonOptsSizer.Add(commonOptsMiscSizer, 0, wx.EXPAND | wx.LEFT, 3) + mainSizer.Add(commonOptsSizer, 0, wx.EXPAND | wx.ALL, 5) + + paramSizer = wx.BoxSizer(wx.HORIZONTAL) + viewOptSizer = wx.BoxSizer(wx.VERTICAL) self.graphSubselSizer = wx.BoxSizer(wx.VERTICAL) viewOptSizer.Add(self.graphSubselSizer, 0, wx.ALL | wx.EXPAND, 5) paramSizer.Add(viewOptSizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.BOTTOM, 5) @@ -66,6 +83,10 @@ class GraphControlPanel(wx.Panel): srcTgtSizer.Add(self.targetList, 1, wx.EXPAND) mainSizer.Add(srcTgtSizer, 1, wx.EXPAND | wx.ALL, 0) + self.indestructible = { + self.showY0Cb, yText, self.ySubSelection, self.xSubSelection, xText, + self.fitList, self.targetList, self.srcVector, self.tgtVector} + self.SetSizer(mainSizer) self.drawTimer = wx.Timer(self) @@ -73,8 +94,6 @@ class GraphControlPanel(wx.Panel): def getValues(self): values = {} - for fieldHandle, field in self.fields.items(): - values[fieldHandle] = field.GetValue() return values @property @@ -85,9 +104,10 @@ class GraphControlPanel(wx.Panel): self.graphSubselSizer.Clear() self.inputsSizer.Clear() for child in self.Children: - if child not in (self.showY0Cb, self.fitList, self.targetList, self.srcVector, self.tgtVector): + if child not in self.indestructible: child.Destroy() - self.fields.clear() + self.ySubSelection.Clear() + self.xSubSelection.Clear() def formatAxisLabel(axisDef): @@ -95,10 +115,12 @@ class GraphControlPanel(wx.Panel): return axisDef.label return '{}, {}'.format(axisDef.label, axisDef.unit) - selectedXRb = wx.RadioBox(self, -1, 'Axis X', wx.DefaultPosition, wx.DefaultSize, [formatAxisLabel(x) for x in view.xDefs], 1, wx.RA_SPECIFY_COLS) - self.graphSubselSizer.Add(selectedXRb, 0, wx.ALL | wx.EXPAND, 0) - selectedYRb = wx.RadioBox(self, -1, 'Axis Y', wx.DefaultPosition, wx.DefaultSize, [formatAxisLabel(y) for y in view.yDefs], 1, wx.RA_SPECIFY_COLS) - self.graphSubselSizer.Add(selectedYRb, 0, wx.ALL | wx.EXPAND, 0) + for yDef in view.yDefs: + self.ySubSelection.Append(formatAxisLabel(yDef), yDef.handle) + self.ySubSelection.SetSelection(0) + for xDef in view.xDefs: + self.xSubSelection.Append(formatAxisLabel(xDef), xDef.handle) + self.xSubSelection.SetSelection(0) # Setup inputs shownHandles = set() From 022f0c06ee307648c87eb5a2c7da8f3b198f462e Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 25 Jun 2019 08:23:21 +0300 Subject: [PATCH 17/93] Do not show sig % except for the cases when it's used as main value range --- gui/builtinGraphs/base.py | 2 +- gui/builtinGraphs/fitDamageStats.py | 8 ++++---- gui/graphFrame/panel.py | 9 +++++---- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index f3f31a70c..77cc1414c 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -132,7 +132,7 @@ class Graph(metaclass=ABCMeta): XDef = namedtuple('XDef', ('handle', 'label', 'unit', 'mainInputHandle')) YDef = namedtuple('YDef', ('handle', 'label', 'unit', 'eosGraph')) -Input = namedtuple('Input', ('handle', 'label', 'unit', 'iconID', 'defaultValue', 'defaultRange')) +Input = namedtuple('Input', ('handle', 'label', 'unit', 'iconID', 'defaultValue', 'defaultRange', 'mainOnly')) # noinspection PyUnresolvedReferences diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index f0a80717d..5f57fc32c 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -50,10 +50,10 @@ class FitDamageStatsGraph(Graph): @property def inputs(self): return [ - Input(handle='time', label='Time', unit='s', iconID=1392, defaultValue=None, defaultRange=(0, 80)), - Input(handle='distance', label='Distance', unit='km', iconID=1391, defaultValue=50, defaultRange=(0, 100)), - Input(handle='tgtSpeed', label='Target speed', unit='%', iconID=1389, defaultValue=100, defaultRange=(0, 100)), - Input(handle='tgtSigRad', label='Target signature radius', unit='%', iconID=1390, defaultValue=100, defaultRange=(100, 200))] + Input(handle='time', label='Time', unit='s', iconID=1392, defaultValue=None, defaultRange=(0, 80), mainOnly=False), + Input(handle='distance', label='Distance', unit='km', iconID=1391, defaultValue=50, defaultRange=(0, 100), mainOnly=False), + Input(handle='tgtSpeed', label='Target speed', unit='%', iconID=1389, defaultValue=100, defaultRange=(0, 100), mainOnly=False), + Input(handle='tgtSigRad', label='Target signature radius', unit='%', iconID=1390, defaultValue=100, defaultRange=(100, 200), mainOnly=True)] @property def hasTargets(self): diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 1cb185930..7ea4de3b5 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -130,11 +130,12 @@ class GraphControlPanel(wx.Panel): if view.hasTgtVector: shownHandles.add(view.tgtVectorLengthHandle) shownHandles.add(view.tgtVectorAngleHandle) - for inputHandle in (view.xDefs[0].mainInputHandle, *(i.handle for i in view.inputs)): - if inputHandle in shownHandles: + for inputDef in (view.inputMap[view.xDefs[0].mainInputHandle], *(i for i in view.inputs)): + if inputDef.handle != view.xDefs[0].mainInputHandle and inputDef.mainOnly: continue - shownHandles.add(inputHandle) - inputDef = view.inputMap[inputHandle] + if inputDef.handle in shownHandles: + continue + shownHandles.add(inputDef.handle) textBox = wx.TextCtrl(self, wx.ID_ANY, style=0) textBox.Bind(wx.EVT_TEXT, self.OnFieldChanged) self.inputsSizer.Add(textBox, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 3) From 4b960af9ab614dd39398a50b220a4b7a75350b5b Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 25 Jun 2019 11:40:10 +0300 Subject: [PATCH 18/93] Rework code to use handle and unit to access various definitions --- gui/builtinGraphs/base.py | 45 ++++++++-------------- gui/builtinGraphs/fitDamageStats.py | 60 +++++++++++------------------ gui/graphFrame/panel.py | 24 ++++++------ 3 files changed, 50 insertions(+), 79 deletions(-) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index 77cc1414c..77e165afd 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -47,7 +47,7 @@ class Graph(metaclass=ABCMeta): @property def yDefMap(self): - return OrderedDict((y.handle, y) for y in self.yDefs) + return OrderedDict(((y.handle, y.unit), y) for y in self.yDefs) @property @abstractmethod @@ -56,7 +56,7 @@ class Graph(metaclass=ABCMeta): @property def xDefMap(self): - return OrderedDict((x.handle, x) for x in self.xDefs) + return OrderedDict(((x.handle, x.unit), x) for x in self.xDefs) @property def inputs(self): @@ -64,36 +64,20 @@ class Graph(metaclass=ABCMeta): @property def inputMap(self): - return OrderedDict((i.handle, i) for i in self.inputs) + return OrderedDict(((i.handle, i.unit), i) for i in self.inputs) + + @property + def srcVectorDef(self): + return None + + @property + def tgtVectorDef(self): + return None @property def hasTargets(self): return False - @property - def hasSrcVector(self): - return False - - @property - def srcVectorLengthHandle(self): - return None - - @property - def srcVectorAngleHandle(self): - return None - - @property - def hasTgtVector(self): - return False - - @property - def tgtVectorLengthHandle(self): - return None - - @property - def tgtVectorAngleHandle(self): - return None - @property def redrawOnEffectiveChange(self): return False @@ -130,9 +114,10 @@ class Graph(metaclass=ABCMeta): getattr(self, yDef.eosGraph).clearCache(key=key) -XDef = namedtuple('XDef', ('handle', 'label', 'unit', 'mainInputHandle')) -YDef = namedtuple('YDef', ('handle', 'label', 'unit', 'eosGraph')) -Input = namedtuple('Input', ('handle', 'label', 'unit', 'iconID', 'defaultValue', 'defaultRange', 'mainOnly')) +XDef = namedtuple('XDef', ('handle', 'unit', 'label', 'mainInput')) +YDef = namedtuple('YDef', ('handle', 'unit', 'label', 'eosGraph')) +Input = namedtuple('Input', ('handle', 'unit', 'label', 'iconID', 'defaultValue', 'defaultRange', 'mainOnly')) +VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle', 'angleUnit')) # noinspection PyUnresolvedReferences diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 5f57fc32c..0dc7a7f99 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -19,7 +19,7 @@ from eos.graph.fitDpsVsRange import FitDpsVsRangeGraph as EosGraph -from .base import Graph, XDef, YDef, Input +from .base import Graph, XDef, YDef, Input, VectorDef class FitDamageStatsGraph(Graph): @@ -33,55 +33,39 @@ class FitDamageStatsGraph(Graph): @property def xDefs(self): return [ - XDef(handle='distance', label='Distance', unit='km', mainInputHandle='distance'), - XDef(handle='time', label='Time', unit='s', mainInputHandle='time'), - XDef(handle='tgtSpeedAbs', label='Target speed', unit='m/s', mainInputHandle='tgtSpeed'), - XDef(handle='tgtSpeedRel', label='Target speed', unit='%', mainInputHandle='tgtSpeed'), - XDef(handle='tgtSigRadAbs', label='Target signature radius', unit='m', mainInputHandle='tgtSigRad'), - XDef(handle='tgtSigRadRel', label='Target signature radius', unit='%', mainInputHandle='tgtSigRad')] + XDef(handle='distance', unit='km', label='Distance', mainInput=('distance', 'km')), + XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')), + XDef(handle='tgtSpeed', unit='m/s', label='Target speed', mainInput=('tgtSpeed', '%')), + XDef(handle='tgtSpeed', unit='%', label='Target speed', mainInput=('tgtSpeed', '%')), + XDef(handle='tgtSigRad', unit='m', label='Target signature radius', mainInput=('tgtSigRad', '%')), + XDef(handle='tgtSigRad', unit='%', label='Target signature radius', mainInput=('tgtSigRad', '%'))] @property def yDefs(self): return [ - YDef(handle='dps', label='DPS', unit=None, eosGraph='eosGraph'), - YDef(handle='volley', label='Volley', unit=None, eosGraph='eosGraph'), - YDef(handle='damage', label='Damage inflicted', unit=None, eosGraph='eosGraph')] + YDef(handle='dps', unit=None, label='DPS', eosGraph='eosGraph'), + YDef(handle='volley', unit=None, label='Volley', eosGraph='eosGraph'), + YDef(handle='damage', unit=None, label='Damage inflicted', eosGraph='eosGraph')] @property def inputs(self): return [ - Input(handle='time', label='Time', unit='s', iconID=1392, defaultValue=None, defaultRange=(0, 80), mainOnly=False), - Input(handle='distance', label='Distance', unit='km', iconID=1391, defaultValue=50, defaultRange=(0, 100), mainOnly=False), - Input(handle='tgtSpeed', label='Target speed', unit='%', iconID=1389, defaultValue=100, defaultRange=(0, 100), mainOnly=False), - Input(handle='tgtSigRad', label='Target signature radius', unit='%', iconID=1390, defaultValue=100, defaultRange=(100, 200), mainOnly=True)] + Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=None, defaultRange=(0, 80), mainOnly=False), + Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=50, defaultRange=(0, 100), mainOnly=False), + Input(handle='tgtSpeed', unit='%', label='Target speed', iconID=1389, defaultValue=100, defaultRange=(0, 100), mainOnly=False), + Input(handle='tgtSigRad', unit='%', label='Target signature radius', iconID=1390, defaultValue=100, defaultRange=(100, 200), mainOnly=True)] + + @property + def srcVectorDef(self): + return VectorDef(lengthHandle='atkSpeed', lengthUnit='%', angleHandle='atkAngle', angleUnit='degrees') + + @property + def tgtVectorDef(self): + return VectorDef(lengthHandle='tgtSpeed', lengthUnit='%', angleHandle='tgtAngle', angleUnit='degrees') @property def hasTargets(self): return True - @property - def hasSrcVector(self): - return True - - @property - def srcVectorLengthHandle(self): - return 'atkSpeed' - - @property - def srcVectorAngleHandle(self): - return 'atkAngle' - - @property - def hasTgtVector(self): - return True - - @property - def tgtVectorLengthHandle(self): - return 'tgtSpeed' - - @property - def tgtVectorAngleHandle(self): - return 'tgtAngle' - FitDamageStatsGraph.register() diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 7ea4de3b5..34bc7f194 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -123,19 +123,21 @@ class GraphControlPanel(wx.Panel): self.xSubSelection.SetSelection(0) # Setup inputs - shownHandles = set() - if view.hasSrcVector: - shownHandles.add(view.srcVectorLengthHandle) - shownHandles.add(view.srcVectorAngleHandle) - if view.hasTgtVector: - shownHandles.add(view.tgtVectorLengthHandle) - shownHandles.add(view.tgtVectorAngleHandle) - for inputDef in (view.inputMap[view.xDefs[0].mainInputHandle], *(i for i in view.inputs)): - if inputDef.handle != view.xDefs[0].mainInputHandle and inputDef.mainOnly: + shownFields = set() + srcVectorDef = view.srcVectorDef + if srcVectorDef is not None: + shownFields.add((srcVectorDef.lengthHandle, srcVectorDef.lengthUnit)) + shownFields.add((srcVectorDef.angleHandle, srcVectorDef.angleUnit)) + tgtVectorDef = view.tgtVectorDef + if tgtVectorDef is not None: + shownFields.add((tgtVectorDef.lengthHandle, tgtVectorDef.lengthUnit)) + shownFields.add((tgtVectorDef.angleHandle, tgtVectorDef.angleUnit)) + for inputDef in (view.inputMap[view.xDefs[0].mainInput], *(i for i in view.inputs)): + if (inputDef.handle, inputDef.unit) != view.xDefs[0].mainInput and inputDef.mainOnly: continue - if inputDef.handle in shownHandles: + if (inputDef.handle, inputDef.unit) in shownFields: continue - shownHandles.add(inputDef.handle) + shownFields.add((inputDef.handle, inputDef.unit)) textBox = wx.TextCtrl(self, wx.ID_ANY, style=0) textBox.Bind(wx.EVT_TEXT, self.OnFieldChanged) self.inputsSizer.Add(textBox, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 3) From 52724d790b1859a34952efeab7f0292d9daa0d32 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 25 Jun 2019 15:25:48 +0300 Subject: [PATCH 19/93] Change control panel layout --- gui/graphFrame/panel.py | 81 +++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 34bc7f194..ee042bf2b 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -34,48 +34,44 @@ class GraphControlPanel(wx.Panel): self.graphFrame = graphFrame mainSizer = wx.BoxSizer(wx.VERTICAL) + optsSizer = wx.BoxSizer(wx.HORIZONTAL) - commonOptsSizer = wx.BoxSizer(wx.HORIZONTAL) + commonOptsSizer = wx.BoxSizer(wx.VERTICAL) ySubSelectionSizer = wx.BoxSizer(wx.HORIZONTAL) yText = wx.StaticText(self, wx.ID_ANY, 'Axis Y:') ySubSelectionSizer.Add(yText, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) self.ySubSelection = wx.Choice(self, wx.ID_ANY) ySubSelectionSizer.Add(self.ySubSelection, 1, wx.EXPAND | wx.ALL, 0) - commonOptsSizer.Add(ySubSelectionSizer, 1, wx.EXPAND | wx.RIGHT, 3) + commonOptsSizer.Add(ySubSelectionSizer, 0, wx.EXPAND | wx.ALL, 0) xSubSelectionSizer = wx.BoxSizer(wx.HORIZONTAL) xText = wx.StaticText(self, wx.ID_ANY, 'Axis X:') xSubSelectionSizer.Add(xText, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) self.xSubSelection = wx.Choice(self, wx.ID_ANY) xSubSelectionSizer.Add(self.xSubSelection, 1, wx.EXPAND | wx.ALL, 0) - commonOptsSizer.Add(xSubSelectionSizer, 1, wx.EXPAND | wx.LEFT | wx.RIGHT, 3) + commonOptsSizer.Add(xSubSelectionSizer, 0, wx.EXPAND | wx.TOP, 5) - commonOptsMiscSizer = wx.BoxSizer(wx.VERTICAL) self.showY0Cb = wx.CheckBox(self, wx.ID_ANY, 'Always show Y = 0', wx.DefaultPosition, wx.DefaultSize, 0) self.showY0Cb.SetValue(True) self.showY0Cb.Bind(wx.EVT_CHECKBOX, self.OnShowY0Change) - commonOptsMiscSizer.Add(self.showY0Cb, 1, wx.EXPAND | wx.ALL, 0) - commonOptsSizer.Add(commonOptsMiscSizer, 0, wx.EXPAND | wx.LEFT, 3) - mainSizer.Add(commonOptsSizer, 0, wx.EXPAND | wx.ALL, 5) + commonOptsSizer.Add(self.showY0Cb, 0, wx.EXPAND | wx.TOP, 5) + optsSizer.Add(commonOptsSizer, 0, wx.EXPAND | wx.RIGHT, 10) - paramSizer = wx.BoxSizer(wx.HORIZONTAL) - viewOptSizer = wx.BoxSizer(wx.VERTICAL) - self.graphSubselSizer = wx.BoxSizer(wx.VERTICAL) - viewOptSizer.Add(self.graphSubselSizer, 0, wx.ALL | wx.EXPAND, 5) - paramSizer.Add(viewOptSizer, 0, wx.EXPAND | wx.LEFT | wx.TOP | wx.BOTTOM, 5) - self.inputsSizer = wx.FlexGridSizer(0, 4, 0, 0) - self.inputsSizer.AddGrowableCol(1) - paramSizer.Add(self.inputsSizer, 1, wx.EXPAND | wx.RIGHT | wx.TOP | wx.BOTTOM, 5) - mainSizer.Add(paramSizer, 0, wx.EXPAND | wx.ALL, 0) + graphOptsSizer = wx.BoxSizer(wx.HORIZONTAL) + self.inputsSizer = wx.BoxSizer(wx.VERTICAL) + graphOptsSizer.Add(self.inputsSizer, 0, wx.EXPAND | wx.ALL, 0) + self.srcVector = VectorPicker(self, style=wx.NO_BORDER, size=75, offset=90) + graphOptsSizer.Add(self.srcVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 10) + self.tgtVector = DirectionPicker(self, style=wx.NO_BORDER, size=75, offset=-90) + graphOptsSizer.Add(self.tgtVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 10) + + optsSizer.Add(graphOptsSizer, 0, wx.EXPAND | wx.ALL, 0) + mainSizer.Add(optsSizer, 0, wx.EXPAND | wx.ALL, 10) srcTgtSizer = wx.BoxSizer(wx.HORIZONTAL) self.fitList = FitList(graphFrame, self) self.fitList.SetMinSize((270, -1)) srcTgtSizer.Add(self.fitList, 1, wx.EXPAND) - self.srcVector = VectorPicker(self, style=wx.NO_BORDER, size=60, offset=90, label='Src', labelpos=2) - srcTgtSizer.Add(self.srcVector, flag=wx.SHAPED | wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL) - self.tgtVector = DirectionPicker(self, style=wx.NO_BORDER, size=60, offset=-90, label='Tgt', labelpos=3) - srcTgtSizer.Add(self.tgtVector, flag=wx.SHAPED | wx.ALIGN_LEFT | wx.ALIGN_CENTER_VERTICAL) self.targets = [] self.targetList = TargetList(graphFrame, self) self.targetList.SetMinSize((270, -1)) @@ -101,7 +97,6 @@ class GraphControlPanel(wx.Panel): return self.showY0Cb.GetValue() def updateControlsForView(self, view): - self.graphSubselSizer.Clear() self.inputsSizer.Clear() for child in self.Children: if child not in self.indestructible: @@ -110,55 +105,55 @@ class GraphControlPanel(wx.Panel): self.xSubSelection.Clear() - def formatAxisLabel(axisDef): + def formatLabel(axisDef): if axisDef.unit is None: return axisDef.label return '{}, {}'.format(axisDef.label, axisDef.unit) for yDef in view.yDefs: - self.ySubSelection.Append(formatAxisLabel(yDef), yDef.handle) + self.ySubSelection.Append(formatLabel(yDef), yDef.handle) self.ySubSelection.SetSelection(0) for xDef in view.xDefs: - self.xSubSelection.Append(formatAxisLabel(xDef), xDef.handle) + self.xSubSelection.Append(formatLabel(xDef), xDef.handle) self.xSubSelection.SetSelection(0) # Setup inputs - shownFields = set() + shownHandles = set() srcVectorDef = view.srcVectorDef if srcVectorDef is not None: - shownFields.add((srcVectorDef.lengthHandle, srcVectorDef.lengthUnit)) - shownFields.add((srcVectorDef.angleHandle, srcVectorDef.angleUnit)) + shownHandles.add(srcVectorDef.lengthHandle) + shownHandles.add(srcVectorDef.angleHandle) tgtVectorDef = view.tgtVectorDef if tgtVectorDef is not None: - shownFields.add((tgtVectorDef.lengthHandle, tgtVectorDef.lengthUnit)) - shownFields.add((tgtVectorDef.angleHandle, tgtVectorDef.angleUnit)) + shownHandles.add(tgtVectorDef.lengthHandle) + shownHandles.add(tgtVectorDef.angleHandle) for inputDef in (view.inputMap[view.xDefs[0].mainInput], *(i for i in view.inputs)): if (inputDef.handle, inputDef.unit) != view.xDefs[0].mainInput and inputDef.mainOnly: continue - if (inputDef.handle, inputDef.unit) in shownFields: + if inputDef.handle in shownHandles: continue - shownFields.add((inputDef.handle, inputDef.unit)) - textBox = wx.TextCtrl(self, wx.ID_ANY, style=0) - textBox.Bind(wx.EVT_TEXT, self.OnFieldChanged) - self.inputsSizer.Add(textBox, 1, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 3) + shownHandles.add(inputDef.handle) + # Handle UI input fields + fieldSizer = wx.BoxSizer(wx.HORIZONTAL) + fieldTextBox = wx.TextCtrl(self, wx.ID_ANY, style=0) + fieldTextBox.Bind(wx.EVT_TEXT, self.OnFieldChanged) if inputDef.defaultValue is not None: inputDefault = inputDef.defaultValue if not isinstance(inputDefault, str): inputDefault = ('%f' % inputDefault).rstrip('0') if inputDefault[-1:] == '.': inputDefault += '0' - textBox.ChangeValue(inputDefault) - imgLabelSizer = wx.BoxSizer(wx.HORIZONTAL) + fieldTextBox.ChangeValue(inputDefault) + fieldSizer.Add(fieldTextBox, 0, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) if inputDef.iconID is not None: icon = BitmapLoader.getBitmap(inputDef.iconID, 'icons') if icon is not None: - static = wx.StaticBitmap(self) - static.SetBitmap(icon) - imgLabelSizer.Add(static, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 1) - - imgLabelSizer.Add(wx.StaticText(self, wx.ID_ANY, inputDef.label), 0, - wx.LEFT | wx.RIGHT | wx.ALIGN_CENTER_VERTICAL, 3) - self.inputsSizer.Add(imgLabelSizer, 0, wx.ALIGN_CENTER_VERTICAL) + fieldIcon = wx.StaticBitmap(self) + fieldIcon.SetBitmap(icon) + fieldSizer.Add(fieldIcon, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 3) + fieldLabel = wx.StaticText(self, wx.ID_ANY, formatLabel(inputDef)) + fieldSizer.Add(fieldLabel, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) + self.inputsSizer.Add(fieldSizer, 0, wx.EXPAND | wx.BOTTOM, 5) self.Layout() def OnShowY0Change(self, event): From 509a45dceeb10c30b81c958c9a5f589003a9ce60 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 25 Jun 2019 16:02:45 +0300 Subject: [PATCH 20/93] Show labels for vectors separately --- gui/builtinGraphs/base.py | 2 +- gui/builtinGraphs/fitDamageStats.py | 4 ++-- gui/graphFrame/panel.py | 21 ++++++++++++++++----- gui/graphFrame/vector.py | 1 - 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index 77e165afd..3df622d4c 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -117,7 +117,7 @@ class Graph(metaclass=ABCMeta): XDef = namedtuple('XDef', ('handle', 'unit', 'label', 'mainInput')) YDef = namedtuple('YDef', ('handle', 'unit', 'label', 'eosGraph')) Input = namedtuple('Input', ('handle', 'unit', 'label', 'iconID', 'defaultValue', 'defaultRange', 'mainOnly')) -VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle', 'angleUnit')) +VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle', 'angleUnit', 'label')) # noinspection PyUnresolvedReferences diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 0dc7a7f99..14bd2e663 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -57,11 +57,11 @@ class FitDamageStatsGraph(Graph): @property def srcVectorDef(self): - return VectorDef(lengthHandle='atkSpeed', lengthUnit='%', angleHandle='atkAngle', angleUnit='degrees') + return VectorDef(lengthHandle='atkSpeed', lengthUnit='%', angleHandle='atkAngle', angleUnit='degrees', label='Attacker') @property def tgtVectorDef(self): - return VectorDef(lengthHandle='tgtSpeed', lengthUnit='%', angleHandle='tgtAngle', angleUnit='degrees') + return VectorDef(lengthHandle='tgtSpeed', lengthUnit='%', angleHandle='tgtAngle', angleUnit='degrees', label='Target') @property def hasTargets(self): diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index ee042bf2b..0201c5b3c 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -60,10 +60,20 @@ class GraphControlPanel(wx.Panel): graphOptsSizer = wx.BoxSizer(wx.HORIZONTAL) self.inputsSizer = wx.BoxSizer(wx.VERTICAL) graphOptsSizer.Add(self.inputsSizer, 0, wx.EXPAND | wx.ALL, 0) + + srcVectorSizer = wx.BoxSizer(wx.VERTICAL) + self.srcVectorLabel = wx.StaticText(self, wx.ID_ANY, 'Attacker') + srcVectorSizer.Add(self.srcVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL| wx.BOTTOM, 5) self.srcVector = VectorPicker(self, style=wx.NO_BORDER, size=75, offset=90) - graphOptsSizer.Add(self.srcVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 10) + srcVectorSizer.Add(self.srcVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) + graphOptsSizer.Add(srcVectorSizer, 0, wx.EXPAND | wx.LEFT, 30) + + tgtVectorSizer = wx.BoxSizer(wx.VERTICAL) + self.tgtVectorLabel = wx.StaticText(self, wx.ID_ANY, 'Target') + tgtVectorSizer.Add(self.tgtVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM, 5) self.tgtVector = DirectionPicker(self, style=wx.NO_BORDER, size=75, offset=-90) - graphOptsSizer.Add(self.tgtVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 10) + tgtVectorSizer.Add(self.tgtVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) + graphOptsSizer.Add(tgtVectorSizer, 0, wx.EXPAND | wx.LEFT, 10) optsSizer.Add(graphOptsSizer, 0, wx.EXPAND | wx.ALL, 0) mainSizer.Add(optsSizer, 0, wx.EXPAND | wx.ALL, 10) @@ -71,17 +81,18 @@ class GraphControlPanel(wx.Panel): srcTgtSizer = wx.BoxSizer(wx.HORIZONTAL) self.fitList = FitList(graphFrame, self) self.fitList.SetMinSize((270, -1)) - srcTgtSizer.Add(self.fitList, 1, wx.EXPAND) + srcTgtSizer.Add(self.fitList, 1, wx.EXPAND | wx.ALL, 0) self.targets = [] self.targetList = TargetList(graphFrame, self) self.targetList.SetMinSize((270, -1)) self.targetList.update(self.targets) - srcTgtSizer.Add(self.targetList, 1, wx.EXPAND) + srcTgtSizer.Add(self.targetList, 1, wx.EXPAND | wx.ALL, 0) mainSizer.Add(srcTgtSizer, 1, wx.EXPAND | wx.ALL, 0) self.indestructible = { self.showY0Cb, yText, self.ySubSelection, self.xSubSelection, xText, - self.fitList, self.targetList, self.srcVector, self.tgtVector} + self.srcVectorLabel, self.srcVector, self.tgtVectorLabel, self.tgtVector, + self.fitList, self.targetList} self.SetSizer(mainSizer) diff --git a/gui/graphFrame/vector.py b/gui/graphFrame/vector.py index 047cb1821..f4aaade0c 100644 --- a/gui/graphFrame/vector.py +++ b/gui/graphFrame/vector.py @@ -86,7 +86,6 @@ class VectorPicker(wx.Window): width, height = self.GetClientSize() if not width or not height: return - dc.SetBackground(wx.Brush(self.GetBackgroundColour(), wx.BRUSHSTYLE_SOLID)) dc.Clear() dc.SetTextForeground(wx.Colour(0)) From 0420f399ad20868a0b1047d0da5ea42ceea3de07 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 25 Jun 2019 16:37:33 +0300 Subject: [PATCH 21/93] Show-hide vectors and target list as needed --- gui/graphFrame/frame.py | 4 ++-- gui/graphFrame/panel.py | 48 ++++++++++++++++++++++++++++++----------- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index 0ae0b39f3..6260f839f 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -113,7 +113,7 @@ class GraphFrame(wx.Frame): for view in Graph.views: self.graphSelection.Append(view.name, view()) self.graphSelection.SetSelection(0) - self.ctrlPanel.updateControlsForView(self.getView()) + self.ctrlPanel.updateControls(self.getView()) # Event bindings - local events self.Bind(wx.EVT_CLOSE, self.closeEvent) @@ -153,7 +153,7 @@ class GraphFrame(wx.Frame): def OnGraphSwitched(self, event): self.clearCache() - self.ctrlPanel.updateControlsForView(self.getView()) + self.ctrlPanel.updateControls(self.getView()) self.draw() event.Skip() diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 0201c5b3c..0d0705dc4 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -61,19 +61,19 @@ class GraphControlPanel(wx.Panel): self.inputsSizer = wx.BoxSizer(wx.VERTICAL) graphOptsSizer.Add(self.inputsSizer, 0, wx.EXPAND | wx.ALL, 0) - srcVectorSizer = wx.BoxSizer(wx.VERTICAL) - self.srcVectorLabel = wx.StaticText(self, wx.ID_ANY, 'Attacker') - srcVectorSizer.Add(self.srcVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL| wx.BOTTOM, 5) + self.srcVectorSizer = wx.BoxSizer(wx.VERTICAL) + self.srcVectorLabel = wx.StaticText(self, wx.ID_ANY, '') + self.srcVectorSizer.Add(self.srcVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL| wx.BOTTOM, 5) self.srcVector = VectorPicker(self, style=wx.NO_BORDER, size=75, offset=90) - srcVectorSizer.Add(self.srcVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) - graphOptsSizer.Add(srcVectorSizer, 0, wx.EXPAND | wx.LEFT, 30) + self.srcVectorSizer.Add(self.srcVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) + graphOptsSizer.Add(self.srcVectorSizer, 0, wx.EXPAND | wx.LEFT, 30) - tgtVectorSizer = wx.BoxSizer(wx.VERTICAL) - self.tgtVectorLabel = wx.StaticText(self, wx.ID_ANY, 'Target') - tgtVectorSizer.Add(self.tgtVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM, 5) + self.tgtVectorSizer = wx.BoxSizer(wx.VERTICAL) + self.tgtVectorLabel = wx.StaticText(self, wx.ID_ANY, '') + self.tgtVectorSizer.Add(self.tgtVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM, 5) self.tgtVector = DirectionPicker(self, style=wx.NO_BORDER, size=75, offset=-90) - tgtVectorSizer.Add(self.tgtVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) - graphOptsSizer.Add(tgtVectorSizer, 0, wx.EXPAND | wx.LEFT, 10) + self.tgtVectorSizer.Add(self.tgtVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) + graphOptsSizer.Add(self.tgtVectorSizer, 0, wx.EXPAND | wx.LEFT, 10) optsSizer.Add(graphOptsSizer, 0, wx.EXPAND | wx.ALL, 0) mainSizer.Add(optsSizer, 0, wx.EXPAND | wx.ALL, 10) @@ -107,7 +107,7 @@ class GraphControlPanel(wx.Panel): def showY0(self): return self.showY0Cb.GetValue() - def updateControlsForView(self, view): + def updateControls(self, view): self.inputsSizer.Clear() for child in self.Children: if child not in self.indestructible: @@ -115,7 +115,6 @@ class GraphControlPanel(wx.Panel): self.ySubSelection.Clear() self.xSubSelection.Clear() - def formatLabel(axisDef): if axisDef.unit is None: return axisDef.label @@ -128,7 +127,7 @@ class GraphControlPanel(wx.Panel): self.xSubSelection.Append(formatLabel(xDef), xDef.handle) self.xSubSelection.SetSelection(0) - # Setup inputs + # Inputs shownHandles = set() srcVectorDef = view.srcVectorDef if srcVectorDef is not None: @@ -165,8 +164,31 @@ class GraphControlPanel(wx.Panel): fieldLabel = wx.StaticText(self, wx.ID_ANY, formatLabel(inputDef)) fieldSizer.Add(fieldLabel, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) self.inputsSizer.Add(fieldSizer, 0, wx.EXPAND | wx.BOTTOM, 5) + + # Vectors + if view.srcVectorDef is not None: + self.srcVectorLabel.SetLabel(view.srcVectorDef.label) + self.srcVector.Show(True) + self.srcVectorLabel.Show(True) + else: + self.srcVector.Show(False) + self.srcVectorLabel.Show(False) + if view.tgtVectorDef is not None: + self.tgtVectorLabel.SetLabel(view.tgtVectorDef.label) + self.tgtVector.Show(True) + self.tgtVectorLabel.Show(True) + else: + self.tgtVector.Show(False) + self.tgtVectorLabel.Show(False) + + # Target list + self.targetList.Show(view.hasTargets) + self.Layout() + def updateInputs(self, view): + pass + def OnShowY0Change(self, event): event.Skip() self.graphFrame.draw() From 2a645b1b04482dfb9c97a12d9dbe45845a4e861d Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 25 Jun 2019 16:56:26 +0300 Subject: [PATCH 22/93] Move input layout code into its own function --- gui/graphFrame/frame.py | 4 +-- gui/graphFrame/panel.py | 72 ++++++++++++++++++++--------------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index 6260f839f..e23242d80 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -113,7 +113,7 @@ class GraphFrame(wx.Frame): for view in Graph.views: self.graphSelection.Append(view.name, view()) self.graphSelection.SetSelection(0) - self.ctrlPanel.updateControls(self.getView()) + self.ctrlPanel.updateControls() # Event bindings - local events self.Bind(wx.EVT_CLOSE, self.closeEvent) @@ -153,7 +153,7 @@ class GraphFrame(wx.Frame): def OnGraphSwitched(self, event): self.clearCache() - self.ctrlPanel.updateControls(self.getView()) + self.ctrlPanel.updateControls() self.draw() event.Skip() diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 0d0705dc4..eac852ca8 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -86,8 +86,8 @@ class GraphControlPanel(wx.Panel): self.targetList = TargetList(graphFrame, self) self.targetList.SetMinSize((270, -1)) self.targetList.update(self.targets) - srcTgtSizer.Add(self.targetList, 1, wx.EXPAND | wx.ALL, 0) - mainSizer.Add(srcTgtSizer, 1, wx.EXPAND | wx.ALL, 0) + srcTgtSizer.Add(self.targetList, 1, wx.EXPAND | wx.LEFT, 10) + mainSizer.Add(srcTgtSizer, 1, wx.EXPAND | wx.LEFT | wx.BOTTOM | wx.RIGHT, 10) self.indestructible = { self.showY0Cb, yText, self.ySubSelection, self.xSubSelection, xText, @@ -107,7 +107,8 @@ class GraphControlPanel(wx.Panel): def showY0(self): return self.showY0Cb.GetValue() - def updateControls(self, view): + def updateControls(self): + view = self.graphFrame.getView() self.inputsSizer.Clear() for child in self.Children: if child not in self.indestructible: @@ -115,19 +116,38 @@ class GraphControlPanel(wx.Panel): self.ySubSelection.Clear() self.xSubSelection.Clear() - def formatLabel(axisDef): - if axisDef.unit is None: - return axisDef.label - return '{}, {}'.format(axisDef.label, axisDef.unit) - for yDef in view.yDefs: - self.ySubSelection.Append(formatLabel(yDef), yDef.handle) + self.ySubSelection.Append(self._formatLabel(yDef), yDef.handle) self.ySubSelection.SetSelection(0) for xDef in view.xDefs: - self.xSubSelection.Append(formatLabel(xDef), xDef.handle) + self.xSubSelection.Append(self._formatLabel(xDef), xDef.handle) self.xSubSelection.SetSelection(0) - # Inputs + self.updateInputs() + + # Vectors + if view.srcVectorDef is not None: + self.srcVectorLabel.SetLabel(view.srcVectorDef.label) + self.srcVector.Show(True) + self.srcVectorLabel.Show(True) + else: + self.srcVector.Show(False) + self.srcVectorLabel.Show(False) + if view.tgtVectorDef is not None: + self.tgtVectorLabel.SetLabel(view.tgtVectorDef.label) + self.tgtVector.Show(True) + self.tgtVectorLabel.Show(True) + else: + self.tgtVector.Show(False) + self.tgtVectorLabel.Show(False) + + # Target list + self.targetList.Show(view.hasTargets) + + self.Layout() + + def updateInputs(self): + view = self.graphFrame.getView() shownHandles = set() srcVectorDef = view.srcVectorDef if srcVectorDef is not None: @@ -161,34 +181,10 @@ class GraphControlPanel(wx.Panel): fieldIcon = wx.StaticBitmap(self) fieldIcon.SetBitmap(icon) fieldSizer.Add(fieldIcon, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 3) - fieldLabel = wx.StaticText(self, wx.ID_ANY, formatLabel(inputDef)) + fieldLabel = wx.StaticText(self, wx.ID_ANY, self._formatLabel(inputDef)) fieldSizer.Add(fieldLabel, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) self.inputsSizer.Add(fieldSizer, 0, wx.EXPAND | wx.BOTTOM, 5) - # Vectors - if view.srcVectorDef is not None: - self.srcVectorLabel.SetLabel(view.srcVectorDef.label) - self.srcVector.Show(True) - self.srcVectorLabel.Show(True) - else: - self.srcVector.Show(False) - self.srcVectorLabel.Show(False) - if view.tgtVectorDef is not None: - self.tgtVectorLabel.SetLabel(view.tgtVectorDef.label) - self.tgtVector.Show(True) - self.tgtVectorLabel.Show(True) - else: - self.tgtVector.Show(False) - self.tgtVectorLabel.Show(False) - - # Target list - self.targetList.Show(view.hasTargets) - - self.Layout() - - def updateInputs(self, view): - pass - def OnShowY0Change(self, event): event.Skip() self.graphFrame.draw() @@ -212,3 +208,7 @@ class GraphControlPanel(wx.Panel): def unbindExternalEvents(self): self.fitList.unbindExternalEvents() + def _formatLabel(self, axisDef): + if axisDef.unit is None: + return axisDef.label + return '{}, {}'.format(axisDef.label, axisDef.unit) From 3aa69a6eaf9758b8dc2b669f38a5f514e00dea98 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 25 Jun 2019 17:42:15 +0300 Subject: [PATCH 23/93] Update inputs when X selection is updated --- gui/builtinGraphs/fitDamageStats.py | 2 +- gui/graphFrame/frame.py | 2 +- gui/graphFrame/panel.py | 68 ++++++++++++++++++----------- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 14bd2e663..1baef53cd 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -53,7 +53,7 @@ class FitDamageStatsGraph(Graph): Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=None, defaultRange=(0, 80), mainOnly=False), Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=50, defaultRange=(0, 100), mainOnly=False), Input(handle='tgtSpeed', unit='%', label='Target speed', iconID=1389, defaultValue=100, defaultRange=(0, 100), mainOnly=False), - Input(handle='tgtSigRad', unit='%', label='Target signature radius', iconID=1390, defaultValue=100, defaultRange=(100, 200), mainOnly=True)] + Input(handle='tgtSigRad', unit='%', label='Target signature', iconID=1390, defaultValue=100, defaultRange=(100, 200), mainOnly=True)] @property def srcVectorDef(self): diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index e23242d80..3b2d4a704 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -158,7 +158,7 @@ class GraphFrame(wx.Frame): event.Skip() def closeWindow(self): - from gui.builtinStatsViews.resistancesViewFull import EFFECTIVE_HP_TOGGLED # Grr gons + from gui.builtinStatsViews.resistancesViewFull import EFFECTIVE_HP_TOGGLED self.mainFrame.Unbind(GE.FIT_CHANGED, handler=self.OnFitChanged) self.mainFrame.Unbind(EFFECTIVE_HP_TOGGLED, handler=self.OnEhpToggled) self.ctrlPanel.unbindExternalEvents() diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index eac852ca8..339378ce6 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -32,6 +32,7 @@ class GraphControlPanel(wx.Panel): def __init__(self, graphFrame, parent): super().__init__(parent) self.graphFrame = graphFrame + self.inputs = {} mainSizer = wx.BoxSizer(wx.VERTICAL) optsSizer = wx.BoxSizer(wx.HORIZONTAL) @@ -41,6 +42,7 @@ class GraphControlPanel(wx.Panel): yText = wx.StaticText(self, wx.ID_ANY, 'Axis Y:') ySubSelectionSizer.Add(yText, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) self.ySubSelection = wx.Choice(self, wx.ID_ANY) + self.ySubSelection.Bind(wx.EVT_CHOICE, self.OnYTypeUpdate) ySubSelectionSizer.Add(self.ySubSelection, 1, wx.EXPAND | wx.ALL, 0) commonOptsSizer.Add(ySubSelectionSizer, 0, wx.EXPAND | wx.ALL, 0) @@ -48,6 +50,7 @@ class GraphControlPanel(wx.Panel): xText = wx.StaticText(self, wx.ID_ANY, 'Axis X:') xSubSelectionSizer.Add(xText, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) self.xSubSelection = wx.Choice(self, wx.ID_ANY) + self.xSubSelection.Bind(wx.EVT_CHOICE, self.OnXTypeUpdate) xSubSelectionSizer.Add(self.xSubSelection, 1, wx.EXPAND | wx.ALL, 0) commonOptsSizer.Add(xSubSelectionSizer, 0, wx.EXPAND | wx.TOP, 5) @@ -89,42 +92,22 @@ class GraphControlPanel(wx.Panel): srcTgtSizer.Add(self.targetList, 1, wx.EXPAND | wx.LEFT, 10) mainSizer.Add(srcTgtSizer, 1, wx.EXPAND | wx.LEFT | wx.BOTTOM | wx.RIGHT, 10) - self.indestructible = { - self.showY0Cb, yText, self.ySubSelection, self.xSubSelection, xText, - self.srcVectorLabel, self.srcVector, self.tgtVectorLabel, self.tgtVector, - self.fitList, self.targetList} - self.SetSizer(mainSizer) self.drawTimer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self.OnDrawTimer, self.drawTimer) - def getValues(self): - values = {} - return values - - @property - def showY0(self): - return self.showY0Cb.GetValue() - def updateControls(self): view = self.graphFrame.getView() - self.inputsSizer.Clear() - for child in self.Children: - if child not in self.indestructible: - child.Destroy() self.ySubSelection.Clear() self.xSubSelection.Clear() - for yDef in view.yDefs: - self.ySubSelection.Append(self._formatLabel(yDef), yDef.handle) + self.ySubSelection.Append(self._formatLabel(yDef), (yDef.handle, yDef.unit)) self.ySubSelection.SetSelection(0) for xDef in view.xDefs: - self.xSubSelection.Append(self._formatLabel(xDef), xDef.handle) + self.xSubSelection.Append(self._formatLabel(xDef), (xDef.handle, xDef.unit)) self.xSubSelection.SetSelection(0) - self.updateInputs() - # Vectors if view.srcVectorDef is not None: self.srcVectorLabel.SetLabel(view.srcVectorDef.label) @@ -144,9 +127,19 @@ class GraphControlPanel(wx.Panel): # Target list self.targetList.Show(view.hasTargets) + self.updateInputs() self.Layout() def updateInputs(self): + # Clean up old inputs + for children in self.inputs.values(): + for child in children: + if child is not None: + child.Destroy() + self.inputsSizer.Clear() + self.inputs.clear() + + # Set up inputs from scratch view = self.graphFrame.getView() shownHandles = set() srcVectorDef = view.srcVectorDef @@ -157,8 +150,9 @@ class GraphControlPanel(wx.Panel): if tgtVectorDef is not None: shownHandles.add(tgtVectorDef.lengthHandle) shownHandles.add(tgtVectorDef.angleHandle) - for inputDef in (view.inputMap[view.xDefs[0].mainInput], *(i for i in view.inputs)): - if (inputDef.handle, inputDef.unit) != view.xDefs[0].mainInput and inputDef.mainOnly: + selectedX = view.xDefMap[self.xType] + for inputDef in (view.inputMap[selectedX.mainInput], *(i for i in view.inputs)): + if (inputDef.handle, inputDef.unit) != selectedX.mainInput and inputDef.mainOnly: continue if inputDef.handle in shownHandles: continue @@ -175,6 +169,7 @@ class GraphControlPanel(wx.Panel): inputDefault += '0' fieldTextBox.ChangeValue(inputDefault) fieldSizer.Add(fieldTextBox, 0, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) + fieldIcon = None if inputDef.iconID is not None: icon = BitmapLoader.getBitmap(inputDef.iconID, 'icons') if icon is not None: @@ -183,6 +178,7 @@ class GraphControlPanel(wx.Panel): fieldSizer.Add(fieldIcon, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 3) fieldLabel = wx.StaticText(self, wx.ID_ANY, self._formatLabel(inputDef)) fieldSizer.Add(fieldLabel, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) + self.inputs[(inputDef.handle, inputDef.unit)] = (fieldTextBox, fieldIcon, fieldLabel) self.inputsSizer.Add(fieldSizer, 0, wx.EXPAND | wx.BOTTOM, 5) def OnShowY0Change(self, event): @@ -191,8 +187,12 @@ class GraphControlPanel(wx.Panel): def OnYTypeUpdate(self, event): event.Skip() - obj = event.GetEventObject() - formatName = obj.GetLabel() + self.graphFrame.draw() + + def OnXTypeUpdate(self, event): + event.Skip() + self.updateInputs() + self.Layout() self.graphFrame.draw() def OnFieldChanged(self, event): @@ -205,6 +205,22 @@ class GraphControlPanel(wx.Panel): self.graphFrame.clearCache() self.graphFrame.draw() + def getValues(self): + values = {} + return values + + @property + def showY0(self): + return self.showY0Cb.GetValue() + + @property + def yType(self): + return self.ySubSelection.GetClientData(self.ySubSelection.GetSelection()) + + @property + def xType(self): + return self.xSubSelection.GetClientData(self.xSubSelection.GetSelection()) + def unbindExternalEvents(self): self.fitList.unbindExternalEvents() From 09ca85ca81511c95bc52c069358787db6732c831 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 25 Jun 2019 19:16:03 +0300 Subject: [PATCH 24/93] Merge vector classes into one --- gui/graphFrame/panel.py | 6 +++--- gui/graphFrame/vector.py | 42 ++++++++++++++++++++++++++-------------- 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 339378ce6..764cd0d2a 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -24,7 +24,7 @@ import wx from gui.bitmap_loader import BitmapLoader from service.fit import Fit from .lists import FitList, TargetList -from .vector import VectorPicker, DirectionPicker +from .vector import VectorPicker class GraphControlPanel(wx.Panel): @@ -67,14 +67,14 @@ class GraphControlPanel(wx.Panel): self.srcVectorSizer = wx.BoxSizer(wx.VERTICAL) self.srcVectorLabel = wx.StaticText(self, wx.ID_ANY, '') self.srcVectorSizer.Add(self.srcVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL| wx.BOTTOM, 5) - self.srcVector = VectorPicker(self, style=wx.NO_BORDER, size=75, offset=90) + self.srcVector = VectorPicker(self, style=wx.NO_BORDER, size=75, offset=90, directionOnly=True) self.srcVectorSizer.Add(self.srcVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) graphOptsSizer.Add(self.srcVectorSizer, 0, wx.EXPAND | wx.LEFT, 30) self.tgtVectorSizer = wx.BoxSizer(wx.VERTICAL) self.tgtVectorLabel = wx.StaticText(self, wx.ID_ANY, '') self.tgtVectorSizer.Add(self.tgtVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM, 5) - self.tgtVector = DirectionPicker(self, style=wx.NO_BORDER, size=75, offset=-90) + self.tgtVector = VectorPicker(self, style=wx.NO_BORDER, size=75, offset=-90) self.tgtVectorSizer.Add(self.tgtVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) graphOptsSizer.Add(self.tgtVectorSizer, 0, wx.EXPAND | wx.LEFT, 10) diff --git a/gui/graphFrame/vector.py b/gui/graphFrame/vector.py index f4aaade0c..a9224367b 100644 --- a/gui/graphFrame/vector.py +++ b/gui/graphFrame/vector.py @@ -28,8 +28,6 @@ class VectorPicker(wx.Window): myEVT_VECTOR_CHANGED = wx.NewEventType() EVT_VECTOR_CHANGED = wx.PyEventBinder(myEVT_VECTOR_CHANGED, 1) - _tooltip = 'Click to set angle and velocity\nRight-click to snap to 15% angle/5% speed increments\nMouse wheel to change velocity only' - _lengthLabel = True def __init__(self, *args, **kwargs): self._label = str(kwargs.pop('label', '')) @@ -37,10 +35,11 @@ class VectorPicker(wx.Window): self._offset = float(kwargs.pop('offset', 0)) self._size = max(0, float(kwargs.pop('size', 50))) self._fontsize = max(1, float(kwargs.pop('fontsize', 8))) + self._directionOnly = kwargs.pop('directionOnly', False) super().__init__(*args, **kwargs) self._font = wx.Font(self._fontsize, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL, False) self._angle = 0 - self._length = 1 + self.__length = 1 self._left = False self._right = False self.SetToolTip(wx.ToolTip(self._tooltip)) @@ -50,6 +49,24 @@ class VectorPicker(wx.Window): self.Bind(wx.EVT_RIGHT_DOWN, self.OnRightDown) self.Bind(wx.EVT_MOUSEWHEEL, self.OnWheel) + @property + def _tooltip(self): + if self._directionOnly: + return 'Click to set angle\nRight-click to snap to 15% angle' + else: + return 'Click to set angle and velocity\nRight-click to snap to 15% angle/5% speed increments\nMouse wheel to change velocity only' + + @property + def _length(self): + if self._directionOnly: + return 1 + else: + return self.__length + + @_length.setter + def _length(self, newLength): + self.__length = newLength + def DoGetBestSize(self): return wx.Size(self._size, self._size) @@ -108,7 +125,7 @@ class VectorPicker(wx.Window): labelTextY = (radius * 2 + 4 - labelTextH) if (self._labelpos & 2) else 0 dc.DrawText(labelText, labelTextX, labelTextY) - if self._lengthLabel: + if not self._directionOnly: lengthText = '%d%%' % (100 * self._length,) lengthTextW, lengthTextH = dc.GetTextExtent(lengthText) lengthTextX = radius + 2 + x / 2 - y / 3 - lengthTextW / 2 @@ -211,16 +228,13 @@ class VectorPicker(wx.Window): changeEvent._length = self._length self.GetEventHandler().ProcessEvent(changeEvent) - -class DirectionPicker(VectorPicker): - - _tooltip = 'Click to set angle\nRight-click to snap to 15% angle' - _lengthLabel = False + def SetDirectionOnly(self, val): + if self._directionOnly is val: + return + self._directionOnly = val + self.GetToolTip().SetTip(self._tooltip) @property - def _length(self): - return 1 + def IsDirectionOnly(self): + return self._directionOnly - @_length.setter - def _length(self, length): - pass From b6a58b4ba6c48e0c187bad55377750fabd8be200 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 25 Jun 2019 19:53:13 +0300 Subject: [PATCH 25/93] Change vectors when needed --- gui/graphFrame/panel.py | 55 ++++++++++++++++++++++++----------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 764cd0d2a..d2e167db5 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -62,12 +62,12 @@ class GraphControlPanel(wx.Panel): graphOptsSizer = wx.BoxSizer(wx.HORIZONTAL) self.inputsSizer = wx.BoxSizer(wx.VERTICAL) - graphOptsSizer.Add(self.inputsSizer, 0, wx.EXPAND | wx.ALL, 0) + graphOptsSizer.Add(self.inputsSizer, 1, wx.EXPAND | wx.ALL, 0) self.srcVectorSizer = wx.BoxSizer(wx.VERTICAL) self.srcVectorLabel = wx.StaticText(self, wx.ID_ANY, '') self.srcVectorSizer.Add(self.srcVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL| wx.BOTTOM, 5) - self.srcVector = VectorPicker(self, style=wx.NO_BORDER, size=75, offset=90, directionOnly=True) + self.srcVector = VectorPicker(self, style=wx.NO_BORDER, size=75, offset=90) self.srcVectorSizer.Add(self.srcVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) graphOptsSizer.Add(self.srcVectorSizer, 0, wx.EXPAND | wx.LEFT, 30) @@ -78,7 +78,7 @@ class GraphControlPanel(wx.Panel): self.tgtVectorSizer.Add(self.tgtVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) graphOptsSizer.Add(self.tgtVectorSizer, 0, wx.EXPAND | wx.LEFT, 10) - optsSizer.Add(graphOptsSizer, 0, wx.EXPAND | wx.ALL, 0) + optsSizer.Add(graphOptsSizer, 1, wx.EXPAND | wx.ALL, 0) mainSizer.Add(optsSizer, 0, wx.EXPAND | wx.ALL, 10) srcTgtSizer = wx.BoxSizer(wx.HORIZONTAL) @@ -139,25 +139,8 @@ class GraphControlPanel(wx.Panel): self.inputsSizer.Clear() self.inputs.clear() - # Set up inputs from scratch - view = self.graphFrame.getView() - shownHandles = set() - srcVectorDef = view.srcVectorDef - if srcVectorDef is not None: - shownHandles.add(srcVectorDef.lengthHandle) - shownHandles.add(srcVectorDef.angleHandle) - tgtVectorDef = view.tgtVectorDef - if tgtVectorDef is not None: - shownHandles.add(tgtVectorDef.lengthHandle) - shownHandles.add(tgtVectorDef.angleHandle) - selectedX = view.xDefMap[self.xType] - for inputDef in (view.inputMap[selectedX.mainInput], *(i for i in view.inputs)): - if (inputDef.handle, inputDef.unit) != selectedX.mainInput and inputDef.mainOnly: - continue - if inputDef.handle in shownHandles: - continue - shownHandles.add(inputDef.handle) - # Handle UI input fields + def addInputField(inputDef, handledHandles): + handledHandles.add(inputDef.handle) fieldSizer = wx.BoxSizer(wx.HORIZONTAL) fieldTextBox = wx.TextCtrl(self, wx.ID_ANY, style=0) fieldTextBox.Bind(wx.EVT_TEXT, self.OnFieldChanged) @@ -181,6 +164,34 @@ class GraphControlPanel(wx.Panel): self.inputs[(inputDef.handle, inputDef.unit)] = (fieldTextBox, fieldIcon, fieldLabel) self.inputsSizer.Add(fieldSizer, 0, wx.EXPAND | wx.BOTTOM, 5) + + # Set up inputs + view = self.graphFrame.getView() + handledHandles = set() + srcVectorDef = view.srcVectorDef + if srcVectorDef is not None: + handledHandles.add(srcVectorDef.lengthHandle) + handledHandles.add(srcVectorDef.angleHandle) + tgtVectorDef = view.tgtVectorDef + if tgtVectorDef is not None: + handledHandles.add(tgtVectorDef.lengthHandle) + handledHandles.add(tgtVectorDef.angleHandle) + selectedX = view.xDefMap[self.xType] + # Always add main input + addInputField(view.inputMap[selectedX.mainInput], handledHandles) + for inputDef in view.inputs: + if inputDef.mainOnly: + continue + if inputDef.handle in handledHandles: + continue + addInputField(inputDef, handledHandles) + + # Modify vectors + mainInputHandle = selectedX.mainInput[0] + self.srcVector.SetDirectionOnly(view.srcVectorDef.lengthHandle == mainInputHandle) + self.tgtVector.SetDirectionOnly(view.tgtVectorDef.lengthHandle == mainInputHandle) + + def OnShowY0Change(self, event): event.Skip() self.graphFrame.draw() From 9f261f5b80b3600db1cfe1b4f06d43ac62ad9490 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 26 Jun 2019 07:53:22 +0300 Subject: [PATCH 26/93] Change window size when needed --- gui/graphFrame/frame.py | 13 +++++++++++-- gui/graphFrame/panel.py | 9 +++++---- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index 3b2d4a704..bd232fc1c 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -124,8 +124,17 @@ class GraphFrame(wx.Frame): self.mainFrame.Bind(EFFECTIVE_HP_TOGGLED, self.OnEhpToggled) self.draw() - self.Fit() - self.SetMinSize(self.GetSize()) + self.UpdateWindowSize() + + def UpdateWindowSize(self): + curW, curH = self.GetSize() + bestW, bestH = self.GetBestSize() + newW = max(curW, bestW) + newH = max(curH, bestH) + if newW > curW or newH > curH: + newSize = wx.Size(newW, newH) + self.SetSize(newSize) + self.SetMinSize(newSize) def closeEvent(self, event): self.closeWindow() diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index d2e167db5..a283efe42 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -69,7 +69,7 @@ class GraphControlPanel(wx.Panel): self.srcVectorSizer.Add(self.srcVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL| wx.BOTTOM, 5) self.srcVector = VectorPicker(self, style=wx.NO_BORDER, size=75, offset=90) self.srcVectorSizer.Add(self.srcVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) - graphOptsSizer.Add(self.srcVectorSizer, 0, wx.EXPAND | wx.LEFT, 30) + graphOptsSizer.Add(self.srcVectorSizer, 0, wx.EXPAND | wx.LEFT, 15) self.tgtVectorSizer = wx.BoxSizer(wx.VERTICAL) self.tgtVectorLabel = wx.StaticText(self, wx.ID_ANY, '') @@ -128,7 +128,8 @@ class GraphControlPanel(wx.Panel): self.targetList.Show(view.hasTargets) self.updateInputs() - self.Layout() + self.graphFrame.Layout() + self.graphFrame.UpdateWindowSize() def updateInputs(self): # Clean up old inputs @@ -191,7 +192,6 @@ class GraphControlPanel(wx.Panel): self.srcVector.SetDirectionOnly(view.srcVectorDef.lengthHandle == mainInputHandle) self.tgtVector.SetDirectionOnly(view.tgtVectorDef.lengthHandle == mainInputHandle) - def OnShowY0Change(self, event): event.Skip() self.graphFrame.draw() @@ -203,7 +203,8 @@ class GraphControlPanel(wx.Panel): def OnXTypeUpdate(self, event): event.Skip() self.updateInputs() - self.Layout() + self.graphFrame.Layout() + self.graphFrame.UpdateWindowSize() self.graphFrame.draw() def OnFieldChanged(self, event): From 15a8c5750a166e4130f4d58b704d7c144c665486 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 26 Jun 2019 07:59:18 +0300 Subject: [PATCH 27/93] Call layout when frame is created as well --- gui/graphFrame/frame.py | 5 +++-- gui/graphFrame/panel.py | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index bd232fc1c..e15b3a64b 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -113,7 +113,7 @@ class GraphFrame(wx.Frame): for view in Graph.views: self.graphSelection.Append(view.name, view()) self.graphSelection.SetSelection(0) - self.ctrlPanel.updateControls() + self.ctrlPanel.updateControls(layout=False) # Event bindings - local events self.Bind(wx.EVT_CLOSE, self.closeEvent) @@ -123,8 +123,9 @@ class GraphFrame(wx.Frame): from gui.builtinStatsViews.resistancesViewFull import EFFECTIVE_HP_TOGGLED # Grr crclar gons self.mainFrame.Bind(EFFECTIVE_HP_TOGGLED, self.OnEhpToggled) - self.draw() + self.Layout() self.UpdateWindowSize() + self.draw() def UpdateWindowSize(self): curW, curH = self.GetSize() diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index a283efe42..1ffdf5b7c 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -97,7 +97,7 @@ class GraphControlPanel(wx.Panel): self.drawTimer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self.OnDrawTimer, self.drawTimer) - def updateControls(self): + def updateControls(self, layout=True): view = self.graphFrame.getView() self.ySubSelection.Clear() self.xSubSelection.Clear() @@ -127,9 +127,12 @@ class GraphControlPanel(wx.Panel): # Target list self.targetList.Show(view.hasTargets) + # Inputs self.updateInputs() - self.graphFrame.Layout() - self.graphFrame.UpdateWindowSize() + + if layout: + self.graphFrame.Layout() + self.graphFrame.UpdateWindowSize() def updateInputs(self): # Clean up old inputs From ee4a1f936bf6793fec742ae911590b021102ac8d Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 26 Jun 2019 15:46:17 +0300 Subject: [PATCH 28/93] Add two classes to handle user input --- gui/graphFrame/input.py | 98 +++++++++++++++++++++++++++++++++++++++++ gui/graphFrame/panel.py | 21 ++++----- 2 files changed, 107 insertions(+), 12 deletions(-) create mode 100644 gui/graphFrame/input.py diff --git a/gui/graphFrame/input.py b/gui/graphFrame/input.py new file mode 100644 index 000000000..cc73417ce --- /dev/null +++ b/gui/graphFrame/input.py @@ -0,0 +1,98 @@ +# ============================================================================= +# Copyright (C) 2010 Diego Duclos +# +# 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 . +# ============================================================================= + + +import wx + +import re + + +def valToStr(val): + if val is None: + return '' + if int(val) == val: + val = int(val) + return str(val) + + +def strToFloat(val): + try: + return float(val) + except ValueError: + return None + + +class ConstantBox(wx.TextCtrl): + + def __init__(self, parent, initial): + super().__init__(parent, wx.ID_ANY, style=0) + self.Bind(wx.EVT_TEXT, self.OnText) + self._storedValue = '' + self.ChangeValue(valToStr(initial)) + + + def ChangeValue(self, value): + self._storedValue = value + super().ChangeValue(value) + + def OnText(self, event): + currentValue = self.GetValue() + if currentValue == self._storedValue: + event.Skip() + return + if currentValue == '' or re.match('^\d*\.?\d*$', currentValue): + self._storedValue = currentValue + event.Skip() + else: + self.ChangeValue(self._storedValue) + + def GetValueFloat(self): + return strToFloat(self.GetValue()) + + +class RangeBox(wx.TextCtrl): + + def __init__(self, parent, initialLow, initialHigh): + super().__init__(parent, wx.ID_ANY, style=0) + self.Bind(wx.EVT_TEXT, self.OnText) + self._storedValue = '' + self.ChangeValue('{}-{}'.format(valToStr(initialLow), valToStr(initialHigh))) + + def ChangeValue(self, value): + self._storedValue = value + super().ChangeValue(value) + + def OnText(self, event): + currentValue = self.GetValue() + if currentValue == self._storedValue: + event.Skip() + return + if currentValue == '' or re.match('^\d*\.?\d*\-?\d*\.?\d*$', currentValue): + self._storedValue = currentValue + event.Skip() + else: + self.ChangeValue(self._storedValue) + + def GetValueRange(self): + parts = self.GetValue().split('-') + if len(parts) == 1: + val = strToFloat(parts) + return (val, val) + else: + return (strToFloat(parts[0]), strToFloat(parts[1])) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 1ffdf5b7c..504630ea0 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -23,6 +23,7 @@ import wx from gui.bitmap_loader import BitmapLoader from service.fit import Fit +from .input import ConstantBox, RangeBox from .lists import FitList, TargetList from .vector import VectorPicker @@ -143,18 +144,14 @@ class GraphControlPanel(wx.Panel): self.inputsSizer.Clear() self.inputs.clear() - def addInputField(inputDef, handledHandles): + def addInputField(inputDef, handledHandles, mainInput=False): handledHandles.add(inputDef.handle) fieldSizer = wx.BoxSizer(wx.HORIZONTAL) - fieldTextBox = wx.TextCtrl(self, wx.ID_ANY, style=0) + if mainInput: + fieldTextBox = RangeBox(self, *inputDef.defaultRange) + else: + fieldTextBox = ConstantBox(self, inputDef.defaultValue) fieldTextBox.Bind(wx.EVT_TEXT, self.OnFieldChanged) - if inputDef.defaultValue is not None: - inputDefault = inputDef.defaultValue - if not isinstance(inputDefault, str): - inputDefault = ('%f' % inputDefault).rstrip('0') - if inputDefault[-1:] == '.': - inputDefault += '0' - fieldTextBox.ChangeValue(inputDefault) fieldSizer.Add(fieldTextBox, 0, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) fieldIcon = None if inputDef.iconID is not None: @@ -182,7 +179,7 @@ class GraphControlPanel(wx.Panel): handledHandles.add(tgtVectorDef.angleHandle) selectedX = view.xDefMap[self.xType] # Always add main input - addInputField(view.inputMap[selectedX.mainInput], handledHandles) + addInputField(view.inputMap[selectedX.mainInput], handledHandles, mainInput=True) for inputDef in view.inputs: if inputDef.mainOnly: continue @@ -217,8 +214,8 @@ class GraphControlPanel(wx.Panel): def OnDrawTimer(self, event): event.Skip() - self.graphFrame.clearCache() - self.graphFrame.draw() + # self.graphFrame.clearCache() + # self.graphFrame.draw() def getValues(self): values = {} From 22ca78cb6804a83c51e4ca05fef7ed0fb349957e Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 26 Jun 2019 16:50:54 +0300 Subject: [PATCH 29/93] Implement method which gathers values across control panel boxes --- gui/graphFrame/frame.py | 5 +++-- gui/graphFrame/input.py | 6 +++--- gui/graphFrame/panel.py | 24 ++++++++++++++++++++++-- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index e15b3a64b..52a122a99 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -178,7 +178,8 @@ class GraphFrame(wx.Frame): return self.graphSelection.GetClientData(self.graphSelection.GetSelection()) def clearCache(self, key=None): - self.getView().clearCache(key=key) + pass + #self.getView().clearCache(key=key) def draw(self): global mpl_version @@ -188,7 +189,7 @@ class GraphFrame(wx.Frame): if not self: pyfalog.warning('GraphFrame handled event, however GraphFrame no longer exists. Ignoring event') return - + self.ctrlPanel.getValues() # values = self.ctrlPanel.getValues() # view = self.getView() # self.subplot.clear() diff --git a/gui/graphFrame/input.py b/gui/graphFrame/input.py index cc73417ce..1b23c4a48 100644 --- a/gui/graphFrame/input.py +++ b/gui/graphFrame/input.py @@ -18,10 +18,10 @@ # ============================================================================= -import wx - import re +import wx + def valToStr(val): if val is None: @@ -83,7 +83,7 @@ class RangeBox(wx.TextCtrl): if currentValue == self._storedValue: event.Skip() return - if currentValue == '' or re.match('^\d*\.?\d*\-?\d*\.?\d*$', currentValue): + if currentValue == '' or re.match('^\d*\.?\d*-?\d*\.?\d*$', currentValue): self._storedValue = currentValue event.Skip() else: diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 504630ea0..29432eab2 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -214,11 +214,31 @@ class GraphControlPanel(wx.Panel): def OnDrawTimer(self, event): event.Skip() - # self.graphFrame.clearCache() - # self.graphFrame.draw() + self.graphFrame.clearCache() + self.graphFrame.draw() def getValues(self): + view = self.graphFrame.getView() values = {} + # Vectors + srcVectorDef = view.srcVectorDef + if srcVectorDef is not None: + if not self.srcVector.IsDirectionOnly: + values[srcVectorDef.lengthHandle] = (self.srcVector.GetLength() * 100, srcVectorDef.lengthUnit) + values[srcVectorDef.angleHandle] = (self.srcVector.GetAngle(), srcVectorDef.angleUnit) + tgtVectorDef = view.tgtVectorDef + if tgtVectorDef is not None: + if not self.tgtVector.IsDirectionOnly: + values[tgtVectorDef.lengthHandle] = (self.tgtVector.GetLength() * 100, tgtVectorDef.lengthUnit) + values[tgtVectorDef.angleHandle] = (self.tgtVector.GetAngle(), srcVectorDef.angleUnit) + # Input boxes which were set up overwrite values if needed + for k, v in self.inputs.items(): + inputHandle, inputUnit = k + inputBox = v[0] + if isinstance(inputBox, RangeBox): + values[inputHandle] = (inputBox.GetValueRange(), inputUnit) + elif isinstance(inputBox, ConstantBox): + values[inputHandle] = (inputBox.GetValueFloat(), inputUnit) return values @property From fa4a2436aac089a47dcf06267e0374ac05327b65 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 26 Jun 2019 17:00:30 +0300 Subject: [PATCH 30/93] When vectors are changed, ask to update graphs --- gui/graphFrame/panel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 29432eab2..1051fa21d 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -69,6 +69,7 @@ class GraphControlPanel(wx.Panel): self.srcVectorLabel = wx.StaticText(self, wx.ID_ANY, '') self.srcVectorSizer.Add(self.srcVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL| wx.BOTTOM, 5) self.srcVector = VectorPicker(self, style=wx.NO_BORDER, size=75, offset=90) + self.srcVector.Bind(VectorPicker.EVT_VECTOR_CHANGED, self.OnFieldChanged) self.srcVectorSizer.Add(self.srcVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) graphOptsSizer.Add(self.srcVectorSizer, 0, wx.EXPAND | wx.LEFT, 15) @@ -76,6 +77,7 @@ class GraphControlPanel(wx.Panel): self.tgtVectorLabel = wx.StaticText(self, wx.ID_ANY, '') self.tgtVectorSizer.Add(self.tgtVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM, 5) self.tgtVector = VectorPicker(self, style=wx.NO_BORDER, size=75, offset=-90) + self.tgtVector.Bind(VectorPicker.EVT_VECTOR_CHANGED, self.OnFieldChanged) self.tgtVectorSizer.Add(self.tgtVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) graphOptsSizer.Add(self.tgtVectorSizer, 0, wx.EXPAND | wx.LEFT, 10) From 8e41a31d1de87c5dd5388805660ba7aa3f33a0ee Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 26 Jun 2019 18:03:37 +0300 Subject: [PATCH 31/93] Set vector defaults on initialization and graph switch --- gui/graphFrame/panel.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 1051fa21d..a00d432a0 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -28,6 +28,13 @@ from .lists import FitList, TargetList from .vector import VectorPicker +def range2Const(defRange, defConst,): + pass + +def const2Range(): + pass + + class GraphControlPanel(wx.Panel): def __init__(self, graphFrame, parent): @@ -99,6 +106,11 @@ class GraphControlPanel(wx.Panel): self.drawTimer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self.OnDrawTimer, self.drawTimer) + self.setVectorDefaults() + + def setVectorDefaults(self): + self.srcVector.SetValue(length=0, angle=0) + self.tgtVector.SetValue(length=1, angle=90) def updateControls(self, layout=True): view = self.graphFrame.getView() @@ -112,6 +124,7 @@ class GraphControlPanel(wx.Panel): self.xSubSelection.SetSelection(0) # Vectors + self.setVectorDefaults() if view.srcVectorDef is not None: self.srcVectorLabel.SetLabel(view.srcVectorDef.label) self.srcVector.Show(True) @@ -167,7 +180,6 @@ class GraphControlPanel(wx.Panel): self.inputs[(inputDef.handle, inputDef.unit)] = (fieldTextBox, fieldIcon, fieldLabel) self.inputsSizer.Add(fieldSizer, 0, wx.EXPAND | wx.BOTTOM, 5) - # Set up inputs view = self.graphFrame.getView() handledHandles = set() @@ -233,7 +245,7 @@ class GraphControlPanel(wx.Panel): if not self.tgtVector.IsDirectionOnly: values[tgtVectorDef.lengthHandle] = (self.tgtVector.GetLength() * 100, tgtVectorDef.lengthUnit) values[tgtVectorDef.angleHandle] = (self.tgtVector.GetAngle(), srcVectorDef.angleUnit) - # Input boxes which were set up overwrite values if needed + # Input boxes for k, v in self.inputs.items(): inputHandle, inputUnit = k inputBox = v[0] From 9ec192de7d9b1c1a1fc6342337b71b3bb7fe2141 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 26 Jun 2019 18:59:52 +0300 Subject: [PATCH 32/93] Add methods to convert input values --- gui/graphFrame/panel.py | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index a00d432a0..27bdbb69b 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -28,11 +28,42 @@ from .lists import FitList, TargetList from .vector import VectorPicker -def range2Const(defRange, defConst,): - pass +def range2Const(defConst, defRange, limits, oldRange): + if oldRange[0] is None or oldRange[1] is None: + return defConst + if oldRange[0] == oldRange[1]: + return oldRange[0] + defPos = (defConst - min(defRange)) / (max(defRange) - min(defRange)) + newConst = min(oldRange) + (max(oldRange) - min(oldRange)) * defPos + newConst = max(newConst, min(limits)) + newConst = min(newConst, max(limits)) + newConst = round(newConst, 3) + return newConst -def const2Range(): - pass + +def const2Range(defConst, defRange, limits, oldConst): + if oldConst is None: + return defRange + newMin = oldConst - defConst + min(defRange) + newMax = oldConst - defConst + max(defRange) + # Fits into limits + if newMin >= min(limits) and newMax <= max(limits): + pass + # Both do not fit + elif newMin < min(limits) and newMax > max(limits): + newMin = min(limits) + newMax = max(limits) + # Min doesn't fit + elif newMin < min(limits): + shift = min(limits) - newMin + newMin += shift + newMax += shift + # Max doesn't fit + elif newMax > max(limits): + shift = newMax - max(limits) + newMin -= shift + newMax -= shift + return round(newMin, 3), round(newMax, 3) class GraphControlPanel(wx.Panel): From 7895e4076dce6fc6ecde5c3c0b0a720307f74aed Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 26 Jun 2019 19:29:11 +0300 Subject: [PATCH 33/93] Add logic which transfers values when switching input fields --- gui/builtinGraphs/base.py | 4 ++-- gui/builtinGraphs/fitDamageStats.py | 10 ++++++---- gui/graphFrame/input.py | 6 +++--- gui/graphFrame/panel.py | 31 ++++++++++++++++++++++++++--- 4 files changed, 39 insertions(+), 12 deletions(-) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index 3df622d4c..c41cf03d6 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -114,9 +114,9 @@ class Graph(metaclass=ABCMeta): getattr(self, yDef.eosGraph).clearCache(key=key) -XDef = namedtuple('XDef', ('handle', 'unit', 'label', 'mainInput')) YDef = namedtuple('YDef', ('handle', 'unit', 'label', 'eosGraph')) -Input = namedtuple('Input', ('handle', 'unit', 'label', 'iconID', 'defaultValue', 'defaultRange', 'mainOnly')) +XDef = namedtuple('XDef', ('handle', 'unit', 'label', 'mainInput')) +Input = namedtuple('Input', ('handle', 'unit', 'label', 'iconID', 'defaultValue', 'defaultRange', 'limits', 'mainOnly')) VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle', 'angleUnit', 'label')) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 1baef53cd..dd6f1a4c4 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -18,6 +18,8 @@ # ============================================================================= +import math + from eos.graph.fitDpsVsRange import FitDpsVsRangeGraph as EosGraph from .base import Graph, XDef, YDef, Input, VectorDef @@ -50,10 +52,10 @@ class FitDamageStatsGraph(Graph): @property def inputs(self): return [ - Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=None, defaultRange=(0, 80), mainOnly=False), - Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=50, defaultRange=(0, 100), mainOnly=False), - Input(handle='tgtSpeed', unit='%', label='Target speed', iconID=1389, defaultValue=100, defaultRange=(0, 100), mainOnly=False), - Input(handle='tgtSigRad', unit='%', label='Target signature', iconID=1390, defaultValue=100, defaultRange=(100, 200), mainOnly=True)] + Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=None, defaultRange=(0, 80), limits=(0, math.inf), mainOnly=False), + Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=50, defaultRange=(0, 100), limits=(0, math.inf), mainOnly=False), + Input(handle='tgtSpeed', unit='%', label='Target speed', iconID=1389, defaultValue=100, defaultRange=(0, 100), limits=(0, math.inf), mainOnly=False), + Input(handle='tgtSigRad', unit='%', label='Target signature', iconID=1390, defaultValue=100, defaultRange=(100, 200), limits=(0, math.inf), mainOnly=True)] @property def srcVectorDef(self): diff --git a/gui/graphFrame/input.py b/gui/graphFrame/input.py index 1b23c4a48..5ddccb72c 100644 --- a/gui/graphFrame/input.py +++ b/gui/graphFrame/input.py @@ -68,11 +68,11 @@ class ConstantBox(wx.TextCtrl): class RangeBox(wx.TextCtrl): - def __init__(self, parent, initialLow, initialHigh): + def __init__(self, parent, initRange): super().__init__(parent, wx.ID_ANY, style=0) self.Bind(wx.EVT_TEXT, self.OnText) self._storedValue = '' - self.ChangeValue('{}-{}'.format(valToStr(initialLow), valToStr(initialHigh))) + self.ChangeValue('{}-{}'.format(valToStr(min(initRange)), valToStr(max(initRange)))) def ChangeValue(self, value): self._storedValue = value @@ -92,7 +92,7 @@ class RangeBox(wx.TextCtrl): def GetValueRange(self): parts = self.GetValue().split('-') if len(parts) == 1: - val = strToFloat(parts) + val = strToFloat(parts[0]) return (val, val) else: return (strToFloat(parts[0]), strToFloat(parts[1])) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 27bdbb69b..5789ddc5a 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -33,6 +33,8 @@ def range2Const(defConst, defRange, limits, oldRange): return defConst if oldRange[0] == oldRange[1]: return oldRange[0] + if defConst is None: + defConst = 0 defPos = (defConst - min(defRange)) / (max(defRange) - min(defRange)) newConst = min(oldRange) + (max(oldRange) - min(oldRange)) * defPos newConst = max(newConst, min(limits)) @@ -44,6 +46,8 @@ def range2Const(defConst, defRange, limits, oldRange): def const2Range(defConst, defRange, limits, oldConst): if oldConst is None: return defRange + if defConst is None: + defConst = 0 newMin = oldConst - defConst + min(defRange) newMax = oldConst - defConst + max(defRange) # Fits into limits @@ -72,6 +76,7 @@ class GraphControlPanel(wx.Panel): super().__init__(parent) self.graphFrame = graphFrame self.inputs = {} + self.storedValues = {} mainSizer = wx.BoxSizer(wx.VERTICAL) optsSizer = wx.BoxSizer(wx.HORIZONTAL) @@ -139,6 +144,12 @@ class GraphControlPanel(wx.Panel): self.Bind(wx.EVT_TIMER, self.OnDrawTimer, self.drawTimer) self.setVectorDefaults() + def storeCurrentValues(self): + for k, v in self.getValues().items(): + handle = k + value, unit = v + self.storedValues[(handle, unit)] = value + def setVectorDefaults(self): self.srcVector.SetValue(length=0, angle=0) self.tgtVector.SetValue(length=1, angle=90) @@ -182,6 +193,7 @@ class GraphControlPanel(wx.Panel): self.graphFrame.UpdateWindowSize() def updateInputs(self): + self.storeCurrentValues() # Clean up old inputs for children in self.inputs.values(): for child in children: @@ -194,9 +206,23 @@ class GraphControlPanel(wx.Panel): handledHandles.add(inputDef.handle) fieldSizer = wx.BoxSizer(wx.HORIZONTAL) if mainInput: - fieldTextBox = RangeBox(self, *inputDef.defaultRange) + initRange = self.storedValues.get((inputDef.handle, inputDef.unit), inputDef.defaultRange) + if not isinstance(initRange, (tuple, list)): + initRange = const2Range( + defConst=inputDef.defaultValue, + defRange=inputDef.defaultRange, + limits=inputDef.limits, + oldConst=initRange) + fieldTextBox = RangeBox(self, initRange) else: - fieldTextBox = ConstantBox(self, inputDef.defaultValue) + initConst = self.storedValues.get((inputDef.handle, inputDef.unit), inputDef.defaultValue) + if isinstance(initConst, (tuple, list)): + initConst = range2Const( + defConst=inputDef.defaultValue, + defRange=inputDef.defaultRange, + limits=inputDef.limits, + oldRange=initConst) + fieldTextBox = ConstantBox(self, initConst) fieldTextBox.Bind(wx.EVT_TEXT, self.OnFieldChanged) fieldSizer.Add(fieldTextBox, 0, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) fieldIcon = None @@ -211,7 +237,6 @@ class GraphControlPanel(wx.Panel): self.inputs[(inputDef.handle, inputDef.unit)] = (fieldTextBox, fieldIcon, fieldLabel) self.inputsSizer.Add(fieldSizer, 0, wx.EXPAND | wx.BOTTOM, 5) - # Set up inputs view = self.graphFrame.getView() handledHandles = set() srcVectorDef = view.srcVectorDef From b125c62930463685472ea68ee6ad3dc11f83b22c Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 26 Jun 2019 19:30:16 +0300 Subject: [PATCH 34/93] Reset stored values when switching graphs --- gui/graphFrame/panel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 5789ddc5a..8bba7d6bd 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -155,6 +155,7 @@ class GraphControlPanel(wx.Panel): self.tgtVector.SetValue(length=1, angle=90) def updateControls(self, layout=True): + self.storedValues.clear() view = self.graphFrame.getView() self.ySubSelection.Clear() self.xSubSelection.Clear() From b733205541474e4697a2c7d3dd7250561076bfbc Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 26 Jun 2019 19:58:26 +0300 Subject: [PATCH 35/93] Get rid of this smart shit and just store ranges and consts separately --- gui/graphFrame/panel.py | 104 +++++++++++----------------------------- 1 file changed, 28 insertions(+), 76 deletions(-) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 8bba7d6bd..2c4b772b1 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -28,55 +28,14 @@ from .lists import FitList, TargetList from .vector import VectorPicker -def range2Const(defConst, defRange, limits, oldRange): - if oldRange[0] is None or oldRange[1] is None: - return defConst - if oldRange[0] == oldRange[1]: - return oldRange[0] - if defConst is None: - defConst = 0 - defPos = (defConst - min(defRange)) / (max(defRange) - min(defRange)) - newConst = min(oldRange) + (max(oldRange) - min(oldRange)) * defPos - newConst = max(newConst, min(limits)) - newConst = min(newConst, max(limits)) - newConst = round(newConst, 3) - return newConst - - -def const2Range(defConst, defRange, limits, oldConst): - if oldConst is None: - return defRange - if defConst is None: - defConst = 0 - newMin = oldConst - defConst + min(defRange) - newMax = oldConst - defConst + max(defRange) - # Fits into limits - if newMin >= min(limits) and newMax <= max(limits): - pass - # Both do not fit - elif newMin < min(limits) and newMax > max(limits): - newMin = min(limits) - newMax = max(limits) - # Min doesn't fit - elif newMin < min(limits): - shift = min(limits) - newMin - newMin += shift - newMax += shift - # Max doesn't fit - elif newMax > max(limits): - shift = newMax - max(limits) - newMin -= shift - newMax -= shift - return round(newMin, 3), round(newMax, 3) - - class GraphControlPanel(wx.Panel): def __init__(self, graphFrame, parent): super().__init__(parent) self.graphFrame = graphFrame self.inputs = {} - self.storedValues = {} + self._storedRanges = {} + self._storedConsts = {} mainSizer = wx.BoxSizer(wx.VERTICAL) optsSizer = wx.BoxSizer(wx.HORIZONTAL) @@ -142,20 +101,10 @@ class GraphControlPanel(wx.Panel): self.drawTimer = wx.Timer(self) self.Bind(wx.EVT_TIMER, self.OnDrawTimer, self.drawTimer) - self.setVectorDefaults() - - def storeCurrentValues(self): - for k, v in self.getValues().items(): - handle = k - value, unit = v - self.storedValues[(handle, unit)] = value - - def setVectorDefaults(self): - self.srcVector.SetValue(length=0, angle=0) - self.tgtVector.SetValue(length=1, angle=90) + self._setVectorDefaults() def updateControls(self, layout=True): - self.storedValues.clear() + self._clearStoredValues() view = self.graphFrame.getView() self.ySubSelection.Clear() self.xSubSelection.Clear() @@ -167,7 +116,7 @@ class GraphControlPanel(wx.Panel): self.xSubSelection.SetSelection(0) # Vectors - self.setVectorDefaults() + self._setVectorDefaults() if view.srcVectorDef is not None: self.srcVectorLabel.SetLabel(view.srcVectorDef.label) self.srcVector.Show(True) @@ -187,14 +136,14 @@ class GraphControlPanel(wx.Panel): self.targetList.Show(view.hasTargets) # Inputs - self.updateInputs() + self._updateInputs() if layout: self.graphFrame.Layout() self.graphFrame.UpdateWindowSize() - def updateInputs(self): - self.storeCurrentValues() + def _updateInputs(self): + self._storeCurrentValues() # Clean up old inputs for children in self.inputs.values(): for child in children: @@ -207,23 +156,9 @@ class GraphControlPanel(wx.Panel): handledHandles.add(inputDef.handle) fieldSizer = wx.BoxSizer(wx.HORIZONTAL) if mainInput: - initRange = self.storedValues.get((inputDef.handle, inputDef.unit), inputDef.defaultRange) - if not isinstance(initRange, (tuple, list)): - initRange = const2Range( - defConst=inputDef.defaultValue, - defRange=inputDef.defaultRange, - limits=inputDef.limits, - oldConst=initRange) - fieldTextBox = RangeBox(self, initRange) + fieldTextBox = RangeBox(self, self._storedRanges.get((inputDef.handle, inputDef.unit), inputDef.defaultRange)) else: - initConst = self.storedValues.get((inputDef.handle, inputDef.unit), inputDef.defaultValue) - if isinstance(initConst, (tuple, list)): - initConst = range2Const( - defConst=inputDef.defaultValue, - defRange=inputDef.defaultRange, - limits=inputDef.limits, - oldRange=initConst) - fieldTextBox = ConstantBox(self, initConst) + fieldTextBox = ConstantBox(self, self._storedConsts.get((inputDef.handle, inputDef.unit), inputDef.defaultValue)) fieldTextBox.Bind(wx.EVT_TEXT, self.OnFieldChanged) fieldSizer.Add(fieldTextBox, 0, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) fieldIcon = None @@ -273,7 +208,7 @@ class GraphControlPanel(wx.Panel): def OnXTypeUpdate(self, event): event.Skip() - self.updateInputs() + self._updateInputs() self.graphFrame.Layout() self.graphFrame.UpdateWindowSize() self.graphFrame.draw() @@ -331,3 +266,20 @@ class GraphControlPanel(wx.Panel): if axisDef.unit is None: return axisDef.label return '{}, {}'.format(axisDef.label, axisDef.unit) + + def _storeCurrentValues(self): + for k, v in self.getValues().items(): + handle = k + value, unit = v + if isinstance(value, (tuple, list)): + self._storedRanges[(handle, unit)] = value + else: + self._storedConsts[(handle, unit)] = value + + def _clearStoredValues(self): + self._storedRanges.clear() + self._storedRanges.clear() + + def _setVectorDefaults(self): + self.srcVector.SetValue(length=0, angle=0) + self.tgtVector.SetValue(length=1, angle=90) From 5320e99276d8261fc838e05126f2f862b1bb2caa Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 26 Jun 2019 20:06:51 +0300 Subject: [PATCH 36/93] Restore values for vectors (just in case!) --- gui/graphFrame/panel.py | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 2c4b772b1..48cb45c0c 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -173,16 +173,28 @@ class GraphControlPanel(wx.Panel): self.inputs[(inputDef.handle, inputDef.unit)] = (fieldTextBox, fieldIcon, fieldLabel) self.inputsSizer.Add(fieldSizer, 0, wx.EXPAND | wx.BOTTOM, 5) + def handleVector(vectorDef, vector, handledHandles): + handledHandles.add(vectorDef.lengthHandle) + handledHandles.add(vectorDef.angleHandle) + try: + storedLength = self._storedConsts[(vectorDef.lengthHandle, vectorDef.lengthUnit)] + except KeyError: + pass + else: + vector.SetLength(storedLength / 100) + try: + storedAngle = self._storedConsts[(vectorDef.angleHandle, vectorDef.angleUnit)] + except KeyError: + pass + else: + vector.SetAngle(storedAngle) + view = self.graphFrame.getView() handledHandles = set() - srcVectorDef = view.srcVectorDef - if srcVectorDef is not None: - handledHandles.add(srcVectorDef.lengthHandle) - handledHandles.add(srcVectorDef.angleHandle) - tgtVectorDef = view.tgtVectorDef - if tgtVectorDef is not None: - handledHandles.add(tgtVectorDef.lengthHandle) - handledHandles.add(tgtVectorDef.angleHandle) + if view.srcVectorDef is not None: + handleVector(view.srcVectorDef, self.srcVector, handledHandles) + if view.tgtVectorDef is not None: + handleVector(view.tgtVectorDef, self.tgtVector, handledHandles) selectedX = view.xDefMap[self.xType] # Always add main input addInputField(view.inputMap[selectedX.mainInput], handledHandles, mainInput=True) From 4af36514bc360009fb5c829387a0be04b0f31bb5 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 26 Jun 2019 20:12:08 +0300 Subject: [PATCH 37/93] Handle vectors a in a separate function --- gui/graphFrame/panel.py | 56 ++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 29 deletions(-) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 48cb45c0c..b905da97f 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -152,6 +152,33 @@ class GraphControlPanel(wx.Panel): self.inputsSizer.Clear() self.inputs.clear() + # Update vectors + def handleVector(vectorDef, vector, handledHandles, mainInputHandle): + handledHandles.add(vectorDef.lengthHandle) + handledHandles.add(vectorDef.angleHandle) + try: + storedLength = self._storedConsts[(vectorDef.lengthHandle, vectorDef.lengthUnit)] + except KeyError: + pass + else: + vector.SetLength(storedLength / 100) + try: + storedAngle = self._storedConsts[(vectorDef.angleHandle, vectorDef.angleUnit)] + except KeyError: + pass + else: + vector.SetAngle(storedAngle) + vector.SetDirectionOnly(vectorDef.lengthHandle == mainInputHandle) + + view = self.graphFrame.getView() + selectedX = view.xDefMap[self.xType] + handledHandles = set() + if view.srcVectorDef is not None: + handleVector(view.srcVectorDef, self.srcVector, handledHandles, selectedX.mainInput[0]) + if view.tgtVectorDef is not None: + handleVector(view.tgtVectorDef, self.tgtVector, handledHandles, selectedX.mainInput[0]) + + # Update inputs def addInputField(inputDef, handledHandles, mainInput=False): handledHandles.add(inputDef.handle) fieldSizer = wx.BoxSizer(wx.HORIZONTAL) @@ -173,30 +200,6 @@ class GraphControlPanel(wx.Panel): self.inputs[(inputDef.handle, inputDef.unit)] = (fieldTextBox, fieldIcon, fieldLabel) self.inputsSizer.Add(fieldSizer, 0, wx.EXPAND | wx.BOTTOM, 5) - def handleVector(vectorDef, vector, handledHandles): - handledHandles.add(vectorDef.lengthHandle) - handledHandles.add(vectorDef.angleHandle) - try: - storedLength = self._storedConsts[(vectorDef.lengthHandle, vectorDef.lengthUnit)] - except KeyError: - pass - else: - vector.SetLength(storedLength / 100) - try: - storedAngle = self._storedConsts[(vectorDef.angleHandle, vectorDef.angleUnit)] - except KeyError: - pass - else: - vector.SetAngle(storedAngle) - - view = self.graphFrame.getView() - handledHandles = set() - if view.srcVectorDef is not None: - handleVector(view.srcVectorDef, self.srcVector, handledHandles) - if view.tgtVectorDef is not None: - handleVector(view.tgtVectorDef, self.tgtVector, handledHandles) - selectedX = view.xDefMap[self.xType] - # Always add main input addInputField(view.inputMap[selectedX.mainInput], handledHandles, mainInput=True) for inputDef in view.inputs: if inputDef.mainOnly: @@ -205,11 +208,6 @@ class GraphControlPanel(wx.Panel): continue addInputField(inputDef, handledHandles) - # Modify vectors - mainInputHandle = selectedX.mainInput[0] - self.srcVector.SetDirectionOnly(view.srcVectorDef.lengthHandle == mainInputHandle) - self.tgtVector.SetDirectionOnly(view.tgtVectorDef.lengthHandle == mainInputHandle) - def OnShowY0Change(self, event): event.Skip() self.graphFrame.draw() From 9b282587b2fb8c7598ee986344e8dd031fb872af Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 26 Jun 2019 21:24:02 +0300 Subject: [PATCH 38/93] Remove limits argument as it's no longer used --- gui/builtinGraphs/base.py | 2 +- gui/builtinGraphs/fitDamageStats.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index c41cf03d6..c3cd36cdb 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -116,7 +116,7 @@ class Graph(metaclass=ABCMeta): YDef = namedtuple('YDef', ('handle', 'unit', 'label', 'eosGraph')) XDef = namedtuple('XDef', ('handle', 'unit', 'label', 'mainInput')) -Input = namedtuple('Input', ('handle', 'unit', 'label', 'iconID', 'defaultValue', 'defaultRange', 'limits', 'mainOnly')) +Input = namedtuple('Input', ('handle', 'unit', 'label', 'iconID', 'defaultValue', 'defaultRange', 'mainOnly')) VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle', 'angleUnit', 'label')) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index dd6f1a4c4..fcceff5b9 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -52,10 +52,10 @@ class FitDamageStatsGraph(Graph): @property def inputs(self): return [ - Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=None, defaultRange=(0, 80), limits=(0, math.inf), mainOnly=False), - Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=50, defaultRange=(0, 100), limits=(0, math.inf), mainOnly=False), - Input(handle='tgtSpeed', unit='%', label='Target speed', iconID=1389, defaultValue=100, defaultRange=(0, 100), limits=(0, math.inf), mainOnly=False), - Input(handle='tgtSigRad', unit='%', label='Target signature', iconID=1390, defaultValue=100, defaultRange=(100, 200), limits=(0, math.inf), mainOnly=True)] + Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=None, defaultRange=(0, 80), mainOnly=False), + Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=50, defaultRange=(0, 100), mainOnly=False), + Input(handle='tgtSpeed', unit='%', label='Target speed', iconID=1389, defaultValue=100, defaultRange=(0, 100), mainOnly=False), + Input(handle='tgtSigRad', unit='%', label='Target signature', iconID=1390, defaultValue=100, defaultRange=(100, 200), mainOnly=True)] @property def srcVectorDef(self): From 7ef79eaa79c53a87b8ab38bad002907f22a9341c Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 26 Jun 2019 21:39:12 +0300 Subject: [PATCH 39/93] Stop using units as part of input key, they are not going to be different anyway --- gui/builtinGraphs/base.py | 2 +- gui/builtinGraphs/fitDamageStats.py | 4 +-- gui/graphFrame/panel.py | 41 +++++++++++++---------------- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index c3cd36cdb..20dbc982a 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -117,7 +117,7 @@ class Graph(metaclass=ABCMeta): YDef = namedtuple('YDef', ('handle', 'unit', 'label', 'eosGraph')) XDef = namedtuple('XDef', ('handle', 'unit', 'label', 'mainInput')) Input = namedtuple('Input', ('handle', 'unit', 'label', 'iconID', 'defaultValue', 'defaultRange', 'mainOnly')) -VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle', 'angleUnit', 'label')) +VectorDef = namedtuple('VectorDef', ('lengthHandle', 'angleHandle', 'label')) # noinspection PyUnresolvedReferences diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index fcceff5b9..113ec266b 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -59,11 +59,11 @@ class FitDamageStatsGraph(Graph): @property def srcVectorDef(self): - return VectorDef(lengthHandle='atkSpeed', lengthUnit='%', angleHandle='atkAngle', angleUnit='degrees', label='Attacker') + return VectorDef(lengthHandle='atkSpeed', angleHandle='atkAngle', label='Attacker') @property def tgtVectorDef(self): - return VectorDef(lengthHandle='tgtSpeed', lengthUnit='%', angleHandle='tgtAngle', angleUnit='degrees', label='Target') + return VectorDef(lengthHandle='tgtSpeed', angleHandle='tgtAngle', label='Target') @property def hasTargets(self): diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index b905da97f..937e36722 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -33,7 +33,7 @@ class GraphControlPanel(wx.Panel): def __init__(self, graphFrame, parent): super().__init__(parent) self.graphFrame = graphFrame - self.inputs = {} + self._inputs = {} self._storedRanges = {} self._storedConsts = {} @@ -145,25 +145,25 @@ class GraphControlPanel(wx.Panel): def _updateInputs(self): self._storeCurrentValues() # Clean up old inputs - for children in self.inputs.values(): + for children in self._inputs.values(): for child in children: if child is not None: child.Destroy() self.inputsSizer.Clear() - self.inputs.clear() + self._inputs.clear() # Update vectors def handleVector(vectorDef, vector, handledHandles, mainInputHandle): handledHandles.add(vectorDef.lengthHandle) handledHandles.add(vectorDef.angleHandle) try: - storedLength = self._storedConsts[(vectorDef.lengthHandle, vectorDef.lengthUnit)] + storedLength = self._storedConsts[vectorDef.lengthHandle] except KeyError: pass else: vector.SetLength(storedLength / 100) try: - storedAngle = self._storedConsts[(vectorDef.angleHandle, vectorDef.angleUnit)] + storedAngle = self._storedConsts[vectorDef.angleHandle] except KeyError: pass else: @@ -183,9 +183,9 @@ class GraphControlPanel(wx.Panel): handledHandles.add(inputDef.handle) fieldSizer = wx.BoxSizer(wx.HORIZONTAL) if mainInput: - fieldTextBox = RangeBox(self, self._storedRanges.get((inputDef.handle, inputDef.unit), inputDef.defaultRange)) + fieldTextBox = RangeBox(self, self._storedRanges.get(inputDef.handle, inputDef.defaultRange)) else: - fieldTextBox = ConstantBox(self, self._storedConsts.get((inputDef.handle, inputDef.unit), inputDef.defaultValue)) + fieldTextBox = ConstantBox(self, self._storedConsts.get(inputDef.handle, inputDef.defaultValue)) fieldTextBox.Bind(wx.EVT_TEXT, self.OnFieldChanged) fieldSizer.Add(fieldTextBox, 0, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) fieldIcon = None @@ -197,7 +197,7 @@ class GraphControlPanel(wx.Panel): fieldSizer.Add(fieldIcon, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 3) fieldLabel = wx.StaticText(self, wx.ID_ANY, self._formatLabel(inputDef)) fieldSizer.Add(fieldLabel, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) - self.inputs[(inputDef.handle, inputDef.unit)] = (fieldTextBox, fieldIcon, fieldLabel) + self._inputs[inputDef.handle] = (fieldTextBox, fieldIcon, fieldLabel) self.inputsSizer.Add(fieldSizer, 0, wx.EXPAND | wx.BOTTOM, 5) addInputField(view.inputMap[selectedX.mainInput], handledHandles, mainInput=True) @@ -240,21 +240,20 @@ class GraphControlPanel(wx.Panel): srcVectorDef = view.srcVectorDef if srcVectorDef is not None: if not self.srcVector.IsDirectionOnly: - values[srcVectorDef.lengthHandle] = (self.srcVector.GetLength() * 100, srcVectorDef.lengthUnit) - values[srcVectorDef.angleHandle] = (self.srcVector.GetAngle(), srcVectorDef.angleUnit) + values[srcVectorDef.lengthHandle] = self.srcVector.GetLength() * 100 + values[srcVectorDef.angleHandle] = self.srcVector.GetAngle() tgtVectorDef = view.tgtVectorDef if tgtVectorDef is not None: if not self.tgtVector.IsDirectionOnly: - values[tgtVectorDef.lengthHandle] = (self.tgtVector.GetLength() * 100, tgtVectorDef.lengthUnit) - values[tgtVectorDef.angleHandle] = (self.tgtVector.GetAngle(), srcVectorDef.angleUnit) + values[tgtVectorDef.lengthHandle] = self.tgtVector.GetLength() * 100 + values[tgtVectorDef.angleHandle] = self.tgtVector.GetAngle() # Input boxes - for k, v in self.inputs.items(): - inputHandle, inputUnit = k - inputBox = v[0] + for inputHandle, inputData in self._inputs.items(): + inputBox = inputData[0] if isinstance(inputBox, RangeBox): - values[inputHandle] = (inputBox.GetValueRange(), inputUnit) + values[inputHandle] = inputBox.GetValueRange() elif isinstance(inputBox, ConstantBox): - values[inputHandle] = (inputBox.GetValueFloat(), inputUnit) + values[inputHandle] = inputBox.GetValueFloat() return values @property @@ -278,13 +277,11 @@ class GraphControlPanel(wx.Panel): return '{}, {}'.format(axisDef.label, axisDef.unit) def _storeCurrentValues(self): - for k, v in self.getValues().items(): - handle = k - value, unit = v + for handle, value in self.getValues().items(): if isinstance(value, (tuple, list)): - self._storedRanges[(handle, unit)] = value + self._storedRanges[handle] = value else: - self._storedConsts[(handle, unit)] = value + self._storedConsts[handle] = value def _clearStoredValues(self): self._storedRanges.clear() From fe50372b1282d8243a17631b8d8e5d12391769a5 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 27 Jun 2019 13:15:22 +0300 Subject: [PATCH 40/93] Use unit as part of key again will be useful in warp graph --- gui/builtinGraphs/base.py | 2 +- gui/builtinGraphs/fitDamageStats.py | 4 ++-- gui/graphFrame/panel.py | 35 ++++++++++++++++------------- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index 20dbc982a..c3cd36cdb 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -117,7 +117,7 @@ class Graph(metaclass=ABCMeta): YDef = namedtuple('YDef', ('handle', 'unit', 'label', 'eosGraph')) XDef = namedtuple('XDef', ('handle', 'unit', 'label', 'mainInput')) Input = namedtuple('Input', ('handle', 'unit', 'label', 'iconID', 'defaultValue', 'defaultRange', 'mainOnly')) -VectorDef = namedtuple('VectorDef', ('lengthHandle', 'angleHandle', 'label')) +VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle', 'angleUnit', 'label')) # noinspection PyUnresolvedReferences diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 113ec266b..fcceff5b9 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -59,11 +59,11 @@ class FitDamageStatsGraph(Graph): @property def srcVectorDef(self): - return VectorDef(lengthHandle='atkSpeed', angleHandle='atkAngle', label='Attacker') + return VectorDef(lengthHandle='atkSpeed', lengthUnit='%', angleHandle='atkAngle', angleUnit='degrees', label='Attacker') @property def tgtVectorDef(self): - return VectorDef(lengthHandle='tgtSpeed', angleHandle='tgtAngle', label='Target') + return VectorDef(lengthHandle='tgtSpeed', lengthUnit='%', angleHandle='tgtAngle', angleUnit='degrees', label='Target') @property def hasTargets(self): diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 937e36722..35be02b9b 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -157,13 +157,13 @@ class GraphControlPanel(wx.Panel): handledHandles.add(vectorDef.lengthHandle) handledHandles.add(vectorDef.angleHandle) try: - storedLength = self._storedConsts[vectorDef.lengthHandle] + storedLength = self._storedConsts[(vectorDef.lengthHandle, vectorDef.lengthUnit)] except KeyError: pass else: vector.SetLength(storedLength / 100) try: - storedAngle = self._storedConsts[vectorDef.angleHandle] + storedAngle = self._storedConsts[(vectorDef.angleHandle, vectorDef.angleUnit)] except KeyError: pass else: @@ -183,9 +183,9 @@ class GraphControlPanel(wx.Panel): handledHandles.add(inputDef.handle) fieldSizer = wx.BoxSizer(wx.HORIZONTAL) if mainInput: - fieldTextBox = RangeBox(self, self._storedRanges.get(inputDef.handle, inputDef.defaultRange)) + fieldTextBox = RangeBox(self, self._storedRanges.get((inputDef.handle, inputDef.unit), inputDef.defaultRange)) else: - fieldTextBox = ConstantBox(self, self._storedConsts.get(inputDef.handle, inputDef.defaultValue)) + fieldTextBox = ConstantBox(self, self._storedConsts.get((inputDef.handle, inputDef.unit), inputDef.defaultValue)) fieldTextBox.Bind(wx.EVT_TEXT, self.OnFieldChanged) fieldSizer.Add(fieldTextBox, 0, wx.EXPAND | wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) fieldIcon = None @@ -197,7 +197,7 @@ class GraphControlPanel(wx.Panel): fieldSizer.Add(fieldIcon, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 3) fieldLabel = wx.StaticText(self, wx.ID_ANY, self._formatLabel(inputDef)) fieldSizer.Add(fieldLabel, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) - self._inputs[inputDef.handle] = (fieldTextBox, fieldIcon, fieldLabel) + self._inputs[(inputDef.handle, inputDef.unit)] = (fieldTextBox, fieldIcon, fieldLabel) self.inputsSizer.Add(fieldSizer, 0, wx.EXPAND | wx.BOTTOM, 5) addInputField(view.inputMap[selectedX.mainInput], handledHandles, mainInput=True) @@ -240,20 +240,21 @@ class GraphControlPanel(wx.Panel): srcVectorDef = view.srcVectorDef if srcVectorDef is not None: if not self.srcVector.IsDirectionOnly: - values[srcVectorDef.lengthHandle] = self.srcVector.GetLength() * 100 - values[srcVectorDef.angleHandle] = self.srcVector.GetAngle() + values[srcVectorDef.lengthHandle] = (self.srcVector.GetLength() * 100, srcVectorDef.lengthUnit) + values[srcVectorDef.angleHandle] = (self.srcVector.GetAngle(), srcVectorDef.angleUnit) tgtVectorDef = view.tgtVectorDef if tgtVectorDef is not None: if not self.tgtVector.IsDirectionOnly: - values[tgtVectorDef.lengthHandle] = self.tgtVector.GetLength() * 100 - values[tgtVectorDef.angleHandle] = self.tgtVector.GetAngle() + values[tgtVectorDef.lengthHandle] = (self.tgtVector.GetLength() * 100, tgtVectorDef.lengthUnit) + values[tgtVectorDef.angleHandle] = (self.tgtVector.GetAngle(), tgtVectorDef.angleUnit) # Input boxes - for inputHandle, inputData in self._inputs.items(): - inputBox = inputData[0] + for k, v in self._inputs.items(): + inputHandle, inputUnit = k + inputBox = v[0] if isinstance(inputBox, RangeBox): - values[inputHandle] = inputBox.GetValueRange() + values[inputHandle] = (inputBox.GetValueRange(), inputUnit) elif isinstance(inputBox, ConstantBox): - values[inputHandle] = inputBox.GetValueFloat() + values[inputHandle] = (inputBox.GetValueFloat(), inputUnit) return values @property @@ -277,11 +278,13 @@ class GraphControlPanel(wx.Panel): return '{}, {}'.format(axisDef.label, axisDef.unit) def _storeCurrentValues(self): - for handle, value in self.getValues().items(): + for k, v in self.getValues().items(): + handle = k + value, unit = v if isinstance(value, (tuple, list)): - self._storedRanges[handle] = value + self._storedRanges[(handle, unit)] = value else: - self._storedConsts[handle] = value + self._storedConsts[(handle, unit)] = value def _clearStoredValues(self): self._storedRanges.clear() From 1e760b211172dac090f5c40ca514184267d0b9a0 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 27 Jun 2019 16:54:12 +0300 Subject: [PATCH 41/93] Re-enable warp graph and adapt it to new framework --- gui/builtinGraphs/__init__.py | 2 +- gui/builtinGraphs/fitWarpTimeVsDistance.py | 20 +++++++++++++------- gui/graphFrame/panel.py | 11 +++++++---- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/gui/builtinGraphs/__init__.py b/gui/builtinGraphs/__init__.py index de2446088..5cf1cd831 100644 --- a/gui/builtinGraphs/__init__.py +++ b/gui/builtinGraphs/__init__.py @@ -7,5 +7,5 @@ from gui.builtinGraphs import ( # noqa: E402,F401 # fitCapRegenVsCapPerc, # fitCapAmountVsTime, # fitMobilityVsTime, - # fitWarpTimeVsDistance + fitWarpTimeVsDistance ) diff --git a/gui/builtinGraphs/fitWarpTimeVsDistance.py b/gui/builtinGraphs/fitWarpTimeVsDistance.py index c7bce5c1d..b54015dff 100644 --- a/gui/builtinGraphs/fitWarpTimeVsDistance.py +++ b/gui/builtinGraphs/fitWarpTimeVsDistance.py @@ -18,27 +18,33 @@ # ============================================================================= -from collections import OrderedDict - from eos.graph.fitWarpTimeVsDistance import FitWarpTimeVsDistanceGraph as EosGraph -from .base import Graph, XDef, YDef +from .base import Graph, XDef, YDef, Input class FitWarpTimeVsDistanceGraph(Graph): - name = 'Warp Time vs Distance' + name = 'Warp Time' def __init__(self): super().__init__() self.eosGraph = EosGraph() @property - def xDef(self): - return XDef(inputDefault='0-50', inputLabel='Distance (AU)', inputIconID=1391, axisLabel='Warp distance, AU') + def xDefs(self): + return [ + XDef(handle='distance', unit='AU', label='Distance', mainInput=('distance', 'AU')), + XDef(handle='distance', unit='km', label='Distance', mainInput=('distance', 'km'))] @property def yDefs(self): - return OrderedDict([('time', YDef(switchLabel='Warp time', axisLabel='Warp time, s', eosGraph='eosGraph'))]) + return [YDef(handle='time', unit='s', label='Warp time', eosGraph='eosGraph')] + + @property + def inputs(self): + return [ + Input(handle='distance', unit='AU', label='Distance', iconID=1391, defaultValue=50, defaultRange=(0, 50), mainOnly=False), + Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=10000, defaultRange=(150, 5000), mainOnly=False)] FitWarpTimeVsDistanceGraph.register() diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 35be02b9b..4d6f25767 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -111,9 +111,11 @@ class GraphControlPanel(wx.Panel): for yDef in view.yDefs: self.ySubSelection.Append(self._formatLabel(yDef), (yDef.handle, yDef.unit)) self.ySubSelection.SetSelection(0) + self.ySubSelection.Enable(len(view.yDefs) > 1) for xDef in view.xDefs: self.xSubSelection.Append(self._formatLabel(xDef), (xDef.handle, xDef.unit)) self.xSubSelection.SetSelection(0) + self.xSubSelection.Enable(len(view.xDefs) > 1) # Vectors self._setVectorDefaults() @@ -136,14 +138,15 @@ class GraphControlPanel(wx.Panel): self.targetList.Show(view.hasTargets) # Inputs - self._updateInputs() + self._updateInputs(storeInputs=False) if layout: self.graphFrame.Layout() self.graphFrame.UpdateWindowSize() - def _updateInputs(self): - self._storeCurrentValues() + def _updateInputs(self, storeInputs=True): + if storeInputs: + self._storeCurrentValues() # Clean up old inputs for children in self._inputs.values(): for child in children: @@ -287,7 +290,7 @@ class GraphControlPanel(wx.Panel): self._storedConsts[(handle, unit)] = value def _clearStoredValues(self): - self._storedRanges.clear() + self._storedConsts.clear() self._storedRanges.clear() def _setVectorDefaults(self): From ef81f9c83065fc42720985ac3fe503621faf9379 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 27 Jun 2019 18:52:23 +0300 Subject: [PATCH 42/93] Return input data in InputData objects for easier access --- gui/builtinGraphs/base.py | 18 ++-------- gui/graphFrame/frame.py | 10 +++--- gui/graphFrame/panel.py | 74 +++++++++++++++++++++++++-------------- 3 files changed, 55 insertions(+), 47 deletions(-) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index c3cd36cdb..935e65ce4 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -82,11 +82,10 @@ class Graph(metaclass=ABCMeta): def redrawOnEffectiveChange(self): return False - def getPlotPoints(self, fit, extraData, xRange, xAmount, yType): + def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt=None): try: - plotData = self._cache[fit.ID][yType] + plotData = self._cache[fit.ID][ySpec] except KeyError: - xRange = self.parseRange(xRange) extraData = {k: float(v) if v else None for k, v in extraData.items()} graph = getattr(self, self.yDefs[yType].eosGraph, None) plotData = graph.getPlotPoints(fit, extraData, xRange, xAmount) @@ -94,23 +93,12 @@ class Graph(metaclass=ABCMeta): fitCache[yType] = plotData return plotData - def parseRange(self, string): - m = re.match('\s*(?P\d+(\.\d+)?)\s*(-\s*(?P\d+(\.\d+)?))?', string) - if m is None: - return (0, 0) - first = float(m.group('first')) - second = m.group('second') - second = float(second) if second is not None else 0 - low = min(first, second) - high = max(first, second) - return (low, high) - def clearCache(self, key=None): if key is None: self._cache.clear() elif key in self._cache: del self._cache[key] - for yDef in self.yDefs.values(): + for yDef in self.yDefs: getattr(self, yDef.eosGraph).clearCache(key=key) diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index 52a122a99..2b4a4e395 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -178,8 +178,7 @@ class GraphFrame(wx.Frame): return self.graphSelection.GetClientData(self.graphSelection.GetSelection()) def clearCache(self, key=None): - pass - #self.getView().clearCache(key=key) + self.getView().clearCache(key=key) def draw(self): global mpl_version @@ -189,8 +188,7 @@ class GraphFrame(wx.Frame): if not self: pyfalog.warning('GraphFrame handled event, however GraphFrame no longer exists. Ignoring event') return - self.ctrlPanel.getValues() - # values = self.ctrlPanel.getValues() + values = self.ctrlPanel.getValues() # view = self.getView() # self.subplot.clear() # self.subplot.grid(True) @@ -274,6 +272,6 @@ class GraphFrame(wx.Frame): # # for l in leg.get_lines(): # l.set_linewidth(1) - - self.canvas.draw() + # + # self.canvas.draw() self.Refresh() diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 4d6f25767..9fb8062a8 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -18,6 +18,8 @@ # ============================================================================= +from collections import namedtuple + # noinspection PyPackageRequirements import wx @@ -28,12 +30,17 @@ from .lists import FitList, TargetList from .vector import VectorPicker +InputData = namedtuple('InputData', ('handle', 'unit', 'value')) +InputBox = namedtuple('InputBox', ('handle', 'unit', 'textBox', 'icon', 'label')) + + class GraphControlPanel(wx.Panel): def __init__(self, graphFrame, parent): super().__init__(parent) self.graphFrame = graphFrame - self._inputs = {} + self._mainInputBox = None + self._miscInputBoxes = [] self._storedRanges = {} self._storedConsts = {} @@ -148,12 +155,15 @@ class GraphControlPanel(wx.Panel): if storeInputs: self._storeCurrentValues() # Clean up old inputs - for children in self._inputs.values(): - for child in children: + for inputBox in (self._mainInputBox, *self._miscInputBoxes): + if inputBox is None: + continue + for child in (inputBox.textBox, inputBox.icon, inputBox.label): if child is not None: child.Destroy() self.inputsSizer.Clear() - self._inputs.clear() + self._mainInputBox = None + self._miscInputBoxes.clear() # Update vectors def handleVector(vectorDef, vector, handledHandles, mainInputHandle): @@ -200,8 +210,14 @@ class GraphControlPanel(wx.Panel): fieldSizer.Add(fieldIcon, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 3) fieldLabel = wx.StaticText(self, wx.ID_ANY, self._formatLabel(inputDef)) fieldSizer.Add(fieldLabel, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) - self._inputs[(inputDef.handle, inputDef.unit)] = (fieldTextBox, fieldIcon, fieldLabel) self.inputsSizer.Add(fieldSizer, 0, wx.EXPAND | wx.BOTTOM, 5) + # Store info about added input box + inputBox = InputBox(handle=inputDef.handle, unit=inputDef.unit, textBox=fieldTextBox, icon=fieldIcon, label=fieldLabel) + if mainInput: + self._mainInputBox = inputBox + else: + self._miscInputBoxes.append(inputBox) + addInputField(view.inputMap[selectedX.mainInput], handledHandles, mainInput=True) for inputDef in view.inputs: @@ -238,27 +254,35 @@ class GraphControlPanel(wx.Panel): def getValues(self): view = self.graphFrame.getView() - values = {} + main = None + misc = [] + processedHandles = set() + + def addMiscData(handle, unit, value): + if handle in processedHandles: + return + inputData = InputData(handle=handle, unit=unit, value=value) + misc.append(inputData) + + # Main input box + main = InputData(handle=self._mainInputBox.handle, unit=self._mainInputBox.unit, value=self._mainInputBox.textBox.GetValueRange()) + processedHandles.add(self._mainInputBox.handle) # Vectors srcVectorDef = view.srcVectorDef if srcVectorDef is not None: if not self.srcVector.IsDirectionOnly: - values[srcVectorDef.lengthHandle] = (self.srcVector.GetLength() * 100, srcVectorDef.lengthUnit) - values[srcVectorDef.angleHandle] = (self.srcVector.GetAngle(), srcVectorDef.angleUnit) + addMiscData(handle=srcVectorDef.lengthHandle, unit=srcVectorDef.lengthUnit, value=self.srcVector.GetLength() * 100) + addMiscData(handle=srcVectorDef.angleHandle, unit=srcVectorDef.angleUnit, value=self.srcVector.GetAngle()) tgtVectorDef = view.tgtVectorDef if tgtVectorDef is not None: if not self.tgtVector.IsDirectionOnly: - values[tgtVectorDef.lengthHandle] = (self.tgtVector.GetLength() * 100, tgtVectorDef.lengthUnit) - values[tgtVectorDef.angleHandle] = (self.tgtVector.GetAngle(), tgtVectorDef.angleUnit) - # Input boxes - for k, v in self._inputs.items(): - inputHandle, inputUnit = k - inputBox = v[0] - if isinstance(inputBox, RangeBox): - values[inputHandle] = (inputBox.GetValueRange(), inputUnit) - elif isinstance(inputBox, ConstantBox): - values[inputHandle] = (inputBox.GetValueFloat(), inputUnit) - return values + addMiscData(handle=tgtVectorDef.lengthHandle, unit=tgtVectorDef.lengthUnit, value=self.tgtVector.GetLength() * 100) + addMiscData(handle=tgtVectorDef.angleHandle, unit=tgtVectorDef.angleUnit, value=self.tgtVector.GetAngle()) + # Other input boxes + for inputBox in self._miscInputBoxes: + addMiscData(handle=inputBox.handle, unit=inputBox.unit, value=inputBox.textBox.GetValueFloat()) + + return main, misc @property def showY0(self): @@ -281,13 +305,11 @@ class GraphControlPanel(wx.Panel): return '{}, {}'.format(axisDef.label, axisDef.unit) def _storeCurrentValues(self): - for k, v in self.getValues().items(): - handle = k - value, unit = v - if isinstance(value, (tuple, list)): - self._storedRanges[(handle, unit)] = value - else: - self._storedConsts[(handle, unit)] = value + main, misc = self.getValues() + if main is not None: + self._storedRanges[(main.handle, main.unit)] = main.value + for input in misc: + self._storedConsts[(input.handle, input.unit)] = input.value def _clearStoredValues(self): self._storedConsts.clear() From 421146eb5464eeb30a30f42cb54047fea8c08ef8 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 27 Jun 2019 20:45:21 +0300 Subject: [PATCH 43/93] More work on interfaces between gui and eos graphs --- eos/graph/__init__.py | 58 -------------- eos/graph/base.py | 76 +++++++++++++++++++ eos/graph/fitCapAmountVsTime.py | 2 +- eos/graph/fitCapRegenVsCapPerc.py | 2 +- eos/graph/fitDistanceVsTime.py | 2 +- eos/graph/fitDmgVsTime.py | 2 +- eos/graph/fitDpsVsRange.py | 2 +- eos/graph/fitDpsVsTime.py | 2 +- eos/graph/fitShieldAmountVsTime.py | 2 +- eos/graph/fitShieldRegenVsShieldPerc.py | 2 +- eos/graph/fitSpeedVsTime.py | 2 +- ...itWarpTimeVsDistance.py => fitWarpTime.py} | 4 +- gui/builtinGraphs/__init__.py | 4 +- gui/builtinGraphs/base.py | 14 ++-- gui/builtinGraphs/fitDamageStats.py | 9 +-- ...itWarpTimeVsDistance.py => fitWarpTime.py} | 11 ++- 16 files changed, 104 insertions(+), 90 deletions(-) create mode 100644 eos/graph/base.py rename eos/graph/{fitWarpTimeVsDistance.py => fitWarpTime.py} (97%) rename gui/builtinGraphs/{fitWarpTimeVsDistance.py => fitWarpTime.py} (83%) diff --git a/eos/graph/__init__.py b/eos/graph/__init__.py index 55b6ec0e4..8ac11a9c5 100644 --- a/eos/graph/__init__.py +++ b/eos/graph/__init__.py @@ -16,61 +16,3 @@ # You should have received a copy of the GNU Lesser General Public License # along with eos. If not, see . # =============================================================================== - - -import math -from abc import ABCMeta, abstractmethod - - -class Graph(metaclass=ABCMeta): - - def __init__(self): - self._cache = {} - - @abstractmethod - def getPlotPoints(self, fit, extraData, xRange, xAmount): - raise NotImplementedError - - def getYForX(self, fit, extraData, x): - raise NotImplementedError - - def _xIter(self, fit, extraData, xRange, xAmount): - rangeLow, rangeHigh = self._limitXRange(xRange, fit, extraData) - # Amount is amount of ranges between points here, not amount of points - step = (rangeHigh - rangeLow) / xAmount - if step == 0: - yield xRange[0] - else: - current = rangeLow - # Take extra half step to make sure end of range is always included - # despite any possible float errors - while current <= (rangeHigh + step / 2): - yield current - current += step - - def _limitXRange(self, xRange, fit, extraData): - rangeLow, rangeHigh = sorted(xRange) - limitLow, limitHigh = self._getXLimits(fit, extraData) - rangeLow = max(limitLow, rangeLow) - rangeHigh = min(limitHigh, rangeHigh) - return rangeLow, rangeHigh - - def _getXLimits(self, fit, extraData): - return -math.inf, math.inf - - def clearCache(self, key=None): - if key is None: - self._cache.clear() - elif key in self._cache: - del self._cache[key] - - -class SmoothGraph(Graph, metaclass=ABCMeta): - - def getPlotPoints(self, fit, extraData, xRange, xAmount): - xs = [] - ys = [] - for x in self._xIter(fit, extraData, xRange, xAmount): - xs.append(x) - ys.append(self.getYForX(fit, extraData, x)) - return xs, ys diff --git a/eos/graph/base.py b/eos/graph/base.py new file mode 100644 index 000000000..fc2daab5d --- /dev/null +++ b/eos/graph/base.py @@ -0,0 +1,76 @@ +# =============================================================================== +# Copyright (C) 2010 Diego Duclos +# +# This file is part of eos. +# +# eos is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# eos 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 Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with eos. If not, see . +# =============================================================================== + + +import math +from abc import ABCMeta, abstractmethod + + +class Graph(metaclass=ABCMeta): + + def __init__(self): + self._cache = {} + + @abstractmethod + def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt): + raise NotImplementedError + + def getYForX(self, x, miscInputs, xSpec, ySpec, fit, tgt): + raise NotImplementedError + + def _xIter(self, fit, extraData, xRange, xAmount): + rangeLow, rangeHigh = self._limitXRange(xRange, fit, extraData) + # Amount is amount of ranges between points here, not amount of points + step = (rangeHigh - rangeLow) / xAmount + if step == 0: + yield xRange[0] + else: + current = rangeLow + # Take extra half step to make sure end of range is always included + # despite any possible float errors + while current <= (rangeHigh + step / 2): + yield current + current += step + + def _limitXRange(self, xRange, fit, extraData): + rangeLow, rangeHigh = sorted(xRange) + limitLow, limitHigh = self._getXLimits(fit, extraData) + rangeLow = max(limitLow, rangeLow) + rangeHigh = min(limitHigh, rangeHigh) + return rangeLow, rangeHigh + + def _getInputLimits(self, inputHandle, inputUnit, fit): + return -math.inf, math.inf + + def clearCache(self, key=None): + if key is None: + self._cache.clear() + elif key in self._cache: + del self._cache[key] + + +class SmoothGraph(Graph, metaclass=ABCMeta): + + def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt): + xs = [] + ys = [] + for x in self._xIter(fit, extraData, xRange, xAmount): + xs.append(x) + ys.append(self.getYForX(fit, extraData, x)) + return xs, ys diff --git a/eos/graph/fitCapAmountVsTime.py b/eos/graph/fitCapAmountVsTime.py index 16627617d..e0a9069a0 100644 --- a/eos/graph/fitCapAmountVsTime.py +++ b/eos/graph/fitCapAmountVsTime.py @@ -1,6 +1,6 @@ import math -from eos.graph import SmoothGraph +from .base import SmoothGraph class FitCapAmountVsTimeGraph(SmoothGraph): diff --git a/eos/graph/fitCapRegenVsCapPerc.py b/eos/graph/fitCapRegenVsCapPerc.py index ff686abea..d4a01f7c7 100644 --- a/eos/graph/fitCapRegenVsCapPerc.py +++ b/eos/graph/fitCapRegenVsCapPerc.py @@ -1,6 +1,6 @@ import math -from eos.graph import SmoothGraph +from .base import SmoothGraph class FitCapRegenVsCapPercGraph(SmoothGraph): diff --git a/eos/graph/fitDistanceVsTime.py b/eos/graph/fitDistanceVsTime.py index dcc5ce506..f1762b18a 100644 --- a/eos/graph/fitDistanceVsTime.py +++ b/eos/graph/fitDistanceVsTime.py @@ -1,6 +1,6 @@ import math -from eos.graph import SmoothGraph +from .base import SmoothGraph class FitDistanceVsTimeGraph(SmoothGraph): diff --git a/eos/graph/fitDmgVsTime.py b/eos/graph/fitDmgVsTime.py index 49912ec90..95db4b253 100644 --- a/eos/graph/fitDmgVsTime.py +++ b/eos/graph/fitDmgVsTime.py @@ -18,9 +18,9 @@ # =============================================================================== -from eos.graph import Graph from eos.utils.spoolSupport import SpoolType, SpoolOptions from gui.utils.numberFormatter import roundToPrec +from .base import Graph class FitDmgVsTimeGraph(Graph): diff --git a/eos/graph/fitDpsVsRange.py b/eos/graph/fitDpsVsRange.py index 31d0de723..741a7d2d8 100644 --- a/eos/graph/fitDpsVsRange.py +++ b/eos/graph/fitDpsVsRange.py @@ -24,8 +24,8 @@ from logbook import Logger import eos.config from eos.const import FittingHardpoint, FittingModuleState -from eos.graph import SmoothGraph from eos.utils.spoolSupport import SpoolType, SpoolOptions +from .base import SmoothGraph pyfalog = Logger(__name__) diff --git a/eos/graph/fitDpsVsTime.py b/eos/graph/fitDpsVsTime.py index b1c4ab58e..e93e8a731 100644 --- a/eos/graph/fitDpsVsTime.py +++ b/eos/graph/fitDpsVsTime.py @@ -20,9 +20,9 @@ from itertools import chain -from eos.graph import Graph from eos.utils.spoolSupport import SpoolType, SpoolOptions from gui.utils.numberFormatter import roundToPrec +from .base import Graph class FitDpsVsTimeGraph(Graph): diff --git a/eos/graph/fitShieldAmountVsTime.py b/eos/graph/fitShieldAmountVsTime.py index 387e80dee..443e1ea1c 100644 --- a/eos/graph/fitShieldAmountVsTime.py +++ b/eos/graph/fitShieldAmountVsTime.py @@ -1,7 +1,7 @@ import math from logbook import Logger -from eos.graph import SmoothGraph +from .base import SmoothGraph pyfalog = Logger(__name__) diff --git a/eos/graph/fitShieldRegenVsShieldPerc.py b/eos/graph/fitShieldRegenVsShieldPerc.py index 27902e428..3793d6727 100644 --- a/eos/graph/fitShieldRegenVsShieldPerc.py +++ b/eos/graph/fitShieldRegenVsShieldPerc.py @@ -1,6 +1,6 @@ import math -from eos.graph import SmoothGraph +from .base import SmoothGraph class FitShieldRegenVsShieldPercGraph(SmoothGraph): diff --git a/eos/graph/fitSpeedVsTime.py b/eos/graph/fitSpeedVsTime.py index ea19a310a..2aa021adb 100644 --- a/eos/graph/fitSpeedVsTime.py +++ b/eos/graph/fitSpeedVsTime.py @@ -1,6 +1,6 @@ import math -from eos.graph import SmoothGraph +from .base import SmoothGraph class FitSpeedVsTimeGraph(SmoothGraph): diff --git a/eos/graph/fitWarpTimeVsDistance.py b/eos/graph/fitWarpTime.py similarity index 97% rename from eos/graph/fitWarpTimeVsDistance.py rename to eos/graph/fitWarpTime.py index a16a63ec9..1ddca2ae4 100644 --- a/eos/graph/fitWarpTimeVsDistance.py +++ b/eos/graph/fitWarpTime.py @@ -1,13 +1,13 @@ import math from eos.const import FittingModuleState -from eos.graph import SmoothGraph +from .base import SmoothGraph AU_METERS = 149597870700 -class FitWarpTimeVsDistanceGraph(SmoothGraph): +class FitWarpTimeGraph(SmoothGraph): def __init__(self): super().__init__() diff --git a/gui/builtinGraphs/__init__.py b/gui/builtinGraphs/__init__.py index 5cf1cd831..ea8b49783 100644 --- a/gui/builtinGraphs/__init__.py +++ b/gui/builtinGraphs/__init__.py @@ -1,11 +1,11 @@ # noinspection PyUnresolvedReferences from gui.builtinGraphs import ( # noqa: E402,F401 - fitDamageStats, + # fitDamageStats, # fitDmgVsTime, # fitShieldRegenVsShieldPerc, # fitShieldAmountVsTime, # fitCapRegenVsCapPerc, # fitCapAmountVsTime, # fitMobilityVsTime, - fitWarpTimeVsDistance + fitWarpTime ) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index 935e65ce4..21eb0f1c1 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -18,7 +18,6 @@ # ============================================================================= -import re from abc import ABCMeta, abstractmethod from collections import OrderedDict, namedtuple @@ -32,7 +31,8 @@ class Graph(metaclass=ABCMeta): def register(cls): Graph.views.append(cls) - def __init__(self): + def __init__(self, eosGraph): + self._eosGraph = eosGraph self._cache = {} @property @@ -84,13 +84,11 @@ class Graph(metaclass=ABCMeta): def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt=None): try: - plotData = self._cache[fit.ID][ySpec] + plotData = self._cache[fit.ID][(ySpec, xSpec)] except KeyError: - extraData = {k: float(v) if v else None for k, v in extraData.items()} - graph = getattr(self, self.yDefs[yType].eosGraph, None) - plotData = graph.getPlotPoints(fit, extraData, xRange, xAmount) + plotData = self._eosGraph.getPlotPoints(mainInput, miscInputs, xSpec, ySpec, fit, tgt) fitCache = self._cache.setdefault(fit.ID, {}) - fitCache[yType] = plotData + fitCache[(ySpec, xSpec)] = plotData return plotData def clearCache(self, key=None): @@ -102,7 +100,7 @@ class Graph(metaclass=ABCMeta): getattr(self, yDef.eosGraph).clearCache(key=key) -YDef = namedtuple('YDef', ('handle', 'unit', 'label', 'eosGraph')) +YDef = namedtuple('YDef', ('handle', 'unit', 'label')) XDef = namedtuple('XDef', ('handle', 'unit', 'label', 'mainInput')) Input = namedtuple('Input', ('handle', 'unit', 'label', 'iconID', 'defaultValue', 'defaultRange', 'mainOnly')) VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle', 'angleUnit', 'label')) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index fcceff5b9..a836c007c 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -29,8 +29,7 @@ class FitDamageStatsGraph(Graph): name = 'Damage Stats' def __init__(self): - super().__init__() - self.eosGraph = EosGraph() + super().__init__(EosGraph()) @property def xDefs(self): @@ -45,9 +44,9 @@ class FitDamageStatsGraph(Graph): @property def yDefs(self): return [ - YDef(handle='dps', unit=None, label='DPS', eosGraph='eosGraph'), - YDef(handle='volley', unit=None, label='Volley', eosGraph='eosGraph'), - YDef(handle='damage', unit=None, label='Damage inflicted', eosGraph='eosGraph')] + YDef(handle='dps', unit=None, label='DPS'), + YDef(handle='volley', unit=None, label='Volley'), + YDef(handle='damage', unit=None, label='Damage inflicted')] @property def inputs(self): diff --git a/gui/builtinGraphs/fitWarpTimeVsDistance.py b/gui/builtinGraphs/fitWarpTime.py similarity index 83% rename from gui/builtinGraphs/fitWarpTimeVsDistance.py rename to gui/builtinGraphs/fitWarpTime.py index b54015dff..468c246b3 100644 --- a/gui/builtinGraphs/fitWarpTimeVsDistance.py +++ b/gui/builtinGraphs/fitWarpTime.py @@ -18,17 +18,16 @@ # ============================================================================= -from eos.graph.fitWarpTimeVsDistance import FitWarpTimeVsDistanceGraph as EosGraph +from eos.graph.fitWarpTime import FitWarpTimeGraph as EosGraph from .base import Graph, XDef, YDef, Input -class FitWarpTimeVsDistanceGraph(Graph): +class FitWarpTimeGraph(Graph): name = 'Warp Time' def __init__(self): - super().__init__() - self.eosGraph = EosGraph() + super().__init__(EosGraph()) @property def xDefs(self): @@ -38,7 +37,7 @@ class FitWarpTimeVsDistanceGraph(Graph): @property def yDefs(self): - return [YDef(handle='time', unit='s', label='Warp time', eosGraph='eosGraph')] + return [YDef(handle='time', unit='s', label='Warp time')] @property def inputs(self): @@ -47,4 +46,4 @@ class FitWarpTimeVsDistanceGraph(Graph): Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=10000, defaultRange=(150, 5000), mainOnly=False)] -FitWarpTimeVsDistanceGraph.register() +FitWarpTimeGraph.register() From 745914bf9e32d84fe7c70f932131bec15b2069aa Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 28 Jun 2019 09:24:06 +0300 Subject: [PATCH 44/93] Add parameter normalization function --- eos/graph/base.py | 19 +++++++++++++++++++ eos/graph/fitWarpTime.py | 17 +++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/eos/graph/base.py b/eos/graph/base.py index fc2daab5d..daa9d498a 100644 --- a/eos/graph/base.py +++ b/eos/graph/base.py @@ -31,6 +31,25 @@ class Graph(metaclass=ABCMeta): def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt): raise NotImplementedError + _normalizers = {} + + def _normalizeParams(self, mainInput, miscInputs, fit, tgt): + if mainInput.unit in self._normalizers: + normalizer = self._normalizers[mainInput.unit] + newMainInput = [mainInput.handle, tuple(normalizer(v) for v in mainInput.value)] + else: + newMainInput = [mainInput.handle, mainInput.value] + newMiscInputs = [] + for miscInput in miscInputs: + if miscInput.unit in self._normalizers: + newMiscInput = [miscInput.handle, self._normalizers[miscInput.unit](miscInput.value)] + else: + newMiscInput = [miscInput.handle, miscInput.value] + newMiscInputs.append(newMiscInput) + return newMainInput, newMiscInputs + + ### Old stuff + def getYForX(self, x, miscInputs, xSpec, ySpec, fit, tgt): raise NotImplementedError diff --git a/eos/graph/fitWarpTime.py b/eos/graph/fitWarpTime.py index 1ddca2ae4..c50f10588 100644 --- a/eos/graph/fitWarpTime.py +++ b/eos/graph/fitWarpTime.py @@ -1,18 +1,31 @@ import math from eos.const import FittingModuleState -from .base import SmoothGraph +from .base import Graph AU_METERS = 149597870700 -class FitWarpTimeGraph(SmoothGraph): +class FitWarpTimeGraph(Graph): def __init__(self): super().__init__() self.subwarpSpeed = None + def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt): + mainInput, miscInputs = self._normalizeParams(mainInput, miscInputs, fit, tgt) + + # limit all parameters to be within limits (time) + # pick getter according to x handle and y handle and run it + # un-render returned parameters if passed x is relative + return [], [] + + _normalizers = { + 'AU': lambda x: x * AU_METERS, + 'km': lambda x: x * 1000} + + def getYForX(self, fit, extraData, distance): if distance == 0: return 0 From 66ff4d827c1cf9b99a1ae3cb92b37b9fd178c31e Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 28 Jun 2019 10:08:53 +0300 Subject: [PATCH 45/93] Integrate graph frame with new APIs --- gui/graphFrame/frame.py | 175 ++++++++++++++++++++-------------------- gui/graphFrame/panel.py | 26 +++--- 2 files changed, 104 insertions(+), 97 deletions(-) diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index 2b4a4e395..9451c028d 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -18,6 +18,7 @@ # ============================================================================= +import itertools import os import traceback @@ -188,90 +189,92 @@ class GraphFrame(wx.Frame): if not self: pyfalog.warning('GraphFrame handled event, however GraphFrame no longer exists. Ignoring event') return - values = self.ctrlPanel.getValues() - # view = self.getView() - # self.subplot.clear() - # self.subplot.grid(True) - # legend = [] - # - # min_y = 0 if self.ctrlPanel.showY0 else None - # max_y = 0 if self.ctrlPanel.showY0 else None - # - # xRange = values['x'] - # extraInputs = {ih: values[ih] for ih in view.extraInputs} - # try: - # chosenY = [i for i in view.yDefs.keys()][self.ctrlPanel.selectedY or 0] - # except IndexError: - # chosenY = [i for i in view.yDefs.keys()][0] - # - # self.subplot.set(xlabel=view.xDef.axisLabel, ylabel=view.yDefs[chosenY].axisLabel) - # - # for fit in self.ctrlPanel.fitList.fits: - # try: - # xs, ys = view.getPlotPoints(fit, extraInputs, xRange, 100, chosenY) - # - # # Figure out min and max Y - # min_y_this = min(ys, default=None) - # if min_y is None: - # min_y = min_y_this - # elif min_y_this is not None: - # min_y = min(min_y, min_y_this) - # max_y_this = max(ys, default=None) - # if max_y is None: - # max_y = max_y_this - # elif max_y_this is not None: - # max_y = max(max_y, max_y_this) - # - # self.subplot.plot(xs, ys) - # legend.append('{} ({})'.format(fit.name, fit.ship.item.getShortName())) - # except Exception as ex: - # pyfalog.warning('Invalid values in "{0}"', fit.name) - # self.canvas.draw() - # return - # - # # Special case for when we do not show Y = 0 and have no fits - # if min_y is None: - # min_y = 0 - # if max_y is None: - # max_y = 0 - # # Extend range a little for some visual space - # y_range = max_y - min_y - # min_y -= y_range * 0.05 - # max_y += y_range * 0.05 - # if min_y == max_y: - # min_y -= min_y * 0.05 - # max_y += min_y * 0.05 - # if min_y == max_y: - # min_y -= 5 - # max_y += 5 - # self.subplot.set_ylim(bottom=min_y, top=max_y) - # - # legend2 = [] - # legend_colors = { - # 0: 'blue', - # 1: 'orange', - # 2: 'green', - # 3: 'red', - # 4: 'purple', - # 5: 'brown', - # 6: 'pink', - # 7: 'grey', - # } - # - # for i, i_name in enumerate(legend): - # try: - # selected_color = legend_colors[i] - # except: - # selected_color = None - # legend2.append(Patch(color=selected_color, label=i_name), ) - # - # if len(legend2) > 0: - # leg = self.subplot.legend(handles=legend2) - # for t in leg.get_texts(): - # t.set_fontsize('small') - # - # for l in leg.get_lines(): - # l.set_linewidth(1) - # - # self.canvas.draw() + + self.subplot.clear() + self.subplot.grid(True) + legend = [] + + min_y = 0 if self.ctrlPanel.showY0 else None + max_y = 0 if self.ctrlPanel.showY0 else None + + chosenX = self.ctrlPanel.xType + chosenY = self.ctrlPanel.yType + self.subplot.set(xlabel=self.ctrlPanel.formatLabel(chosenX), ylabel=self.ctrlPanel.formatLabel(chosenY)) + + mainInput, miscInputs = self.ctrlPanel.getValues() + view = self.getView() + fits = self.ctrlPanel.fits + if view.hasTargets: + targets = self.ctrlPanel.targets + iterList = tuple(itertools.combinations(fits, targets)) + else: + iterList = tuple((f, None) for f in fits) + for fit, target in iterList: + try: + xs, ys = view.getPlotPoints(mainInput, miscInputs, chosenX, chosenY, fit, target) + + # Figure out min and max Y + min_y_this = min(ys, default=None) + if min_y is None: + min_y = min_y_this + elif min_y_this is not None: + min_y = min(min_y, min_y_this) + max_y_this = max(ys, default=None) + if max_y is None: + max_y = max_y_this + elif max_y_this is not None: + max_y = max(max_y, max_y_this) + + self.subplot.plot(xs, ys) + legend.append('{} ({})'.format(fit.name, fit.ship.item.getShortName())) + except Exception as ex: + pyfalog.warning('Invalid values in "{0}"', fit.name) + self.canvas.draw() + return + + # Special case for when we do not show Y = 0 and have no fits + if min_y is None: + min_y = 0 + if max_y is None: + max_y = 0 + # Extend range a little for some visual space + y_range = max_y - min_y + min_y -= y_range * 0.05 + max_y += y_range * 0.05 + if min_y == max_y: + min_y -= min_y * 0.05 + max_y += min_y * 0.05 + if min_y == max_y: + min_y -= 5 + max_y += 5 + self.subplot.set_ylim(bottom=min_y, top=max_y) + + legend2 = [] + legend_colors = { + 0: 'blue', + 1: 'orange', + 2: 'green', + 3: 'red', + 4: 'purple', + 5: 'brown', + 6: 'pink', + 7: 'grey', + } + + for i, i_name in enumerate(legend): + try: + selected_color = legend_colors[i] + except: + selected_color = None + legend2.append(Patch(color=selected_color, label=i_name), ) + + if len(legend2) > 0: + leg = self.subplot.legend(handles=legend2) + for t in leg.get_texts(): + t.set_fontsize('small') + + for l in leg.get_lines(): + l.set_linewidth(1) + + self.canvas.draw() self.Refresh() diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 9fb8062a8..b08e19ce4 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -97,10 +97,8 @@ class GraphControlPanel(wx.Panel): self.fitList = FitList(graphFrame, self) self.fitList.SetMinSize((270, -1)) srcTgtSizer.Add(self.fitList, 1, wx.EXPAND | wx.ALL, 0) - self.targets = [] self.targetList = TargetList(graphFrame, self) self.targetList.SetMinSize((270, -1)) - self.targetList.update(self.targets) srcTgtSizer.Add(self.targetList, 1, wx.EXPAND | wx.LEFT, 10) mainSizer.Add(srcTgtSizer, 1, wx.EXPAND | wx.LEFT | wx.BOTTOM | wx.RIGHT, 10) @@ -116,11 +114,11 @@ class GraphControlPanel(wx.Panel): self.ySubSelection.Clear() self.xSubSelection.Clear() for yDef in view.yDefs: - self.ySubSelection.Append(self._formatLabel(yDef), (yDef.handle, yDef.unit)) + self.ySubSelection.Append(self.formatLabel(yDef), yDef) self.ySubSelection.SetSelection(0) self.ySubSelection.Enable(len(view.yDefs) > 1) for xDef in view.xDefs: - self.xSubSelection.Append(self._formatLabel(xDef), (xDef.handle, xDef.unit)) + self.xSubSelection.Append(self.formatLabel(xDef), xDef) self.xSubSelection.SetSelection(0) self.xSubSelection.Enable(len(view.xDefs) > 1) @@ -184,12 +182,11 @@ class GraphControlPanel(wx.Panel): vector.SetDirectionOnly(vectorDef.lengthHandle == mainInputHandle) view = self.graphFrame.getView() - selectedX = view.xDefMap[self.xType] handledHandles = set() if view.srcVectorDef is not None: - handleVector(view.srcVectorDef, self.srcVector, handledHandles, selectedX.mainInput[0]) + handleVector(view.srcVectorDef, self.srcVector, handledHandles, self.xType.mainInput[0]) if view.tgtVectorDef is not None: - handleVector(view.tgtVectorDef, self.tgtVector, handledHandles, selectedX.mainInput[0]) + handleVector(view.tgtVectorDef, self.tgtVector, handledHandles, self.xType.mainInput[0]) # Update inputs def addInputField(inputDef, handledHandles, mainInput=False): @@ -208,7 +205,7 @@ class GraphControlPanel(wx.Panel): fieldIcon = wx.StaticBitmap(self) fieldIcon.SetBitmap(icon) fieldSizer.Add(fieldIcon, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 3) - fieldLabel = wx.StaticText(self, wx.ID_ANY, self._formatLabel(inputDef)) + fieldLabel = wx.StaticText(self, wx.ID_ANY, self.formatLabel(inputDef)) fieldSizer.Add(fieldLabel, 0, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) self.inputsSizer.Add(fieldSizer, 0, wx.EXPAND | wx.BOTTOM, 5) # Store info about added input box @@ -219,7 +216,7 @@ class GraphControlPanel(wx.Panel): self._miscInputBoxes.append(inputBox) - addInputField(view.inputMap[selectedX.mainInput], handledHandles, mainInput=True) + addInputField(view.inputMap[self.xType.mainInput], handledHandles, mainInput=True) for inputDef in view.inputs: if inputDef.mainOnly: continue @@ -254,7 +251,6 @@ class GraphControlPanel(wx.Panel): def getValues(self): view = self.graphFrame.getView() - main = None misc = [] processedHandles = set() @@ -296,10 +292,18 @@ class GraphControlPanel(wx.Panel): def xType(self): return self.xSubSelection.GetClientData(self.xSubSelection.GetSelection()) + @property + def fits(self): + return self.fitList.fits + + @property + def targets(self): + return self.targetList.targetFits + def unbindExternalEvents(self): self.fitList.unbindExternalEvents() - def _formatLabel(self, axisDef): + def formatLabel(self, axisDef): if axisDef.unit is None: return axisDef.label return '{}, {}'.format(axisDef.label, axisDef.unit) From c2017f3cb971eea9f0146d6357cdd0a8fc45ebf6 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 28 Jun 2019 10:13:03 +0300 Subject: [PATCH 46/93] Re-enable DPS graph and make few fixes --- gui/builtinGraphs/__init__.py | 2 +- gui/builtinGraphs/base.py | 3 +-- gui/graphFrame/frame.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/gui/builtinGraphs/__init__.py b/gui/builtinGraphs/__init__.py index ea8b49783..6c7beeef6 100644 --- a/gui/builtinGraphs/__init__.py +++ b/gui/builtinGraphs/__init__.py @@ -1,6 +1,6 @@ # noinspection PyUnresolvedReferences from gui.builtinGraphs import ( # noqa: E402,F401 - # fitDamageStats, + fitDamageStats, # fitDmgVsTime, # fitShieldRegenVsShieldPerc, # fitShieldAmountVsTime, diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index 21eb0f1c1..11dce9d51 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -96,8 +96,7 @@ class Graph(metaclass=ABCMeta): self._cache.clear() elif key in self._cache: del self._cache[key] - for yDef in self.yDefs: - getattr(self, yDef.eosGraph).clearCache(key=key) + self._eosGraph.clearCache(key=key) YDef = namedtuple('YDef', ('handle', 'unit', 'label')) diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index 9451c028d..0a360eaf3 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -206,7 +206,7 @@ class GraphFrame(wx.Frame): fits = self.ctrlPanel.fits if view.hasTargets: targets = self.ctrlPanel.targets - iterList = tuple(itertools.combinations(fits, targets)) + iterList = tuple(itertools.product(fits, targets)) else: iterList = tuple((f, None) for f in fits) for fit, target in iterList: From d195ec7e68921732826825e8ce4065dc46104f0b Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 28 Jun 2019 15:44:50 +0300 Subject: [PATCH 47/93] Move all the logic from eos graph to gui graph for warp time Now backend graphs have to be aware of handles used in UI graphs, so why not --- eos/graph/base.py | 19 ----- eos/graph/fitWarpTime.py | 111 ---------------------------- gui/builtinGraphs/__init__.py | 2 +- gui/builtinGraphs/base.py | 123 +++++++++++++++++++++++++++---- gui/builtinGraphs/fitWarpTime.py | 115 ++++++++++++++++++++++++++++- 5 files changed, 222 insertions(+), 148 deletions(-) delete mode 100644 eos/graph/fitWarpTime.py diff --git a/eos/graph/base.py b/eos/graph/base.py index daa9d498a..ed1fd3445 100644 --- a/eos/graph/base.py +++ b/eos/graph/base.py @@ -27,26 +27,7 @@ class Graph(metaclass=ABCMeta): def __init__(self): self._cache = {} - @abstractmethod - def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt): - raise NotImplementedError - _normalizers = {} - - def _normalizeParams(self, mainInput, miscInputs, fit, tgt): - if mainInput.unit in self._normalizers: - normalizer = self._normalizers[mainInput.unit] - newMainInput = [mainInput.handle, tuple(normalizer(v) for v in mainInput.value)] - else: - newMainInput = [mainInput.handle, mainInput.value] - newMiscInputs = [] - for miscInput in miscInputs: - if miscInput.unit in self._normalizers: - newMiscInput = [miscInput.handle, self._normalizers[miscInput.unit](miscInput.value)] - else: - newMiscInput = [miscInput.handle, miscInput.value] - newMiscInputs.append(newMiscInput) - return newMainInput, newMiscInputs ### Old stuff diff --git a/eos/graph/fitWarpTime.py b/eos/graph/fitWarpTime.py deleted file mode 100644 index c50f10588..000000000 --- a/eos/graph/fitWarpTime.py +++ /dev/null @@ -1,111 +0,0 @@ -import math - -from eos.const import FittingModuleState -from .base import Graph - - -AU_METERS = 149597870700 - - -class FitWarpTimeGraph(Graph): - - def __init__(self): - super().__init__() - self.subwarpSpeed = None - - def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt): - mainInput, miscInputs = self._normalizeParams(mainInput, miscInputs, fit, tgt) - - # limit all parameters to be within limits (time) - # pick getter according to x handle and y handle and run it - # un-render returned parameters if passed x is relative - return [], [] - - _normalizers = { - 'AU': lambda x: x * AU_METERS, - 'km': lambda x: x * 1000} - - - def getYForX(self, fit, extraData, distance): - if distance == 0: - return 0 - if fit.ID not in self._cache: - self.__generateCache(fit) - maxWarpSpeed = fit.warpSpeed - subwarpSpeed = self._cache[fit.ID]['cleanSubwarpSpeed'] - time = calculate_time_in_warp(maxWarpSpeed, subwarpSpeed, distance * AU_METERS) - return time - - def _getXLimits(self, fit, extraData): - return 0, fit.maxWarpDistance - - def __generateCache(self, fit): - modStates = {} - for mod in fit.modules: - if mod.item is not None and mod.item.group.name in ('Propulsion Module', 'Mass Entanglers', 'Cloaking Device') and mod.state >= FittingModuleState.ACTIVE: - modStates[mod] = mod.state - mod.state = FittingModuleState.ONLINE - projFitStates = {} - for projFit in fit.projectedFits: - projectionInfo = projFit.getProjectionInfo(fit.ID) - if projectionInfo is not None and projectionInfo.active: - projFitStates[projectionInfo] = projectionInfo.active - projectionInfo.active = False - projModStates = {} - for mod in fit.projectedModules: - if not mod.isExclusiveSystemEffect and mod.state >= FittingModuleState.ACTIVE: - projModStates[mod] = mod.state - mod.state = FittingModuleState.ONLINE - projDroneStates = {} - for drone in fit.projectedDrones: - if drone.amountActive > 0: - projDroneStates[drone] = drone.amountActive - drone.amountActive = 0 - projFighterStates = {} - for fighter in fit.projectedFighters: - if fighter.active: - projFighterStates[fighter] = fighter.active - fighter.active = False - fit.calculateModifiedAttributes() - self._cache[fit.ID] = {'cleanSubwarpSpeed': fit.ship.getModifiedItemAttr('maxVelocity')} - for projInfo, state in projFitStates.items(): - projInfo.active = state - for mod, state in modStates.items(): - mod.state = state - for mod, state in projModStates.items(): - mod.state = state - for drone, amountActive in projDroneStates.items(): - drone.amountActive = amountActive - for fighter, state in projFighterStates.items(): - fighter.active = state - fit.calculateModifiedAttributes() - - -# Taken from https://wiki.eveuniversity.org/Warp_time_calculation#Implementation -# with minor modifications -# Warp speed in AU/s, subwarp speed in m/s, distance in m -def calculate_time_in_warp(max_warp_speed, max_subwarp_speed, warp_dist): - - k_accel = max_warp_speed - k_decel = min(max_warp_speed / 3, 2) - - warp_dropout_speed = max_subwarp_speed / 2 - max_ms_warp_speed = max_warp_speed * AU_METERS - - accel_dist = AU_METERS - decel_dist = max_ms_warp_speed / k_decel - - minimum_dist = accel_dist + decel_dist - - cruise_time = 0 - - if minimum_dist > warp_dist: - max_ms_warp_speed = warp_dist * k_accel * k_decel / (k_accel + k_decel) - else: - cruise_time = (warp_dist - minimum_dist) / max_ms_warp_speed - - accel_time = math.log(max_ms_warp_speed / k_accel) / k_accel - decel_time = math.log(max_ms_warp_speed / warp_dropout_speed) / k_decel - - total_time = cruise_time + accel_time + decel_time - return total_time diff --git a/gui/builtinGraphs/__init__.py b/gui/builtinGraphs/__init__.py index 6c7beeef6..ea8b49783 100644 --- a/gui/builtinGraphs/__init__.py +++ b/gui/builtinGraphs/__init__.py @@ -1,6 +1,6 @@ # noinspection PyUnresolvedReferences from gui.builtinGraphs import ( # noqa: E402,F401 - fitDamageStats, + # fitDamageStats, # fitDmgVsTime, # fitShieldRegenVsShieldPerc, # fitShieldAmountVsTime, diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index 11dce9d51..a1a88205b 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -22,18 +22,24 @@ from abc import ABCMeta, abstractmethod from collections import OrderedDict, namedtuple +YDef = namedtuple('YDef', ('handle', 'unit', 'label')) +XDef = namedtuple('XDef', ('handle', 'unit', 'label', 'mainInput')) +Input = namedtuple('Input', ('handle', 'unit', 'label', 'iconID', 'defaultValue', 'defaultRange', 'mainOnly')) +VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle', 'angleUnit', 'label')) + + class Graph(metaclass=ABCMeta): + # UI stuff views = [] - yTypes = None @classmethod def register(cls): Graph.views.append(cls) - def __init__(self, eosGraph): - self._eosGraph = eosGraph - self._cache = {} + def __init__(self): + self._plotCache = {} + self._calcCache = {} @property @abstractmethod @@ -84,25 +90,112 @@ class Graph(metaclass=ABCMeta): def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt=None): try: - plotData = self._cache[fit.ID][(ySpec, xSpec)] + plotData = self._plotCache[fit.ID][(ySpec, xSpec)] except KeyError: - plotData = self._eosGraph.getPlotPoints(mainInput, miscInputs, xSpec, ySpec, fit, tgt) - fitCache = self._cache.setdefault(fit.ID, {}) + plotData = self._calcPlotPoints(mainInput, miscInputs, xSpec, ySpec, fit, tgt) + fitCache = self._plotCache.setdefault(fit.ID, {}) fitCache[(ySpec, xSpec)] = plotData return plotData def clearCache(self, key=None): if key is None: - self._cache.clear() - elif key in self._cache: - del self._cache[key] - self._eosGraph.clearCache(key=key) + self._plotCache.clear() + self._calcCache.clear() + if key in self._plotCache: + del self._plotCache[key] + if key in self._calcCache: + del self._calcCache[key] + # Calculation stuff + def _calcPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt): + mainInput, miscInputs = self._normalizeParams(mainInput, miscInputs, fit, tgt) + mainInput, miscInputs = self._limitParams(mainInput, miscInputs, fit, tgt) + xs, ys = self._getPoints(mainInput, miscInputs, xSpec, ySpec, fit, tgt) + xs = self._denormalizeValues(xs, xSpec, fit, tgt) + ys = self._denormalizeValues(ys, ySpec, fit, tgt) + return xs, ys -YDef = namedtuple('YDef', ('handle', 'unit', 'label')) -XDef = namedtuple('XDef', ('handle', 'unit', 'label', 'mainInput')) -Input = namedtuple('Input', ('handle', 'unit', 'label', 'iconID', 'defaultValue', 'defaultRange', 'mainOnly')) -VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle', 'angleUnit', 'label')) + _normalizers = {} + + def _normalizeParams(self, mainInput, miscInputs, fit, tgt): + key = (mainInput.handle, mainInput.unit) + if key in self._normalizers: + normalizer = self._normalizers[key] + newMainInput = (mainInput.handle, tuple(normalizer(v, fit, tgt) for v in mainInput.value)) + else: + newMainInput = (mainInput.handle, mainInput.value) + newMiscInputs = [] + for miscInput in miscInputs: + key = (miscInput.handle, miscInput.unit) + if key in self._normalizers: + normalizer = self._normalizers[key] + newMiscInput = (miscInput.handle, normalizer(miscInput.value)) + else: + newMiscInput = (miscInput.handle, miscInput.value) + newMiscInputs.append(newMiscInput) + return newMainInput, newMiscInputs + + _limiters = {} + + def _limitParams(self, mainInput, miscInputs, fit, tgt): + + def limitToRange(val, limitRange): + if val is None: + return None + val = max(val, min(limitRange)) + val = min(val, max(limitRange)) + return val + + mainHandle, mainValue = mainInput + if mainHandle in self._limiters: + limiter = self._limiters[mainHandle] + newMainInput = (mainHandle, tuple(limitToRange(v, limiter(fit, tgt)) for v in mainValue)) + else: + newMainInput = mainInput + newMiscInputs = [] + for miscInput in miscInputs: + miscHandle, miscValue = miscInput + if miscHandle in self._limiters: + limiter = self._limiters[miscHandle] + newMiscInput = (miscHandle, limitToRange(miscValue, limiter(fit, tgt))) + newMiscInputs.append(newMiscInput) + else: + newMiscInputs.append(miscInput) + return newMainInput, newMiscInputs + + _getters = {} + + def _getPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt): + try: + getter = self._getters[(xSpec.handle, ySpec.handle)] + except KeyError: + return [], [] + else: + return getter(self, mainInput, miscInputs, fit, tgt) + + _denormalizers = {} + + def _denormalizeValues(self, values, axisSpec, fit, tgt): + key = (axisSpec.handle, axisSpec.unit) + if key in self._denormalizers: + denormalizer = self._denormalizers[key] + values = [denormalizer(v, fit, tgt) for v in values] + return values + + def _iterLinear(self, valRange, resolution=100): + rangeLow = min(valRange) + rangeHigh = max(valRange) + # Amount is amount of ranges between points here, not amount of points + step = (rangeHigh - rangeLow) / resolution + if step == 0: + yield rangeLow + else: + current = rangeLow + # Take extra half step to make sure end of range is always included + # despite any possible float errors + while current <= (rangeHigh + step / 2): + yield current + current += step # noinspection PyUnresolvedReferences diff --git a/gui/builtinGraphs/fitWarpTime.py b/gui/builtinGraphs/fitWarpTime.py index 468c246b3..d7337f00f 100644 --- a/gui/builtinGraphs/fitWarpTime.py +++ b/gui/builtinGraphs/fitWarpTime.py @@ -18,17 +18,23 @@ # ============================================================================= -from eos.graph.fitWarpTime import FitWarpTimeGraph as EosGraph +import math + +from eos.const import FittingModuleState from .base import Graph, XDef, YDef, Input +AU_METERS = 149597870700 + + class FitWarpTimeGraph(Graph): name = 'Warp Time' def __init__(self): - super().__init__(EosGraph()) + super().__init__() + # UI stuff @property def xDefs(self): return [ @@ -45,5 +51,110 @@ class FitWarpTimeGraph(Graph): Input(handle='distance', unit='AU', label='Distance', iconID=1391, defaultValue=50, defaultRange=(0, 50), mainOnly=False), Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=10000, defaultRange=(150, 5000), mainOnly=False)] + # Calculation stuff + _normalizers = { + ('distance', 'AU'): lambda v, fit, tgt: v * AU_METERS, + ('distance', 'km'): lambda v, fit, tgt: v * 1000} + + _limiters = { + 'distance': lambda fit, tgt: (0, fit.maxWarpDistance * AU_METERS)} + + _denormalizers = { + ('distance', 'AU'): lambda v, fit, tgt: v / AU_METERS, + ('distance', 'km'): lambda v, fit, tgt: v / 1000} + + def _distance2time(self, mainInput, miscInputs, fit, tgt): + xs = [] + ys = [] + subwarpSpeed = self.__getSubwarpSpeed(fit) + warpSpeed = fit.warpSpeed + for distance in self._iterLinear(mainInput[1]): + time = calculate_time_in_warp(subwarpSpeed, warpSpeed, distance) + xs.append(distance) + ys.append(time) + return xs, ys + + _getters = { + ('distance', 'time'): _distance2time} + + def __getSubwarpSpeed(self, fit): + try: + subwarpSpeed = self._calcCache[fit.ID]['cleanSubwarpSpeed'] + except KeyError: + modStates = {} + for mod in fit.modules: + if mod.item is not None and mod.item.group.name in ('Propulsion Module', 'Mass Entanglers', 'Cloaking Device') and mod.state >= FittingModuleState.ACTIVE: + modStates[mod] = mod.state + mod.state = FittingModuleState.ONLINE + projFitStates = {} + for projFit in fit.projectedFits: + projectionInfo = projFit.getProjectionInfo(fit.ID) + if projectionInfo is not None and projectionInfo.active: + projFitStates[projectionInfo] = projectionInfo.active + projectionInfo.active = False + projModStates = {} + for mod in fit.projectedModules: + if not mod.isExclusiveSystemEffect and mod.state >= FittingModuleState.ACTIVE: + projModStates[mod] = mod.state + mod.state = FittingModuleState.ONLINE + projDroneStates = {} + for drone in fit.projectedDrones: + if drone.amountActive > 0: + projDroneStates[drone] = drone.amountActive + drone.amountActive = 0 + projFighterStates = {} + for fighter in fit.projectedFighters: + if fighter.active: + projFighterStates[fighter] = fighter.active + fighter.active = False + fit.calculateModifiedAttributes() + subwarpSpeed = fit.ship.getModifiedItemAttr('maxVelocity') + self._calcCache[fit.ID] = {'cleanSubwarpSpeed': subwarpSpeed} + for projInfo, state in projFitStates.items(): + projInfo.active = state + for mod, state in modStates.items(): + mod.state = state + for mod, state in projModStates.items(): + mod.state = state + for drone, amountActive in projDroneStates.items(): + drone.amountActive = amountActive + for fighter, state in projFighterStates.items(): + fighter.active = state + fit.calculateModifiedAttributes() + return subwarpSpeed + + +# Taken from https://wiki.eveuniversity.org/Warp_time_calculation#Implementation +# with minor modifications +# Warp speed in AU/s, subwarp speed in m/s, distance in m +def calculate_time_in_warp(max_warp_speed, max_subwarp_speed, warp_dist): + + if warp_dist == 0: + return 0 + + k_accel = max_warp_speed + k_decel = min(max_warp_speed / 3, 2) + + warp_dropout_speed = max_subwarp_speed / 2 + max_ms_warp_speed = max_warp_speed * AU_METERS + + accel_dist = AU_METERS + decel_dist = max_ms_warp_speed / k_decel + + minimum_dist = accel_dist + decel_dist + + cruise_time = 0 + + if minimum_dist > warp_dist: + max_ms_warp_speed = warp_dist * k_accel * k_decel / (k_accel + k_decel) + else: + cruise_time = (warp_dist - minimum_dist) / max_ms_warp_speed + + accel_time = math.log(max_ms_warp_speed / k_accel) / k_accel + decel_time = math.log(max_ms_warp_speed / warp_dropout_speed) / k_decel + + total_time = cruise_time + accel_time + decel_time + return total_time + FitWarpTimeGraph.register() From 428cb5c8887db2eff9d5453695be66afe058c8e3 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 28 Jun 2019 18:31:39 +0300 Subject: [PATCH 48/93] Re-enable mobility graph --- eos/graph/fitSpeedVsTime.py | 14 --- gui/builtinGraphs/__init__.py | 2 +- gui/builtinGraphs/base.py | 4 +- gui/builtinGraphs/fitCapAmountVsTime.py | 4 +- gui/builtinGraphs/fitCapRegenVsCapPerc.py | 4 +- gui/builtinGraphs/fitDamageStats.py | 8 +- gui/builtinGraphs/fitDmgVsTime.py | 4 +- gui/builtinGraphs/fitMobility.py | 90 +++++++++++++++++++ gui/builtinGraphs/fitMobilityVsTime.py | 48 ---------- gui/builtinGraphs/fitShieldAmountVsTime.py | 4 +- .../fitShieldRegenVsShieldPerc.py | 4 +- gui/builtinGraphs/fitWarpTime.py | 7 +- gui/graphFrame/frame.py | 4 +- 13 files changed, 109 insertions(+), 88 deletions(-) delete mode 100644 eos/graph/fitSpeedVsTime.py create mode 100644 gui/builtinGraphs/fitMobility.py delete mode 100644 gui/builtinGraphs/fitMobilityVsTime.py diff --git a/eos/graph/fitSpeedVsTime.py b/eos/graph/fitSpeedVsTime.py deleted file mode 100644 index 2aa021adb..000000000 --- a/eos/graph/fitSpeedVsTime.py +++ /dev/null @@ -1,14 +0,0 @@ -import math - -from .base import SmoothGraph - - -class FitSpeedVsTimeGraph(SmoothGraph): - - def getYForX(self, fit, extraData, time): - maxSpeed = fit.ship.getModifiedItemAttr('maxVelocity') - mass = fit.ship.getModifiedItemAttr('mass') - agility = fit.ship.getModifiedItemAttr('agility') - # https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae - speed = maxSpeed * (1 - math.exp((-time * 1000000) / (agility * mass))) - return speed diff --git a/gui/builtinGraphs/__init__.py b/gui/builtinGraphs/__init__.py index ea8b49783..f8495c26e 100644 --- a/gui/builtinGraphs/__init__.py +++ b/gui/builtinGraphs/__init__.py @@ -6,6 +6,6 @@ from gui.builtinGraphs import ( # noqa: E402,F401 # fitShieldAmountVsTime, # fitCapRegenVsCapPerc, # fitCapAmountVsTime, - # fitMobilityVsTime, + fitMobility, fitWarpTime ) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index a1a88205b..c5e1262ab 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -28,14 +28,14 @@ Input = namedtuple('Input', ('handle', 'unit', 'label', 'iconID', 'defaultValue' VectorDef = namedtuple('VectorDef', ('lengthHandle', 'lengthUnit', 'angleHandle', 'angleUnit', 'label')) -class Graph(metaclass=ABCMeta): +class FitGraph(metaclass=ABCMeta): # UI stuff views = [] @classmethod def register(cls): - Graph.views.append(cls) + FitGraph.views.append(cls) def __init__(self): self._plotCache = {} diff --git a/gui/builtinGraphs/fitCapAmountVsTime.py b/gui/builtinGraphs/fitCapAmountVsTime.py index f29573175..3958687a7 100644 --- a/gui/builtinGraphs/fitCapAmountVsTime.py +++ b/gui/builtinGraphs/fitCapAmountVsTime.py @@ -21,10 +21,10 @@ from collections import OrderedDict from eos.graph.fitCapAmountVsTime import FitCapAmountVsTimeGraph as EosGraph -from .base import Graph, XDef, YDef +from .base import FitGraph, XDef, YDef -class FitCapAmountVsTimeGraph(Graph): +class FitCapAmountVsTimeGraph(FitGraph): name = 'Cap Amount vs Time' diff --git a/gui/builtinGraphs/fitCapRegenVsCapPerc.py b/gui/builtinGraphs/fitCapRegenVsCapPerc.py index ad4ae639e..a2607837c 100644 --- a/gui/builtinGraphs/fitCapRegenVsCapPerc.py +++ b/gui/builtinGraphs/fitCapRegenVsCapPerc.py @@ -21,10 +21,10 @@ from collections import OrderedDict from eos.graph.fitCapRegenVsCapPerc import FitCapRegenVsCapPercGraph as EosGraph -from .base import Graph, XDef, YDef +from .base import FitGraph, XDef, YDef -class FitCapRegenVsCapPercGraph(Graph): +class FitCapRegenVsCapPercGraph(FitGraph): name = 'Cap Regen vs Cap Amount' diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index a836c007c..45aa71ef9 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -20,17 +20,13 @@ import math -from eos.graph.fitDpsVsRange import FitDpsVsRangeGraph as EosGraph -from .base import Graph, XDef, YDef, Input, VectorDef +from .base import FitGraph, XDef, YDef, Input, VectorDef -class FitDamageStatsGraph(Graph): +class FitDamageStatsGraph(FitGraph): name = 'Damage Stats' - def __init__(self): - super().__init__(EosGraph()) - @property def xDefs(self): return [ diff --git a/gui/builtinGraphs/fitDmgVsTime.py b/gui/builtinGraphs/fitDmgVsTime.py index e9b1b14a6..cf2af9284 100644 --- a/gui/builtinGraphs/fitDmgVsTime.py +++ b/gui/builtinGraphs/fitDmgVsTime.py @@ -22,10 +22,10 @@ from collections import OrderedDict from eos.graph.fitDmgVsTime import FitDmgVsTimeGraph as EosGraphDmg from eos.graph.fitDpsVsTime import FitDpsVsTimeGraph as EosGraphDps -from .base import Graph, XDef, YDef +from .base import FitGraph, XDef, YDef -class FitDmgVsTimeGraph(Graph): +class FitDmgVsTimeGraph(FitGraph): name = 'Damage vs Time' diff --git a/gui/builtinGraphs/fitMobility.py b/gui/builtinGraphs/fitMobility.py new file mode 100644 index 000000000..4a084d94f --- /dev/null +++ b/gui/builtinGraphs/fitMobility.py @@ -0,0 +1,90 @@ +# ============================================================================= +# Copyright (C) 2010 Diego Duclos +# +# 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 . +# ============================================================================= + + +import math + +from .base import FitGraph, XDef, YDef, Input + + +class FitMobilityVsTimeGraph(FitGraph): + + name = 'Mobility' + + def __init__(self): + super().__init__() + + # UI stuff + @property + def xDefs(self): + return [XDef(handle='time', unit='s', label='Time', mainInput=('time', 's'))] + + @property + def yDefs(self): + return [ + YDef(handle='speed', unit='m/s', label='Speed'), + YDef(handle='distance', unit='km', label='Distance')] + + @property + def inputs(self): + return [Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=10, defaultRange=(0, 30), mainOnly=False)] + + @property + def xDef(self): + return XDef(inputDefault='0-80', inputLabel='Time (seconds)', inputIconID=1392, axisLabel='Time, s') + + # Calculation stuff + _denormalizers = { + ('distance', 'km'): lambda v, fit, tgt: v / 1000} + + def _time2speed(self, mainInput, miscInputs, fit, tgt): + xs = [] + ys = [] + maxSpeed = fit.ship.getModifiedItemAttr('maxVelocity') + mass = fit.ship.getModifiedItemAttr('mass') + agility = fit.ship.getModifiedItemAttr('agility') + for time in self._iterLinear(mainInput[1]): + # https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae + speed = maxSpeed * (1 - math.exp((-time * 1000000) / (agility * mass))) + xs.append(time) + ys.append(speed) + return xs, ys + + def _time2distance(self, mainInput, miscInputs, fit, tgt): + xs = [] + ys = [] + maxSpeed = fit.ship.getModifiedItemAttr('maxVelocity') + mass = fit.ship.getModifiedItemAttr('mass') + agility = fit.ship.getModifiedItemAttr('agility') + for time in self._iterLinear(mainInput[1]): + # Definite integral of: + # https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae + distance_t = maxSpeed * time + (maxSpeed * agility * mass * math.exp((-time * 1000000) / (agility * mass)) / 1000000) + distance_0 = maxSpeed * 0 + (maxSpeed * agility * mass * math.exp((-0 * 1000000) / (agility * mass)) / 1000000) + distance = distance_t - distance_0 + xs.append(time) + ys.append(distance) + return xs, ys + + _getters = { + ('time', 'speed'): _time2speed, + ('time', 'distance'): _time2distance} + + +FitMobilityVsTimeGraph.register() diff --git a/gui/builtinGraphs/fitMobilityVsTime.py b/gui/builtinGraphs/fitMobilityVsTime.py deleted file mode 100644 index 6f002bce9..000000000 --- a/gui/builtinGraphs/fitMobilityVsTime.py +++ /dev/null @@ -1,48 +0,0 @@ -# ============================================================================= -# Copyright (C) 2010 Diego Duclos -# -# 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 . -# ============================================================================= - - -from collections import OrderedDict - -from eos.graph.fitDistanceVsTime import FitDistanceVsTimeGraph as EosGraphDistance -from eos.graph.fitSpeedVsTime import FitSpeedVsTimeGraph as EosGraphSpeed -from .base import Graph, XDef, YDef - - -class FitMobilityVsTimeGraph(Graph): - - name = 'Mobility vs Time' - - def __init__(self): - super().__init__() - self.eosGraphSpeed = EosGraphSpeed() - self.eosGraphDistance = EosGraphDistance() - - @property - def xDef(self): - return XDef(inputDefault='0-80', inputLabel='Time (seconds)', inputIconID=1392, axisLabel='Time, s') - - @property - def yDefs(self): - return OrderedDict([ - ('speed', YDef(switchLabel='Speed', axisLabel='Speed, m/s', eosGraph='eosGraphSpeed')), - ('distance', YDef(switchLabel='Distance', axisLabel='Distance, m', eosGraph='eosGraphDistance'))]) - - -FitMobilityVsTimeGraph.register() diff --git a/gui/builtinGraphs/fitShieldAmountVsTime.py b/gui/builtinGraphs/fitShieldAmountVsTime.py index ae44178f6..1cb16aef6 100644 --- a/gui/builtinGraphs/fitShieldAmountVsTime.py +++ b/gui/builtinGraphs/fitShieldAmountVsTime.py @@ -22,10 +22,10 @@ from collections import OrderedDict import gui.mainFrame from eos.graph.fitShieldAmountVsTime import FitShieldAmountVsTimeGraph as EosGraph -from .base import Graph, XDef, YDef +from .base import FitGraph, XDef, YDef -class FitShieldAmountVsTimeGraph(Graph): +class FitShieldAmountVsTimeGraph(FitGraph): name = 'Shield Amount vs Time' diff --git a/gui/builtinGraphs/fitShieldRegenVsShieldPerc.py b/gui/builtinGraphs/fitShieldRegenVsShieldPerc.py index e86ce5aef..7dc8212cb 100644 --- a/gui/builtinGraphs/fitShieldRegenVsShieldPerc.py +++ b/gui/builtinGraphs/fitShieldRegenVsShieldPerc.py @@ -22,10 +22,10 @@ from collections import OrderedDict import gui.mainFrame from eos.graph.fitShieldRegenVsShieldPerc import FitShieldRegenVsShieldPercGraph as EosGraph -from .base import Graph, XDef, YDef +from .base import FitGraph, XDef, YDef -class FitShieldRegenVsShieldPercGraph(Graph): +class FitShieldRegenVsShieldPercGraph(FitGraph): name = 'Shield Regen vs Shield Amount' diff --git a/gui/builtinGraphs/fitWarpTime.py b/gui/builtinGraphs/fitWarpTime.py index d7337f00f..6d2eafcce 100644 --- a/gui/builtinGraphs/fitWarpTime.py +++ b/gui/builtinGraphs/fitWarpTime.py @@ -21,19 +21,16 @@ import math from eos.const import FittingModuleState -from .base import Graph, XDef, YDef, Input +from .base import FitGraph, XDef, YDef, Input AU_METERS = 149597870700 -class FitWarpTimeGraph(Graph): +class FitWarpTimeGraph(FitGraph): name = 'Warp Time' - def __init__(self): - super().__init__() - # UI stuff @property def xDefs(self): diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index 0a360eaf3..350f5870b 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -30,7 +30,7 @@ import gui.display import gui.globalEvents as GE import gui.mainFrame from gui.bitmap_loader import BitmapLoader -from gui.builtinGraphs.base import Graph +from gui.builtinGraphs.base import FitGraph from .panel import GraphControlPanel @@ -111,7 +111,7 @@ class GraphFrame(wx.Frame): self.SetSizer(mainSizer) # Setup - graph selector - for view in Graph.views: + for view in FitGraph.views: self.graphSelection.Append(view.name, view()) self.graphSelection.SetSelection(0) self.ctrlPanel.updateControls(layout=False) From 60933a309f162933a53a9df02b979076aa5506bf Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 28 Jun 2019 18:56:57 +0300 Subject: [PATCH 49/93] Re-enable dps graph again and add some info about how to process inputs and outputs Real calculation hasn't been transferred yet --- eos/graph/fitDistanceVsTime.py | 17 ----------------- gui/builtinGraphs/__init__.py | 2 +- gui/builtinGraphs/fitDamageStats.py | 16 ++++++++++++++-- 3 files changed, 15 insertions(+), 20 deletions(-) delete mode 100644 eos/graph/fitDistanceVsTime.py diff --git a/eos/graph/fitDistanceVsTime.py b/eos/graph/fitDistanceVsTime.py deleted file mode 100644 index f1762b18a..000000000 --- a/eos/graph/fitDistanceVsTime.py +++ /dev/null @@ -1,17 +0,0 @@ -import math - -from .base import SmoothGraph - - -class FitDistanceVsTimeGraph(SmoothGraph): - - def getYForX(self, fit, extraData, time): - maxSpeed = fit.ship.getModifiedItemAttr('maxVelocity') - mass = fit.ship.getModifiedItemAttr('mass') - agility = fit.ship.getModifiedItemAttr('agility') - # Definite integral of: - # https://wiki.eveuniversity.org/Acceleration#Mathematics_and_formulae - distance_t = maxSpeed * time + (maxSpeed * agility * mass * math.exp((-time * 1000000) / (agility * mass)) / 1000000) - distance_0 = maxSpeed * 0 + (maxSpeed * agility * mass * math.exp((-0 * 1000000) / (agility * mass)) / 1000000) - distance = distance_t - distance_0 - return distance diff --git a/gui/builtinGraphs/__init__.py b/gui/builtinGraphs/__init__.py index f8495c26e..5e3cf7838 100644 --- a/gui/builtinGraphs/__init__.py +++ b/gui/builtinGraphs/__init__.py @@ -1,6 +1,6 @@ # noinspection PyUnresolvedReferences from gui.builtinGraphs import ( # noqa: E402,F401 - # fitDamageStats, + fitDamageStats, # fitDmgVsTime, # fitShieldRegenVsShieldPerc, # fitShieldAmountVsTime, diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 45aa71ef9..00385df83 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -18,8 +18,6 @@ # ============================================================================= -import math - from .base import FitGraph, XDef, YDef, Input, VectorDef @@ -27,6 +25,7 @@ class FitDamageStatsGraph(FitGraph): name = 'Damage Stats' + # UI stuff @property def xDefs(self): return [ @@ -64,5 +63,18 @@ class FitDamageStatsGraph(FitGraph): def hasTargets(self): return True + # Calculation stuff + _normalizers = { + ('atkSpeed', '%'): lambda v, fit, tgt: v * fit.ship.getModifiedItemAttr('maxVelocity', 0), + ('tgtSpeed', '%'): lambda v, fit, tgt: v * tgt.ship.getModifiedItemAttr('maxVelocity', 0), + ('tgtSigRad', '%'): lambda v, fit, tgt: v * fit.ship.getModifiedItemAttr('signatureRadius', 0)} + + _limiters = { + 'time': lambda fit, tgt: (0, 2500)} + + _denormalizers = { + ('tgtSpeed', '%'): lambda v, fit, tgt: v / tgt.ship.getModifiedItemAttr('maxVelocity', 0), + ('tgtSigRad', '%'): lambda v, fit, tgt: v / fit.ship.getModifiedItemAttr('signatureRadius', 0)} + FitDamageStatsGraph.register() From 75ce6ffbcf8df2af64da9c7ca6d5b98655fc68bb Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 28 Jun 2019 19:07:16 +0300 Subject: [PATCH 50/93] Add stubs for getters --- gui/builtinGraphs/fitDamageStats.py | 52 +++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 00385df83..d92684703 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -65,6 +65,7 @@ class FitDamageStatsGraph(FitGraph): # Calculation stuff _normalizers = { + ('distance', 'km'): lambda v, fit, tgt: v * 1000, ('atkSpeed', '%'): lambda v, fit, tgt: v * fit.ship.getModifiedItemAttr('maxVelocity', 0), ('tgtSpeed', '%'): lambda v, fit, tgt: v * tgt.ship.getModifiedItemAttr('maxVelocity', 0), ('tgtSigRad', '%'): lambda v, fit, tgt: v * fit.ship.getModifiedItemAttr('signatureRadius', 0)} @@ -73,8 +74,59 @@ class FitDamageStatsGraph(FitGraph): 'time': lambda fit, tgt: (0, 2500)} _denormalizers = { + ('distance', 'km'): lambda v, fit, tgt: v / 1000, ('tgtSpeed', '%'): lambda v, fit, tgt: v / tgt.ship.getModifiedItemAttr('maxVelocity', 0), ('tgtSigRad', '%'): lambda v, fit, tgt: v / fit.ship.getModifiedItemAttr('signatureRadius', 0)} + def _distance2dps(self, mainInput, miscInputs, fit, tgt): + return [], [] + + def _distance2volley(self, mainInput, miscInputs, fit, tgt): + return [], [] + + def _distance2damage(self, mainInput, miscInputs, fit, tgt): + return [], [] + + def _time2dps(self, mainInput, miscInputs, fit, tgt): + return [], [] + + def _time2volley(self, mainInput, miscInputs, fit, tgt): + return [], [] + + def _time2damage(self, mainInput, miscInputs, fit, tgt): + return [], [] + + def _tgtSpeed2dps(self, mainInput, miscInputs, fit, tgt): + return [], [] + + def _tgtSpeed2volley(self, mainInput, miscInputs, fit, tgt): + return [], [] + + def _tgtSpeed2damage(self, mainInput, miscInputs, fit, tgt): + return [], [] + + def _tgtSigRad2dps(self, mainInput, miscInputs, fit, tgt): + return [], [] + + def _tgtSigRad2volley(self, mainInput, miscInputs, fit, tgt): + return [], [] + + def _tgtSigRad2damage(self, mainInput, miscInputs, fit, tgt): + return [], [] + + _getters = { + ('distance', 'dps'): _distance2dps, + ('distance', 'volley'): _distance2volley, + ('distance', 'damage'): _distance2damage, + ('time', 'dps'): _time2dps, + ('time', 'volley'): _time2volley, + ('time', 'damage'): _time2damage, + ('tgtSpeed', 'dps'): _tgtSpeed2dps, + ('tgtSpeed', 'volley'): _tgtSpeed2volley, + ('tgtSpeed', 'damage'): _tgtSpeed2damage, + ('tgtSigRad', 'dps'): _tgtSigRad2dps, + ('tgtSigRad', 'volley'): _tgtSigRad2volley, + ('tgtSigRad', 'damage'): _tgtSigRad2damage} + FitDamageStatsGraph.register() From d448116e91e4a94129ecee44bf3d0ee626423c4d Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 28 Jun 2019 19:42:49 +0300 Subject: [PATCH 51/93] Transfer cap amount vs time graph to new infrastructure --- eos/graph/fitCapAmountVsTime.py | 15 ------- gui/builtinGraphs/__init__.py | 2 +- gui/builtinGraphs/fitCap.py | 60 +++++++++++++++++++++++++ gui/builtinGraphs/fitCapAmountVsTime.py | 44 ------------------ 4 files changed, 61 insertions(+), 60 deletions(-) delete mode 100644 eos/graph/fitCapAmountVsTime.py create mode 100644 gui/builtinGraphs/fitCap.py delete mode 100644 gui/builtinGraphs/fitCapAmountVsTime.py diff --git a/eos/graph/fitCapAmountVsTime.py b/eos/graph/fitCapAmountVsTime.py deleted file mode 100644 index e0a9069a0..000000000 --- a/eos/graph/fitCapAmountVsTime.py +++ /dev/null @@ -1,15 +0,0 @@ -import math - -from .base import SmoothGraph - - -class FitCapAmountVsTimeGraph(SmoothGraph): - - def getYForX(self, fit, extraData, time): - if time < 0: - return 0 - maxCap = fit.ship.getModifiedItemAttr('capacitorCapacity') - regenTime = fit.ship.getModifiedItemAttr('rechargeRate') / 1000 - # https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate - cap = maxCap * (1 + math.exp(5 * -time / regenTime) * -1) ** 2 - return cap diff --git a/gui/builtinGraphs/__init__.py b/gui/builtinGraphs/__init__.py index 5e3cf7838..b61b12335 100644 --- a/gui/builtinGraphs/__init__.py +++ b/gui/builtinGraphs/__init__.py @@ -4,7 +4,7 @@ from gui.builtinGraphs import ( # noqa: E402,F401 # fitDmgVsTime, # fitShieldRegenVsShieldPerc, # fitShieldAmountVsTime, - # fitCapRegenVsCapPerc, + fitCap, # fitCapAmountVsTime, fitMobility, fitWarpTime diff --git a/gui/builtinGraphs/fitCap.py b/gui/builtinGraphs/fitCap.py new file mode 100644 index 000000000..33ef59aa9 --- /dev/null +++ b/gui/builtinGraphs/fitCap.py @@ -0,0 +1,60 @@ +# ============================================================================= +# Copyright (C) 2010 Diego Duclos +# +# 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 . +# ============================================================================= + + +import math + +from .base import FitGraph, XDef, YDef, Input + + +class FitCapAmountVsTimeGraph(FitGraph): + + name = 'Capacitor' + + # UI stuff + @property + def xDefs(self): + return [XDef(handle='time', unit='s', label='Time', mainInput=('time', 's'))] + + @property + def yDefs(self): + return [YDef(handle='capAmount', unit='GJ', label='Cap amount')] + + @property + def inputs(self): + return [Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=120, defaultRange=(0, 300), mainOnly=False)] + + # Calculation stuff + def _time2capAmount(self, mainInput, miscInputs, fit, tgt): + xs = [] + ys = [] + maxCap = fit.ship.getModifiedItemAttr('capacitorCapacity') + regenTime = fit.ship.getModifiedItemAttr('rechargeRate') / 1000 + for time in self._iterLinear(mainInput[1]): + # https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate + cap = maxCap * (1 + math.exp(5 * -time / regenTime) * -1) ** 2 + xs.append(time) + ys.append(cap) + return xs, ys + + _getters = { + ('time', 'capAmount'): _time2capAmount} + + +FitCapAmountVsTimeGraph.register() diff --git a/gui/builtinGraphs/fitCapAmountVsTime.py b/gui/builtinGraphs/fitCapAmountVsTime.py deleted file mode 100644 index 3958687a7..000000000 --- a/gui/builtinGraphs/fitCapAmountVsTime.py +++ /dev/null @@ -1,44 +0,0 @@ -# ============================================================================= -# Copyright (C) 2010 Diego Duclos -# -# 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 . -# ============================================================================= - - -from collections import OrderedDict - -from eos.graph.fitCapAmountVsTime import FitCapAmountVsTimeGraph as EosGraph -from .base import FitGraph, XDef, YDef - - -class FitCapAmountVsTimeGraph(FitGraph): - - name = 'Cap Amount vs Time' - - def __init__(self): - super().__init__() - self.eosGraph = EosGraph() - - @property - def xDef(self): - return XDef(inputDefault='0-300', inputLabel='Time (seconds)', inputIconID=1392, axisLabel='Time, s') - - @property - def yDefs(self): - return OrderedDict([('capAmount', YDef(switchLabel='Cap amount', axisLabel='Cap amount, GJ', eosGraph='eosGraph'))]) - - -FitCapAmountVsTimeGraph.register() From 988688939b6e87f5de3b296e803cc34f084e3c89 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 28 Jun 2019 20:17:23 +0300 Subject: [PATCH 52/93] Merge cap regen graph into already existing cap graph --- eos/graph/fitCapRegenVsCapPerc.py | 14 ---- gui/builtinGraphs/fitCap.py | 78 ++++++++++++++++++++--- gui/builtinGraphs/fitCapRegenVsCapPerc.py | 44 ------------- gui/builtinGraphs/fitDamageStats.py | 10 +-- gui/builtinGraphs/fitWarpTime.py | 2 +- 5 files changed, 75 insertions(+), 73 deletions(-) delete mode 100644 eos/graph/fitCapRegenVsCapPerc.py delete mode 100644 gui/builtinGraphs/fitCapRegenVsCapPerc.py diff --git a/eos/graph/fitCapRegenVsCapPerc.py b/eos/graph/fitCapRegenVsCapPerc.py deleted file mode 100644 index d4a01f7c7..000000000 --- a/eos/graph/fitCapRegenVsCapPerc.py +++ /dev/null @@ -1,14 +0,0 @@ -import math - -from .base import SmoothGraph - - -class FitCapRegenVsCapPercGraph(SmoothGraph): - - def getYForX(self, fit, extraData, perc): - maxCap = fit.ship.getModifiedItemAttr('capacitorCapacity') - regenTime = fit.ship.getModifiedItemAttr('rechargeRate') / 1000 - currentCap = maxCap * perc / 100 - # https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate - regen = 10 * maxCap / regenTime * (math.sqrt(currentCap / maxCap) - currentCap / maxCap) - return regen diff --git a/gui/builtinGraphs/fitCap.py b/gui/builtinGraphs/fitCap.py index 33ef59aa9..7e9dc8c58 100644 --- a/gui/builtinGraphs/fitCap.py +++ b/gui/builtinGraphs/fitCap.py @@ -30,31 +30,91 @@ class FitCapAmountVsTimeGraph(FitGraph): # UI stuff @property def xDefs(self): - return [XDef(handle='time', unit='s', label='Time', mainInput=('time', 's'))] + return [ + XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')), + XDef(handle='capAmount', unit='GJ', label='Cap amount', mainInput=('capAmount', '%')), + XDef(handle='capAmount', unit='%', label='Cap amount', mainInput=('capAmount', '%'))] @property def yDefs(self): - return [YDef(handle='capAmount', unit='GJ', label='Cap amount')] + return [ + YDef(handle='capAmount', unit='GJ', label='Cap amount'), + YDef(handle='capRegen', unit='GJ/s', label='Cap regen')] @property def inputs(self): - return [Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=120, defaultRange=(0, 300), mainOnly=False)] + return [ + Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=120, defaultRange=(0, 300), mainOnly=False), + Input(handle='capAmount', unit='%', label='Cap amount', iconID=1668, defaultValue=25, defaultRange=(0, 100), mainOnly=True)] # Calculation stuff + _normalizers = { + ('capAmount', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('capacitorCapacity')} + + _limiters = { + 'capAmount': lambda fit, tgt: (0, fit.ship.getModifiedItemAttr('capacitorCapacity'))} + + _denormalizers = { + ('capAmount', '%'): lambda v, fit, tgt: v * 100 / fit.ship.getModifiedItemAttr('capacitorCapacity')} + def _time2capAmount(self, mainInput, miscInputs, fit, tgt): xs = [] ys = [] - maxCap = fit.ship.getModifiedItemAttr('capacitorCapacity') - regenTime = fit.ship.getModifiedItemAttr('rechargeRate') / 1000 + maxCapAmount = fit.ship.getModifiedItemAttr('capacitorCapacity') + capRegenTime = fit.ship.getModifiedItemAttr('rechargeRate') / 1000 for time in self._iterLinear(mainInput[1]): - # https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate - cap = maxCap * (1 + math.exp(5 * -time / regenTime) * -1) ** 2 + currentCapAmount = calculateCapAmount(maxCapAmount=maxCapAmount, capRegenTime=capRegenTime, time=time) xs.append(time) - ys.append(cap) + ys.append(currentCapAmount) + return xs, ys + + def _time2capRegen(self, mainInput, miscInputs, fit, tgt): + xs = [] + ys = [] + maxCapAmount = fit.ship.getModifiedItemAttr('capacitorCapacity') + capRegenTime = fit.ship.getModifiedItemAttr('rechargeRate') / 1000 + for time in self._iterLinear(mainInput[1]): + currentCapAmount = calculateCapAmount(maxCapAmount=maxCapAmount, capRegenTime=capRegenTime, time=time) + currentRegen = calculateCapRegen(maxCapAmount=maxCapAmount, capRegenTime=capRegenTime, currentCapAmount=currentCapAmount) + xs.append(time) + ys.append(currentRegen) + return xs, ys + + def _capAmount2capAmount(self, mainInput, miscInputs, fit, tgt): + # Useless, but valid combination of x and y + xs = [] + ys = [] + for currentCapAmount in self._iterLinear(mainInput[1]): + xs.append(currentCapAmount) + ys.append(currentCapAmount) + return xs, ys + + def _capAmount2capRegen(self, mainInput, miscInputs, fit, tgt): + xs = [] + ys = [] + maxCapAmount = fit.ship.getModifiedItemAttr('capacitorCapacity') + capRegenTime = fit.ship.getModifiedItemAttr('rechargeRate') / 1000 + for currentCapAmount in self._iterLinear(mainInput[1]): + currentRegen = calculateCapRegen(maxCapAmount=maxCapAmount, capRegenTime=capRegenTime, currentCapAmount=currentCapAmount) + xs.append(currentCapAmount) + ys.append(currentRegen) return xs, ys _getters = { - ('time', 'capAmount'): _time2capAmount} + ('time', 'capAmount'): _time2capAmount, + ('time', 'capRegen'): _time2capRegen, + ('capAmount', 'capAmount'): _capAmount2capAmount, + ('capAmount', 'capRegen'): _capAmount2capRegen} + + +def calculateCapAmount(maxCapAmount, capRegenTime, time): + # https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate + return maxCapAmount * (1 + math.exp(5 * -time / capRegenTime) * -1) ** 2 + + +def calculateCapRegen(maxCapAmount, capRegenTime, currentCapAmount): + # https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate + return 10 * maxCapAmount / capRegenTime * (math.sqrt(currentCapAmount / maxCapAmount) - currentCapAmount / maxCapAmount) FitCapAmountVsTimeGraph.register() diff --git a/gui/builtinGraphs/fitCapRegenVsCapPerc.py b/gui/builtinGraphs/fitCapRegenVsCapPerc.py deleted file mode 100644 index a2607837c..000000000 --- a/gui/builtinGraphs/fitCapRegenVsCapPerc.py +++ /dev/null @@ -1,44 +0,0 @@ -# ============================================================================= -# Copyright (C) 2010 Diego Duclos -# -# 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 . -# ============================================================================= - - -from collections import OrderedDict - -from eos.graph.fitCapRegenVsCapPerc import FitCapRegenVsCapPercGraph as EosGraph -from .base import FitGraph, XDef, YDef - - -class FitCapRegenVsCapPercGraph(FitGraph): - - name = 'Cap Regen vs Cap Amount' - - def __init__(self): - super().__init__() - self.eosGraph = EosGraph() - - @property - def xDef(self): - return XDef(inputDefault='0-100', inputLabel='Cap amount (percent)', inputIconID=1668, axisLabel='Cap amount, %') - - @property - def yDefs(self): - return OrderedDict([('capRegen', YDef(switchLabel='Cap regen', axisLabel='Cap regen, GJ/s', eosGraph='eosGraph'))]) - - -FitCapRegenVsCapPercGraph.register() diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index d92684703..60e1f17d5 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -66,17 +66,17 @@ class FitDamageStatsGraph(FitGraph): # Calculation stuff _normalizers = { ('distance', 'km'): lambda v, fit, tgt: v * 1000, - ('atkSpeed', '%'): lambda v, fit, tgt: v * fit.ship.getModifiedItemAttr('maxVelocity', 0), - ('tgtSpeed', '%'): lambda v, fit, tgt: v * tgt.ship.getModifiedItemAttr('maxVelocity', 0), - ('tgtSigRad', '%'): lambda v, fit, tgt: v * fit.ship.getModifiedItemAttr('signatureRadius', 0)} + ('atkSpeed', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('maxVelocity'), + ('tgtSpeed', '%'): lambda v, fit, tgt: v / 100 * tgt.ship.getModifiedItemAttr('maxVelocity'), + ('tgtSigRad', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('signatureRadius')} _limiters = { 'time': lambda fit, tgt: (0, 2500)} _denormalizers = { ('distance', 'km'): lambda v, fit, tgt: v / 1000, - ('tgtSpeed', '%'): lambda v, fit, tgt: v / tgt.ship.getModifiedItemAttr('maxVelocity', 0), - ('tgtSigRad', '%'): lambda v, fit, tgt: v / fit.ship.getModifiedItemAttr('signatureRadius', 0)} + ('tgtSpeed', '%'): lambda v, fit, tgt: v * 100 / tgt.ship.getModifiedItemAttr('maxVelocity'), + ('tgtSigRad', '%'): lambda v, fit, tgt: v * 100 / fit.ship.getModifiedItemAttr('signatureRadius')} def _distance2dps(self, mainInput, miscInputs, fit, tgt): return [], [] diff --git a/gui/builtinGraphs/fitWarpTime.py b/gui/builtinGraphs/fitWarpTime.py index 6d2eafcce..d2c322dec 100644 --- a/gui/builtinGraphs/fitWarpTime.py +++ b/gui/builtinGraphs/fitWarpTime.py @@ -66,7 +66,7 @@ class FitWarpTimeGraph(FitGraph): subwarpSpeed = self.__getSubwarpSpeed(fit) warpSpeed = fit.warpSpeed for distance in self._iterLinear(mainInput[1]): - time = calculate_time_in_warp(subwarpSpeed, warpSpeed, distance) + time = calculate_time_in_warp(max_subwarp_speed=subwarpSpeed, max_warp_speed=warpSpeed, warp_dist=distance) xs.append(distance) ys.append(time) return xs, ys From eff0510092a8f231748ee55e32ff2846e219898f Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Fri, 28 Jun 2019 22:08:19 +0300 Subject: [PATCH 53/93] Do not show time input when it's not needed --- gui/builtinGraphs/fitCap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/builtinGraphs/fitCap.py b/gui/builtinGraphs/fitCap.py index 7e9dc8c58..5f6420f7e 100644 --- a/gui/builtinGraphs/fitCap.py +++ b/gui/builtinGraphs/fitCap.py @@ -44,7 +44,7 @@ class FitCapAmountVsTimeGraph(FitGraph): @property def inputs(self): return [ - Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=120, defaultRange=(0, 300), mainOnly=False), + Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=120, defaultRange=(0, 300), mainOnly=True), Input(handle='capAmount', unit='%', label='Cap amount', iconID=1668, defaultValue=25, defaultRange=(0, 100), mainOnly=True)] # Calculation stuff From 24494e9b2912fcfa0983516ad2ac70342a9c40ad Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Sat, 29 Jun 2019 00:28:24 +0300 Subject: [PATCH 54/93] Rename cap graph --- gui/builtinGraphs/__init__.py | 3 +-- gui/builtinGraphs/{fitCap.py => fitCapRegen.py} | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) rename gui/builtinGraphs/{fitCap.py => fitCapRegen.py} (98%) diff --git a/gui/builtinGraphs/__init__.py b/gui/builtinGraphs/__init__.py index b61b12335..1cfd88290 100644 --- a/gui/builtinGraphs/__init__.py +++ b/gui/builtinGraphs/__init__.py @@ -4,8 +4,7 @@ from gui.builtinGraphs import ( # noqa: E402,F401 # fitDmgVsTime, # fitShieldRegenVsShieldPerc, # fitShieldAmountVsTime, - fitCap, - # fitCapAmountVsTime, + fitCapRegen, fitMobility, fitWarpTime ) diff --git a/gui/builtinGraphs/fitCap.py b/gui/builtinGraphs/fitCapRegen.py similarity index 98% rename from gui/builtinGraphs/fitCap.py rename to gui/builtinGraphs/fitCapRegen.py index 5f6420f7e..a0c1452f4 100644 --- a/gui/builtinGraphs/fitCap.py +++ b/gui/builtinGraphs/fitCapRegen.py @@ -23,9 +23,9 @@ import math from .base import FitGraph, XDef, YDef, Input -class FitCapAmountVsTimeGraph(FitGraph): +class FitCapRegenGraph(FitGraph): - name = 'Capacitor' + name = 'Capacitor Regeneration' # UI stuff @property @@ -117,4 +117,4 @@ def calculateCapRegen(maxCapAmount, capRegenTime, currentCapAmount): return 10 * maxCapAmount / capRegenTime * (math.sqrt(currentCapAmount / maxCapAmount) - currentCapAmount / maxCapAmount) -FitCapAmountVsTimeGraph.register() +FitCapRegenGraph.register() From 7e7b49d2e4e7bd790ebfeea5ce137a75d5dc7c19 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Sat, 29 Jun 2019 10:21:16 +0300 Subject: [PATCH 55/93] Move shield regen graph to new infrastructure --- eos/graph/fitShieldAmountVsTime.py | 27 ---- eos/graph/fitShieldRegenVsShieldPerc.py | 22 ---- gui/builtinGraphs/__init__.py | 3 +- gui/builtinGraphs/base.py | 20 +-- gui/builtinGraphs/fitCapRegen.py | 41 +++--- gui/builtinGraphs/fitDamageStats.py | 60 +++------ gui/builtinGraphs/fitMobility.py | 30 ++--- gui/builtinGraphs/fitShieldAmountVsTime.py | 50 -------- gui/builtinGraphs/fitShieldRegen.py | 117 ++++++++++++++++++ .../fitShieldRegenVsShieldPerc.py | 51 -------- gui/builtinGraphs/fitWarpTime.py | 28 ++--- gui/builtinStatsViews/resistancesViewFull.py | 1 + gui/graphFrame/frame.py | 10 -- 13 files changed, 175 insertions(+), 285 deletions(-) delete mode 100644 eos/graph/fitShieldAmountVsTime.py delete mode 100644 eos/graph/fitShieldRegenVsShieldPerc.py delete mode 100644 gui/builtinGraphs/fitShieldAmountVsTime.py create mode 100644 gui/builtinGraphs/fitShieldRegen.py delete mode 100644 gui/builtinGraphs/fitShieldRegenVsShieldPerc.py diff --git a/eos/graph/fitShieldAmountVsTime.py b/eos/graph/fitShieldAmountVsTime.py deleted file mode 100644 index 443e1ea1c..000000000 --- a/eos/graph/fitShieldAmountVsTime.py +++ /dev/null @@ -1,27 +0,0 @@ -import math -from logbook import Logger - -from .base import SmoothGraph - - -pyfalog = Logger(__name__) - - -class FitShieldAmountVsTimeGraph(SmoothGraph): - - def __init__(self): - super().__init__() - import gui.mainFrame - self.mainFrame = gui.mainFrame.MainFrame.getInstance() - - def getYForX(self, fit, extraData, time): - if time < 0: - return 0 - maxShield = fit.ship.getModifiedItemAttr('shieldCapacity') - regenTime = fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000 - # https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate (shield is similar to cap) - shield = maxShield * (1 + math.exp(5 * -time / regenTime) * -1) ** 2 - useEhp = self.mainFrame.statsPane.nameViewMap["resistancesViewFull"].showEffective - if fit.damagePattern is not None and useEhp: - shield = fit.damagePattern.effectivify(fit, shield, 'shield') - return shield diff --git a/eos/graph/fitShieldRegenVsShieldPerc.py b/eos/graph/fitShieldRegenVsShieldPerc.py deleted file mode 100644 index 3793d6727..000000000 --- a/eos/graph/fitShieldRegenVsShieldPerc.py +++ /dev/null @@ -1,22 +0,0 @@ -import math - -from .base import SmoothGraph - - -class FitShieldRegenVsShieldPercGraph(SmoothGraph): - - def __init__(self): - super().__init__() - import gui.mainFrame - self.mainFrame = gui.mainFrame.MainFrame.getInstance() - - def getYForX(self, fit, extraData, perc): - maxShield = fit.ship.getModifiedItemAttr('shieldCapacity') - regenTime = fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000 - currentShield = maxShield * perc / 100 - # https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate (shield is similar to cap) - regen = 10 * maxShield / regenTime * (math.sqrt(currentShield / maxShield) - currentShield / maxShield) - useEhp = self.mainFrame.statsPane.nameViewMap["resistancesViewFull"].showEffective - if fit.damagePattern is not None and useEhp: - regen = fit.damagePattern.effectivify(fit, regen, 'shield') - return regen diff --git a/gui/builtinGraphs/__init__.py b/gui/builtinGraphs/__init__.py index 1cfd88290..82c10e1f7 100644 --- a/gui/builtinGraphs/__init__.py +++ b/gui/builtinGraphs/__init__.py @@ -2,8 +2,7 @@ from gui.builtinGraphs import ( # noqa: E402,F401 fitDamageStats, # fitDmgVsTime, - # fitShieldRegenVsShieldPerc, - # fitShieldAmountVsTime, + fitShieldRegen, fitCapRegen, fitMobility, fitWarpTime diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index c5e1262ab..83dfa775a 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -72,21 +72,9 @@ class FitGraph(metaclass=ABCMeta): def inputMap(self): return OrderedDict(((i.handle, i.unit), i) for i in self.inputs) - @property - def srcVectorDef(self): - return None - - @property - def tgtVectorDef(self): - return None - - @property - def hasTargets(self): - return False - - @property - def redrawOnEffectiveChange(self): - return False + srcVectorDef = None + tgtVectorDef = None + hasTargets = False def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt=None): try: @@ -182,7 +170,7 @@ class FitGraph(metaclass=ABCMeta): values = [denormalizer(v, fit, tgt) for v in values] return values - def _iterLinear(self, valRange, resolution=100): + def _iterLinear(self, valRange, resolution=200): rangeLow = min(valRange) rangeHigh = max(valRange) # Amount is amount of ranges between points here, not amount of points diff --git a/gui/builtinGraphs/fitCapRegen.py b/gui/builtinGraphs/fitCapRegen.py index a0c1452f4..1dfb65130 100644 --- a/gui/builtinGraphs/fitCapRegen.py +++ b/gui/builtinGraphs/fitCapRegen.py @@ -25,35 +25,24 @@ from .base import FitGraph, XDef, YDef, Input class FitCapRegenGraph(FitGraph): - name = 'Capacitor Regeneration' - # UI stuff - @property - def xDefs(self): - return [ - XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')), - XDef(handle='capAmount', unit='GJ', label='Cap amount', mainInput=('capAmount', '%')), - XDef(handle='capAmount', unit='%', label='Cap amount', mainInput=('capAmount', '%'))] - - @property - def yDefs(self): - return [ - YDef(handle='capAmount', unit='GJ', label='Cap amount'), - YDef(handle='capRegen', unit='GJ/s', label='Cap regen')] - - @property - def inputs(self): - return [ - Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=120, defaultRange=(0, 300), mainOnly=True), - Input(handle='capAmount', unit='%', label='Cap amount', iconID=1668, defaultValue=25, defaultRange=(0, 100), mainOnly=True)] + name = 'Capacitor Regeneration' + xDefs = [ + XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')), + XDef(handle='capAmount', unit='GJ', label='Cap amount', mainInput=('capAmount', '%')), + XDef(handle='capAmount', unit='%', label='Cap amount', mainInput=('capAmount', '%'))] + yDefs = [ + YDef(handle='capAmount', unit='GJ', label='Cap amount'), + YDef(handle='capRegen', unit='GJ/s', label='Cap regen')] + inputs = [ + Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=120, defaultRange=(0, 300), mainOnly=True), + Input(handle='capAmount', unit='%', label='Cap amount', iconID=1668, defaultValue=25, defaultRange=(0, 100), mainOnly=True)] # Calculation stuff _normalizers = { ('capAmount', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('capacitorCapacity')} - _limiters = { 'capAmount': lambda fit, tgt: (0, fit.ship.getModifiedItemAttr('capacitorCapacity'))} - _denormalizers = { ('capAmount', '%'): lambda v, fit, tgt: v * 100 / fit.ship.getModifiedItemAttr('capacitorCapacity')} @@ -75,9 +64,9 @@ class FitCapRegenGraph(FitGraph): capRegenTime = fit.ship.getModifiedItemAttr('rechargeRate') / 1000 for time in self._iterLinear(mainInput[1]): currentCapAmount = calculateCapAmount(maxCapAmount=maxCapAmount, capRegenTime=capRegenTime, time=time) - currentRegen = calculateCapRegen(maxCapAmount=maxCapAmount, capRegenTime=capRegenTime, currentCapAmount=currentCapAmount) + currentCapRegen = calculateCapRegen(maxCapAmount=maxCapAmount, capRegenTime=capRegenTime, currentCapAmount=currentCapAmount) xs.append(time) - ys.append(currentRegen) + ys.append(currentCapRegen) return xs, ys def _capAmount2capAmount(self, mainInput, miscInputs, fit, tgt): @@ -95,9 +84,9 @@ class FitCapRegenGraph(FitGraph): maxCapAmount = fit.ship.getModifiedItemAttr('capacitorCapacity') capRegenTime = fit.ship.getModifiedItemAttr('rechargeRate') / 1000 for currentCapAmount in self._iterLinear(mainInput[1]): - currentRegen = calculateCapRegen(maxCapAmount=maxCapAmount, capRegenTime=capRegenTime, currentCapAmount=currentCapAmount) + currentCapRegen = calculateCapRegen(maxCapAmount=maxCapAmount, capRegenTime=capRegenTime, currentCapAmount=currentCapAmount) xs.append(currentCapAmount) - ys.append(currentRegen) + ys.append(currentCapRegen) return xs, ys _getters = { diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 60e1f17d5..50a8962bc 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -23,45 +23,27 @@ from .base import FitGraph, XDef, YDef, Input, VectorDef class FitDamageStatsGraph(FitGraph): - name = 'Damage Stats' - # UI stuff - @property - def xDefs(self): - return [ - XDef(handle='distance', unit='km', label='Distance', mainInput=('distance', 'km')), - XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')), - XDef(handle='tgtSpeed', unit='m/s', label='Target speed', mainInput=('tgtSpeed', '%')), - XDef(handle='tgtSpeed', unit='%', label='Target speed', mainInput=('tgtSpeed', '%')), - XDef(handle='tgtSigRad', unit='m', label='Target signature radius', mainInput=('tgtSigRad', '%')), - XDef(handle='tgtSigRad', unit='%', label='Target signature radius', mainInput=('tgtSigRad', '%'))] - - @property - def yDefs(self): - return [ - YDef(handle='dps', unit=None, label='DPS'), - YDef(handle='volley', unit=None, label='Volley'), - YDef(handle='damage', unit=None, label='Damage inflicted')] - - @property - def inputs(self): - return [ - Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=None, defaultRange=(0, 80), mainOnly=False), - Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=50, defaultRange=(0, 100), mainOnly=False), - Input(handle='tgtSpeed', unit='%', label='Target speed', iconID=1389, defaultValue=100, defaultRange=(0, 100), mainOnly=False), - Input(handle='tgtSigRad', unit='%', label='Target signature', iconID=1390, defaultValue=100, defaultRange=(100, 200), mainOnly=True)] - - @property - def srcVectorDef(self): - return VectorDef(lengthHandle='atkSpeed', lengthUnit='%', angleHandle='atkAngle', angleUnit='degrees', label='Attacker') - - @property - def tgtVectorDef(self): - return VectorDef(lengthHandle='tgtSpeed', lengthUnit='%', angleHandle='tgtAngle', angleUnit='degrees', label='Target') - - @property - def hasTargets(self): - return True + name = 'Damage Stats' + xDefs = [ + XDef(handle='distance', unit='km', label='Distance', mainInput=('distance', 'km')), + XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')), + XDef(handle='tgtSpeed', unit='m/s', label='Target speed', mainInput=('tgtSpeed', '%')), + XDef(handle='tgtSpeed', unit='%', label='Target speed', mainInput=('tgtSpeed', '%')), + XDef(handle='tgtSigRad', unit='m', label='Target signature radius', mainInput=('tgtSigRad', '%')), + XDef(handle='tgtSigRad', unit='%', label='Target signature radius', mainInput=('tgtSigRad', '%'))] + yDefs = [ + YDef(handle='dps', unit=None, label='DPS'), + YDef(handle='volley', unit=None, label='Volley'), + YDef(handle='damage', unit=None, label='Damage inflicted')] + inputs = [ + Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=None, defaultRange=(0, 80), mainOnly=False), + Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=50, defaultRange=(0, 100), mainOnly=False), + Input(handle='tgtSpeed', unit='%', label='Target speed', iconID=1389, defaultValue=100, defaultRange=(0, 100), mainOnly=False), + Input(handle='tgtSigRad', unit='%', label='Target signature', iconID=1390, defaultValue=100, defaultRange=(100, 200), mainOnly=True)] + srcVectorDef = VectorDef(lengthHandle='atkSpeed', lengthUnit='%', angleHandle='atkAngle', angleUnit='degrees', label='Attacker') + tgtVectorDef = VectorDef(lengthHandle='tgtSpeed', lengthUnit='%', angleHandle='tgtAngle', angleUnit='degrees', label='Target') + hasTargets = True # Calculation stuff _normalizers = { @@ -69,10 +51,8 @@ class FitDamageStatsGraph(FitGraph): ('atkSpeed', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('maxVelocity'), ('tgtSpeed', '%'): lambda v, fit, tgt: v / 100 * tgt.ship.getModifiedItemAttr('maxVelocity'), ('tgtSigRad', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('signatureRadius')} - _limiters = { 'time': lambda fit, tgt: (0, 2500)} - _denormalizers = { ('distance', 'km'): lambda v, fit, tgt: v / 1000, ('tgtSpeed', '%'): lambda v, fit, tgt: v * 100 / tgt.ship.getModifiedItemAttr('maxVelocity'), diff --git a/gui/builtinGraphs/fitMobility.py b/gui/builtinGraphs/fitMobility.py index 4a084d94f..385d570c2 100644 --- a/gui/builtinGraphs/fitMobility.py +++ b/gui/builtinGraphs/fitMobility.py @@ -25,29 +25,15 @@ from .base import FitGraph, XDef, YDef, Input class FitMobilityVsTimeGraph(FitGraph): - name = 'Mobility' - - def __init__(self): - super().__init__() - # UI stuff - @property - def xDefs(self): - return [XDef(handle='time', unit='s', label='Time', mainInput=('time', 's'))] - - @property - def yDefs(self): - return [ - YDef(handle='speed', unit='m/s', label='Speed'), - YDef(handle='distance', unit='km', label='Distance')] - - @property - def inputs(self): - return [Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=10, defaultRange=(0, 30), mainOnly=False)] - - @property - def xDef(self): - return XDef(inputDefault='0-80', inputLabel='Time (seconds)', inputIconID=1392, axisLabel='Time, s') + name = 'Mobility' + xDefs = [ + XDef(handle='time', unit='s', label='Time', mainInput=('time', 's'))] + yDefs = [ + YDef(handle='speed', unit='m/s', label='Speed'), + YDef(handle='distance', unit='km', label='Distance')] + inputs = [ + Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=10, defaultRange=(0, 30), mainOnly=False)] # Calculation stuff _denormalizers = { diff --git a/gui/builtinGraphs/fitShieldAmountVsTime.py b/gui/builtinGraphs/fitShieldAmountVsTime.py deleted file mode 100644 index 1cb16aef6..000000000 --- a/gui/builtinGraphs/fitShieldAmountVsTime.py +++ /dev/null @@ -1,50 +0,0 @@ -# ============================================================================= -# Copyright (C) 2010 Diego Duclos -# -# 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 . -# ============================================================================= - - -from collections import OrderedDict - -import gui.mainFrame -from eos.graph.fitShieldAmountVsTime import FitShieldAmountVsTimeGraph as EosGraph -from .base import FitGraph, XDef, YDef - - -class FitShieldAmountVsTimeGraph(FitGraph): - - name = 'Shield Amount vs Time' - - def __init__(self): - super().__init__() - self.eosGraph = EosGraph() - self.mainFrame = gui.mainFrame.MainFrame.getInstance() - - @property - def xDef(self): - return XDef(inputDefault='0-300', inputLabel='Time (seconds)', inputIconID=1392, axisLabel='Time, s') - - @property - def yDefs(self): - axisLabel = 'Shield amount, {}'.format('EHP' if self.mainFrame.statsPane.nameViewMap["resistancesViewFull"].showEffective else 'HP') - return OrderedDict([('shieldAmount', YDef(switchLabel='Shield amount', axisLabel=axisLabel, eosGraph='eosGraph'))]) - - def redrawOnEffectiveChange(self): - return True - - -FitShieldAmountVsTimeGraph.register() diff --git a/gui/builtinGraphs/fitShieldRegen.py b/gui/builtinGraphs/fitShieldRegen.py new file mode 100644 index 000000000..837c722e4 --- /dev/null +++ b/gui/builtinGraphs/fitShieldRegen.py @@ -0,0 +1,117 @@ +# ============================================================================= +# Copyright (C) 2010 Diego Duclos +# +# 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 . +# ============================================================================= + + +import math + +import gui.mainFrame +from .base import FitGraph, XDef, YDef, Input + + +class FitShieldRegenGraph(FitGraph): + + # UI stuff + name = 'Shield Regeneration' + xDefs = [ + XDef(handle='time', unit='s', label='Time', mainInput=('time', 's')), + XDef(handle='shieldAmount', unit='EHP', label='Shield amount', mainInput=('shieldAmount', '%')), + XDef(handle='shieldAmount', unit='HP', label='Shield amount', mainInput=('shieldAmount', '%')), + XDef(handle='shieldAmount', unit='%', label='Shield amount', mainInput=('shieldAmount', '%'))] + yDefs = [ + YDef(handle='shieldAmount', unit='EHP', label='Shield amount'), + YDef(handle='shieldAmount', unit='HP', label='Shield amount'), + YDef(handle='shieldRegen', unit='EHP/s', label='Shield regen'), + YDef(handle='shieldRegen', unit='HP/s', label='Shield regen')] + inputs = [ + Input(handle='time', unit='s', label='Time', iconID=1392, defaultValue=120, defaultRange=(0, 300), mainOnly=True), + Input(handle='shieldAmount', unit='%', label='Shield amount', iconID=1384, defaultValue=25, defaultRange=(0, 100), mainOnly=True)] + + # Calculation stuff + _normalizers = { + ('shieldAmount', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('shieldCapacity')} + _limiters = { + 'shieldAmount': lambda fit, tgt: (0, fit.ship.getModifiedItemAttr('shieldCapacity'))} + _denormalizers = { + ('shieldAmount', '%'): lambda v, fit, tgt: v * 100 / fit.ship.getModifiedItemAttr('shieldCapacity'), + ('shieldAmount', 'EHP'): lambda v, fit, tgt: fit.damagePattern.effectivify(fit, v, 'shield'), + ('shieldRegen', 'EHP/s'): lambda v, fit, tgt: fit.damagePattern.effectivify(fit, v, 'shield')} + + def _time2shieldAmount(self, mainInput, miscInputs, fit, tgt): + xs = [] + ys = [] + maxShieldAmount = fit.ship.getModifiedItemAttr('shieldCapacity') + shieldRegenTime = fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000 + for time in self._iterLinear(mainInput[1]): + currentShieldAmount = calculateShieldAmount(maxShieldAmount=maxShieldAmount, shieldRegenTime=shieldRegenTime, time=time) + xs.append(time) + ys.append(currentShieldAmount) + return xs, ys + + def _time2shieldRegen(self, mainInput, miscInputs, fit, tgt): + xs = [] + ys = [] + maxShieldAmount = fit.ship.getModifiedItemAttr('shieldCapacity') + shieldRegenTime = fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000 + for time in self._iterLinear(mainInput[1]): + currentShieldAmount = calculateShieldAmount(maxShieldAmount=maxShieldAmount, shieldRegenTime=shieldRegenTime, time=time) + currentShieldRegen = calculateShieldRegen(maxShieldAmount=maxShieldAmount, shieldRegenTime=shieldRegenTime, currentShieldAmount=currentShieldAmount) + xs.append(time) + ys.append(currentShieldRegen) + return xs, ys + + def _shieldAmount2shieldAmount(self, mainInput, miscInputs, fit, tgt): + # Useless, but valid combination of x and y + xs = [] + ys = [] + for currentShieldAmount in self._iterLinear(mainInput[1]): + xs.append(currentShieldAmount) + ys.append(currentShieldAmount) + return xs, ys + + def _shieldAmount2shieldRegen(self, mainInput, miscInputs, fit, tgt): + xs = [] + ys = [] + maxShieldAmount = fit.ship.getModifiedItemAttr('shieldCapacity') + shieldRegenTime = fit.ship.getModifiedItemAttr('shieldRechargeRate') / 1000 + for currentShieldAmount in self._iterLinear(mainInput[1]): + currentShieldRegen = calculateShieldRegen(maxShieldAmount=maxShieldAmount, shieldRegenTime=shieldRegenTime, currentShieldAmount=currentShieldAmount) + xs.append(currentShieldAmount) + ys.append(currentShieldRegen) + return xs, ys + + _getters = { + ('time', 'shieldAmount'): _time2shieldAmount, + ('time', 'shieldRegen'): _time2shieldRegen, + ('shieldAmount', 'shieldAmount'): _shieldAmount2shieldAmount, + ('shieldAmount', 'shieldRegen'): _shieldAmount2shieldRegen} + + +def calculateShieldAmount(maxShieldAmount, shieldRegenTime, time): + # The same formula as for cap + # https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate + return maxShieldAmount * (1 + math.exp(5 * -time / shieldRegenTime) * -1) ** 2 + + +def calculateShieldRegen(maxShieldAmount, shieldRegenTime, currentShieldAmount): + # The same formula as for cap + # https://wiki.eveuniversity.org/Capacitor#Capacitor_recharge_rate + return 10 * maxShieldAmount / shieldRegenTime * (math.sqrt(currentShieldAmount / maxShieldAmount) - currentShieldAmount / maxShieldAmount) + + +FitShieldRegenGraph.register() diff --git a/gui/builtinGraphs/fitShieldRegenVsShieldPerc.py b/gui/builtinGraphs/fitShieldRegenVsShieldPerc.py deleted file mode 100644 index 7dc8212cb..000000000 --- a/gui/builtinGraphs/fitShieldRegenVsShieldPerc.py +++ /dev/null @@ -1,51 +0,0 @@ -# ============================================================================= -# Copyright (C) 2010 Diego Duclos -# -# 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 . -# ============================================================================= - - -from collections import OrderedDict - -import gui.mainFrame -from eos.graph.fitShieldRegenVsShieldPerc import FitShieldRegenVsShieldPercGraph as EosGraph -from .base import FitGraph, XDef, YDef - - -class FitShieldRegenVsShieldPercGraph(FitGraph): - - name = 'Shield Regen vs Shield Amount' - - def __init__(self): - super().__init__() - self.eosGraph = EosGraph() - self.mainFrame = gui.mainFrame.MainFrame.getInstance() - - @property - def xDef(self): - return XDef(inputDefault='0-100', inputLabel='Shield amount (percent)', inputIconID=1384, axisLabel='Shield amount, %') - - @property - def yDefs(self): - axisLabel = 'Shield regen, {}/s'.format('EHP' if self.mainFrame.statsPane.nameViewMap["resistancesViewFull"].showEffective else 'HP') - return OrderedDict([('shieldRegen', YDef(switchLabel='Shield regen', axisLabel=axisLabel, eosGraph='eosGraph'))]) - - @property - def redrawOnEffectiveChange(self): - return True - - -FitShieldRegenVsShieldPercGraph.register() diff --git a/gui/builtinGraphs/fitWarpTime.py b/gui/builtinGraphs/fitWarpTime.py index d2c322dec..2fd50301a 100644 --- a/gui/builtinGraphs/fitWarpTime.py +++ b/gui/builtinGraphs/fitWarpTime.py @@ -29,33 +29,23 @@ AU_METERS = 149597870700 class FitWarpTimeGraph(FitGraph): - name = 'Warp Time' - # UI stuff - @property - def xDefs(self): - return [ - XDef(handle='distance', unit='AU', label='Distance', mainInput=('distance', 'AU')), - XDef(handle='distance', unit='km', label='Distance', mainInput=('distance', 'km'))] - - @property - def yDefs(self): - return [YDef(handle='time', unit='s', label='Warp time')] - - @property - def inputs(self): - return [ - Input(handle='distance', unit='AU', label='Distance', iconID=1391, defaultValue=50, defaultRange=(0, 50), mainOnly=False), - Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=10000, defaultRange=(150, 5000), mainOnly=False)] + name = 'Warp Time' + xDefs = [ + XDef(handle='distance', unit='AU', label='Distance', mainInput=('distance', 'AU')), + XDef(handle='distance', unit='km', label='Distance', mainInput=('distance', 'km'))] + yDefs = [ + YDef(handle='time', unit='s', label='Warp time')] + inputs = [ + Input(handle='distance', unit='AU', label='Distance', iconID=1391, defaultValue=20, defaultRange=(0, 50), mainOnly=False), + Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=1000, defaultRange=(150, 5000), mainOnly=False)] # Calculation stuff _normalizers = { ('distance', 'AU'): lambda v, fit, tgt: v * AU_METERS, ('distance', 'km'): lambda v, fit, tgt: v * 1000} - _limiters = { 'distance': lambda fit, tgt: (0, fit.maxWarpDistance * AU_METERS)} - _denormalizers = { ('distance', 'AU'): lambda v, fit, tgt: v / AU_METERS, ('distance', 'km'): lambda v, fit, tgt: v / 1000} diff --git a/gui/builtinStatsViews/resistancesViewFull.py b/gui/builtinStatsViews/resistancesViewFull.py index d053b91b2..1dcf1da37 100644 --- a/gui/builtinStatsViews/resistancesViewFull.py +++ b/gui/builtinStatsViews/resistancesViewFull.py @@ -27,6 +27,7 @@ import gui.mainFrame import gui.globalEvents as GE from gui.utils import fonts + EffectiveHpToggled, EFFECTIVE_HP_TOGGLED = wx.lib.newevent.NewEvent() diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index 350f5870b..74482b1c3 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -121,8 +121,6 @@ class GraphFrame(wx.Frame): self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent) # Event bindings - external events self.mainFrame.Bind(GE.FIT_CHANGED, self.OnFitChanged) - from gui.builtinStatsViews.resistancesViewFull import EFFECTIVE_HP_TOGGLED # Grr crclar gons - self.mainFrame.Bind(EFFECTIVE_HP_TOGGLED, self.OnEhpToggled) self.Layout() self.UpdateWindowSize() @@ -150,13 +148,6 @@ class GraphFrame(wx.Frame): return event.Skip() - def OnEhpToggled(self, event): - event.Skip() - view = self.getView() - if view.redrawOnEffectiveChange: - view.clearCache() - self.draw() - def OnFitChanged(self, event): event.Skip() self.getView().clearCache(key=event.fitID) @@ -171,7 +162,6 @@ class GraphFrame(wx.Frame): def closeWindow(self): from gui.builtinStatsViews.resistancesViewFull import EFFECTIVE_HP_TOGGLED self.mainFrame.Unbind(GE.FIT_CHANGED, handler=self.OnFitChanged) - self.mainFrame.Unbind(EFFECTIVE_HP_TOGGLED, handler=self.OnEhpToggled) self.ctrlPanel.unbindExternalEvents() self.Destroy() From dd55493b4eeafd844c20b8c157fdf8ee470f1e42 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Sat, 29 Jun 2019 10:24:11 +0300 Subject: [PATCH 56/93] Minor stylistic fixes --- gui/builtinGraphs/fitDamageStats.py | 4 ++-- gui/builtinGraphs/fitShieldRegen.py | 1 - gui/builtinGraphs/fitWarpTime.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 50a8962bc..e1ad30936 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -41,8 +41,8 @@ class FitDamageStatsGraph(FitGraph): Input(handle='distance', unit='km', label='Distance', iconID=1391, defaultValue=50, defaultRange=(0, 100), mainOnly=False), Input(handle='tgtSpeed', unit='%', label='Target speed', iconID=1389, defaultValue=100, defaultRange=(0, 100), mainOnly=False), Input(handle='tgtSigRad', unit='%', label='Target signature', iconID=1390, defaultValue=100, defaultRange=(100, 200), mainOnly=True)] - srcVectorDef = VectorDef(lengthHandle='atkSpeed', lengthUnit='%', angleHandle='atkAngle', angleUnit='degrees', label='Attacker') - tgtVectorDef = VectorDef(lengthHandle='tgtSpeed', lengthUnit='%', angleHandle='tgtAngle', angleUnit='degrees', label='Target') + srcVectorDef = VectorDef(lengthHandle='atkSpeed', lengthUnit='%', angleHandle='atkAngle', angleUnit='degrees', label='Attacker') + tgtVectorDef = VectorDef(lengthHandle='tgtSpeed', lengthUnit='%', angleHandle='tgtAngle', angleUnit='degrees', label='Target') hasTargets = True # Calculation stuff diff --git a/gui/builtinGraphs/fitShieldRegen.py b/gui/builtinGraphs/fitShieldRegen.py index 837c722e4..11c20a8c1 100644 --- a/gui/builtinGraphs/fitShieldRegen.py +++ b/gui/builtinGraphs/fitShieldRegen.py @@ -20,7 +20,6 @@ import math -import gui.mainFrame from .base import FitGraph, XDef, YDef, Input diff --git a/gui/builtinGraphs/fitWarpTime.py b/gui/builtinGraphs/fitWarpTime.py index 2fd50301a..ee2ee970e 100644 --- a/gui/builtinGraphs/fitWarpTime.py +++ b/gui/builtinGraphs/fitWarpTime.py @@ -34,7 +34,7 @@ class FitWarpTimeGraph(FitGraph): xDefs = [ XDef(handle='distance', unit='AU', label='Distance', mainInput=('distance', 'AU')), XDef(handle='distance', unit='km', label='Distance', mainInput=('distance', 'km'))] - yDefs = [ + yDefs = [ YDef(handle='time', unit='s', label='Warp time')] inputs = [ Input(handle='distance', unit='AU', label='Distance', iconID=1391, defaultValue=20, defaultRange=(0, 50), mainOnly=False), From 744fce2e8269d7de644e25e7449bc44813f49011 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Sat, 29 Jun 2019 10:31:21 +0300 Subject: [PATCH 57/93] Make it obvious that we're clearning cache by fitID --- gui/builtinGraphs/base.py | 12 ++++++------ gui/builtinGraphs/fitWarpTime.py | 4 ++-- gui/graphFrame/frame.py | 6 +++--- gui/graphFrame/lists.py | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index 83dfa775a..702d8a828 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -85,14 +85,14 @@ class FitGraph(metaclass=ABCMeta): fitCache[(ySpec, xSpec)] = plotData return plotData - def clearCache(self, key=None): - if key is None: + def clearCache(self, fitID=None): + if fitID is None: self._plotCache.clear() self._calcCache.clear() - if key in self._plotCache: - del self._plotCache[key] - if key in self._calcCache: - del self._calcCache[key] + if fitID in self._plotCache: + del self._plotCache[fitID] + if fitID in self._calcCache: + del self._calcCache[fitID] # Calculation stuff def _calcPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt): diff --git a/gui/builtinGraphs/fitWarpTime.py b/gui/builtinGraphs/fitWarpTime.py index ee2ee970e..000dbf14b 100644 --- a/gui/builtinGraphs/fitWarpTime.py +++ b/gui/builtinGraphs/fitWarpTime.py @@ -66,7 +66,7 @@ class FitWarpTimeGraph(FitGraph): def __getSubwarpSpeed(self, fit): try: - subwarpSpeed = self._calcCache[fit.ID]['cleanSubwarpSpeed'] + subwarpSpeed = self._calcCache[fit.ID] except KeyError: modStates = {} for mod in fit.modules: @@ -96,7 +96,7 @@ class FitWarpTimeGraph(FitGraph): fighter.active = False fit.calculateModifiedAttributes() subwarpSpeed = fit.ship.getModifiedItemAttr('maxVelocity') - self._calcCache[fit.ID] = {'cleanSubwarpSpeed': subwarpSpeed} + self._calcCache[fit.ID] = subwarpSpeed for projInfo, state in projFitStates.items(): projInfo.active = state for mod, state in modStates.items(): diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index 74482b1c3..033957113 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -150,7 +150,7 @@ class GraphFrame(wx.Frame): def OnFitChanged(self, event): event.Skip() - self.getView().clearCache(key=event.fitID) + self.getView().clearCache(fitID=event.fitID) self.draw() def OnGraphSwitched(self, event): @@ -168,8 +168,8 @@ class GraphFrame(wx.Frame): def getView(self): return self.graphSelection.GetClientData(self.graphSelection.GetSelection()) - def clearCache(self, key=None): - self.getView().clearCache(key=key) + def clearCache(self, fitID=None): + self.getView().clearCache(fitID=fitID) def draw(self): global mpl_version diff --git a/gui/graphFrame/lists.py b/gui/graphFrame/lists.py index 4956f23ab..02ccb6546 100644 --- a/gui/graphFrame/lists.py +++ b/gui/graphFrame/lists.py @@ -109,7 +109,7 @@ class FitList(gui.display.Display): self.fits.remove(fit) self.update(self.fits) for fit in fits: - self.graphFrame.clearCache(key=fit.ID) + self.graphFrame.clearCache(fitID=fit.ID) self.graphFrame.draw() def unbindExternalEvents(self): From 62b7b44120c39a0088c3425b780ada88e1607f2b Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Sat, 29 Jun 2019 10:43:21 +0300 Subject: [PATCH 58/93] Rework cache to store plot results based on composite key --- gui/builtinGraphs/base.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index 702d8a828..5c120079c 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -38,7 +38,9 @@ class FitGraph(metaclass=ABCMeta): FitGraph.views.append(cls) def __init__(self): + # Format: {(fit ID, target type, target ID): data} self._plotCache = {} + # Format: {fit ID: data} self._calcCache = {} @property @@ -77,20 +79,31 @@ class FitGraph(metaclass=ABCMeta): hasTargets = False def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt=None): + cacheKey = (fit.ID, None, tgt) try: - plotData = self._plotCache[fit.ID][(ySpec, xSpec)] + plotData = self._plotCache[fit.ID][cacheKey] except KeyError: plotData = self._calcPlotPoints(mainInput, miscInputs, xSpec, ySpec, fit, tgt) - fitCache = self._plotCache.setdefault(fit.ID, {}) - fitCache[(ySpec, xSpec)] = plotData + self._plotCache.setdefault(cacheKey, {})[(ySpec, xSpec)] = plotData return plotData def clearCache(self, fitID=None): + # Clear everything if fitID is None: self._plotCache.clear() self._calcCache.clear() - if fitID in self._plotCache: - del self._plotCache[fitID] + return + # Clear plot cache + plotKeysToClear = set() + for cacheKey in self._plotCache: + cacheFitID, cacheTgtType, cacheTgtID = cacheKey + if fitID == cacheFitID: + plotKeysToClear.add(cacheKey) + elif fitID == cacheTgtID: + plotKeysToClear.add(cacheKey) + for cacheKey in plotKeysToClear: + del self._plotCache[cacheKey] + # Clear calc cache if fitID in self._calcCache: del self._calcCache[fitID] From fc7613451ed3f1509a0115c96a20ebcf0be7df5f Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Sat, 29 Jun 2019 11:21:54 +0300 Subject: [PATCH 59/93] Copy functionality from fit list to target list --- gui/graphFrame/lists.py | 31 ++++++++++++++----------------- gui/graphFrame/panel.py | 3 ++- 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/gui/graphFrame/lists.py b/gui/graphFrame/lists.py index 02ccb6546..a9b3ba340 100644 --- a/gui/graphFrame/lists.py +++ b/gui/graphFrame/lists.py @@ -26,7 +26,7 @@ import gui.globalEvents as GE from service.fit import Fit -class FitList(gui.display.Display): +class BaseList(gui.display.Display): DEFAULT_COLS = ( 'Base Icon', @@ -40,11 +40,6 @@ class FitList(gui.display.Display): fitToolTip = wx.ToolTip('Drag a fit into this list to graph it') self.SetToolTip(fitToolTip) - fit = Fit.getInstance().getFit(self.graphFrame.mainFrame.getActiveFit()) - if fit is not None: - self.fits.append(fit) - self.update(self.fits) - self.contextMenu = wx.Menu() removeItem = wx.MenuItem(self.contextMenu, 1, 'Remove Fit') self.contextMenu.Append(removeItem) @@ -55,7 +50,6 @@ class FitList(gui.display.Display): self.Bind(wx.EVT_CHAR_HOOK, self.kbEvent) self.Bind(wx.EVT_CONTEXT_MENU, self.OnContextMenu) - def kbEvent(self, event): keycode = event.GetKeyCode() mstate = wx.GetMouseState() @@ -125,15 +119,18 @@ class FitList(gui.display.Display): self.graphFrame.draw() -class TargetList(gui.display.Display): - - DEFAULT_COLS = ( - 'Base Icon', - 'Base Name') +class FitList(BaseList): def __init__(self, graphFrame, parent): - super().__init__(parent) - self.graphFrame = graphFrame - self.targetFits = [] - fitToolTip = wx.ToolTip('Drag a fit into this list to graph it') - self.SetToolTip(fitToolTip) + super().__init__(graphFrame, parent) + fit = Fit.getInstance().getFit(self.graphFrame.mainFrame.getActiveFit()) + if fit is not None: + self.fits.append(fit) + self.update(self.fits) + + +class TargetList(BaseList): + + def __init__(self, graphFrame, parent): + super().__init__(graphFrame, parent) + self.update(self.fits) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index b08e19ce4..8ae9a127f 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -298,10 +298,11 @@ class GraphControlPanel(wx.Panel): @property def targets(self): - return self.targetList.targetFits + return self.targetList.fits def unbindExternalEvents(self): self.fitList.unbindExternalEvents() + self.targetList.unbindExternalEvents() def formatLabel(self, axisDef): if axisDef.unit is None: From c365efb67e9d0e6f1e5af12ed9f1cf24872861b3 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Sat, 29 Jun 2019 12:31:30 +0300 Subject: [PATCH 60/93] Move dmg vs time logic into new graph infrastructure --- eos/graph/fitDmgVsTime.py | 151 ---------------------------- gui/builtinGraphs/base.py | 2 +- gui/builtinGraphs/fitDamageStats.py | 128 ++++++++++++++++++++++- gui/builtinGraphs/fitWarpTime.py | 1 + 4 files changed, 125 insertions(+), 157 deletions(-) delete mode 100644 eos/graph/fitDmgVsTime.py diff --git a/eos/graph/fitDmgVsTime.py b/eos/graph/fitDmgVsTime.py deleted file mode 100644 index 95db4b253..000000000 --- a/eos/graph/fitDmgVsTime.py +++ /dev/null @@ -1,151 +0,0 @@ -# =============================================================================== -# Copyright (C) 2010 Diego Duclos -# -# This file is part of eos. -# -# eos is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# eos 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with eos. If not, see . -# =============================================================================== - - -from eos.utils.spoolSupport import SpoolType, SpoolOptions -from gui.utils.numberFormatter import roundToPrec -from .base import Graph - - -class FitDmgVsTimeGraph(Graph): - - def getPlotPoints(self, fit, extraData, xRange, xAmount): - # We deliberately ignore xAmount here to build graph which will reflect - # all steps of building up the damage - minX, maxX = self._limitXRange(xRange, fit, extraData) - if fit.ID not in self._cache: - self.__generateCache(fit, maxX) - currentY = None - xs = [] - ys = [] - cache = self._cache[fit.ID] - for time in sorted(cache): - prevY = currentY - currentX = time / 1000 - currentY = roundToPrec(cache[time], 6) - if currentX < minX: - continue - # First set of data points - if not xs: - # Start at exactly requested time, at last known value - initialY = prevY or 0 - xs.append(minX) - ys.append(initialY) - # If current time is bigger then starting, extend plot to that time with old value - if currentX > minX: - xs.append(currentX) - ys.append(initialY) - # If new value is different, extend it with new point to the new value - if currentY != prevY: - xs.append(currentX) - ys.append(currentY) - continue - # Last data point - if currentX >= maxX: - xs.append(maxX) - ys.append(prevY) - break - # Anything in-between - if currentY != prevY: - if prevY is not None: - xs.append(currentX) - ys.append(prevY) - xs.append(currentX) - ys.append(currentY) - return xs, ys - - def getYForX(self, fit, extraData, x): - time = x * 1000 - cache = self._cache[fit.ID] - closestTime = max((t for t in cache if t <= time), default=None) - if closestTime is None: - return 0 - return roundToPrec(cache[closestTime], 6) - - def _getXLimits(self, fit, extraData): - return 0, 2500 - - def __generateCache(self, fit, maxTime): - cache = self._cache[fit.ID] = {} - - def addDmg(addedTime, addedDmg): - if addedDmg == 0: - return - if addedTime not in cache: - prevTime = max((t for t in cache if t < addedTime), default=None) - if prevTime is None: - cache[addedTime] = 0 - else: - cache[addedTime] = cache[prevTime] - for time in (t for t in cache if t >= addedTime): - cache[time] += addedDmg - - # We'll handle calculations in milliseconds - maxTime = maxTime * 1000 - for mod in fit.modules: - if not mod.isDealingDamage(): - continue - cycleParams = mod.getCycleParameters(reloadOverride=True) - if cycleParams is None: - continue - currentTime = 0 - nonstopCycles = 0 - for cycleTime, inactiveTime in cycleParams.iterCycles(): - volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True)) - for volleyTime, volley in volleyParams.items(): - addDmg(currentTime + volleyTime, volley.total) - if inactiveTime == 0: - nonstopCycles += 1 - else: - nonstopCycles = 0 - if currentTime > maxTime: - break - currentTime += cycleTime + inactiveTime - for drone in fit.drones: - if not drone.isDealingDamage(): - continue - cycleParams = drone.getCycleParameters(reloadOverride=True) - if cycleParams is None: - continue - currentTime = 0 - volleyParams = drone.getVolleyParameters() - for cycleTime, inactiveTime in cycleParams.iterCycles(): - for volleyTime, volley in volleyParams.items(): - addDmg(currentTime + volleyTime, volley.total) - if currentTime > maxTime: - break - currentTime += cycleTime + inactiveTime - for fighter in fit.fighters: - if not fighter.isDealingDamage(): - continue - cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True) - if cycleParams is None: - continue - volleyParams = fighter.getVolleyParametersPerEffect() - for effectID, abilityCycleParams in cycleParams.items(): - if effectID not in volleyParams: - continue - currentTime = 0 - abilityVolleyParams = volleyParams[effectID] - for cycleTime, inactiveTime in abilityCycleParams.iterCycles(): - for volleyTime, volley in abilityVolleyParams.items(): - addDmg(currentTime + volleyTime, volley.total) - if currentTime > maxTime: - break - currentTime += cycleTime + inactiveTime diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index 5c120079c..0cb41ab8b 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -130,7 +130,7 @@ class FitGraph(metaclass=ABCMeta): key = (miscInput.handle, miscInput.unit) if key in self._normalizers: normalizer = self._normalizers[key] - newMiscInput = (miscInput.handle, normalizer(miscInput.value)) + newMiscInput = (miscInput.handle, normalizer(miscInput.value, fit, tgt)) else: newMiscInput = (miscInput.handle, miscInput.value) newMiscInputs.append(newMiscInput) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index e1ad30936..2dbb72632 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -18,6 +18,8 @@ # ============================================================================= +from eos.utils.spoolSupport import SpoolType, SpoolOptions +from gui.utils.numberFormatter import roundToPrec from .base import FitGraph, XDef, YDef, Input, VectorDef @@ -49,14 +51,16 @@ class FitDamageStatsGraph(FitGraph): _normalizers = { ('distance', 'km'): lambda v, fit, tgt: v * 1000, ('atkSpeed', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('maxVelocity'), - ('tgtSpeed', '%'): lambda v, fit, tgt: v / 100 * tgt.ship.getModifiedItemAttr('maxVelocity'), - ('tgtSigRad', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('signatureRadius')} + #('tgtSpeed', '%'): lambda v, fit, tgt: v / 100 * tgt.ship.getModifiedItemAttr('maxVelocity'), + #('tgtSigRad', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('signatureRadius') + } _limiters = { 'time': lambda fit, tgt: (0, 2500)} _denormalizers = { ('distance', 'km'): lambda v, fit, tgt: v / 1000, - ('tgtSpeed', '%'): lambda v, fit, tgt: v * 100 / tgt.ship.getModifiedItemAttr('maxVelocity'), - ('tgtSigRad', '%'): lambda v, fit, tgt: v * 100 / fit.ship.getModifiedItemAttr('signatureRadius')} + #('tgtSpeed', '%'): lambda v, fit, tgt: v * 100 / tgt.ship.getModifiedItemAttr('maxVelocity'), + #('tgtSigRad', '%'): lambda v, fit, tgt: v * 100 / fit.ship.getModifiedItemAttr('signatureRadius') + } def _distance2dps(self, mainInput, miscInputs, fit, tgt): return [], [] @@ -74,7 +78,46 @@ class FitDamageStatsGraph(FitGraph): return [], [] def _time2damage(self, mainInput, miscInputs, fit, tgt): - return [], [] + xs = [] + ys = [] + minX, maxX = mainInput[1] + self._generateTimeCacheDmg(fit, maxX) + cache = self._calcCache[fit.ID]['timeDmg'] + currentY = None + for time in sorted(cache): + prevY = currentY + currentX = time / 1000 + currentY = roundToPrec(cache[time], 6) + if currentX < minX: + continue + # First set of data points + if not xs: + # Start at exactly requested time, at last known value + initialY = prevY or 0 + xs.append(minX) + ys.append(initialY) + # If current time is bigger then starting, extend plot to that time with old value + if currentX > minX: + xs.append(currentX) + ys.append(initialY) + # If new value is different, extend it with new point to the new value + if currentY != prevY: + xs.append(currentX) + ys.append(currentY) + continue + # Last data point + if currentX >= maxX: + xs.append(maxX) + ys.append(prevY) + break + # Anything in-between + if currentY != prevY: + if prevY is not None: + xs.append(currentX) + ys.append(prevY) + xs.append(currentX) + ys.append(currentY) + return xs, ys def _tgtSpeed2dps(self, mainInput, miscInputs, fit, tgt): return [], [] @@ -108,5 +151,80 @@ class FitDamageStatsGraph(FitGraph): ('tgtSigRad', 'volley'): _tgtSigRad2volley, ('tgtSigRad', 'damage'): _tgtSigRad2damage} + # Cache generation + def _generateTimeCacheDmg(self, fit, maxTime): + if fit.ID in self._calcCache and 'timeDmg' in self._calcCache[fit.ID]: + return + + fitCache = self._calcCache.setdefault(fit.ID, {}) + cache = fitCache['timeDmg'] = {} + + def addDmg(addedTime, addedDmg): + if addedDmg == 0: + return + if addedTime not in cache: + prevTime = max((t for t in cache if t < addedTime), default=None) + if prevTime is None: + cache[addedTime] = 0 + else: + cache[addedTime] = cache[prevTime] + for time in (t for t in cache if t >= addedTime): + cache[time] += addedDmg + + # We'll handle calculations in milliseconds + maxTime = maxTime * 1000 + for mod in fit.modules: + if not mod.isDealingDamage(): + continue + cycleParams = mod.getCycleParameters(reloadOverride=True) + if cycleParams is None: + continue + currentTime = 0 + nonstopCycles = 0 + for cycleTime, inactiveTime in cycleParams.iterCycles(): + volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True)) + for volleyTime, volley in volleyParams.items(): + addDmg(currentTime + volleyTime, volley.total) + if inactiveTime == 0: + nonstopCycles += 1 + else: + nonstopCycles = 0 + if currentTime > maxTime: + break + currentTime += cycleTime + inactiveTime + for drone in fit.drones: + if not drone.isDealingDamage(): + continue + cycleParams = drone.getCycleParameters(reloadOverride=True) + if cycleParams is None: + continue + currentTime = 0 + volleyParams = drone.getVolleyParameters() + for cycleTime, inactiveTime in cycleParams.iterCycles(): + for volleyTime, volley in volleyParams.items(): + addDmg(currentTime + volleyTime, volley.total) + if currentTime > maxTime: + break + currentTime += cycleTime + inactiveTime + for fighter in fit.fighters: + if not fighter.isDealingDamage(): + continue + cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True) + if cycleParams is None: + continue + volleyParams = fighter.getVolleyParametersPerEffect() + for effectID, abilityCycleParams in cycleParams.items(): + if effectID not in volleyParams: + continue + currentTime = 0 + abilityVolleyParams = volleyParams[effectID] + for cycleTime, inactiveTime in abilityCycleParams.iterCycles(): + for volleyTime, volley in abilityVolleyParams.items(): + addDmg(currentTime + volleyTime, volley.total) + if currentTime > maxTime: + break + currentTime += cycleTime + inactiveTime + + FitDamageStatsGraph.register() diff --git a/gui/builtinGraphs/fitWarpTime.py b/gui/builtinGraphs/fitWarpTime.py index 000dbf14b..955fa63b6 100644 --- a/gui/builtinGraphs/fitWarpTime.py +++ b/gui/builtinGraphs/fitWarpTime.py @@ -64,6 +64,7 @@ class FitWarpTimeGraph(FitGraph): _getters = { ('distance', 'time'): _distance2time} + # Cache generation def __getSubwarpSpeed(self, fit): try: subwarpSpeed = self._calcCache[fit.ID] From af642a4259e273dc8a68a8a7e308d4b2f2f29f56 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Sat, 29 Jun 2019 12:44:25 +0300 Subject: [PATCH 61/93] Normalize to seconds when possible --- gui/builtinGraphs/fitDamageStats.py | 80 ++++++++++++++--------------- 1 file changed, 38 insertions(+), 42 deletions(-) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 2dbb72632..d1242c0bd 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -80,43 +80,42 @@ class FitDamageStatsGraph(FitGraph): def _time2damage(self, mainInput, miscInputs, fit, tgt): xs = [] ys = [] - minX, maxX = mainInput[1] - self._generateTimeCacheDmg(fit, maxX) + minTime, maxTime = mainInput[1] + self._generateTimeCacheDmg(fit, maxTime) cache = self._calcCache[fit.ID]['timeDmg'] - currentY = None - for time in sorted(cache): - prevY = currentY - currentX = time / 1000 - currentY = roundToPrec(cache[time], 6) - if currentX < minX: + currentDmg = None + for currentTime in sorted(cache): + prevDmg = currentDmg + currentDmg = roundToPrec(cache[currentTime], 6) + if currentTime < minTime: continue # First set of data points if not xs: # Start at exactly requested time, at last known value - initialY = prevY or 0 - xs.append(minX) - ys.append(initialY) + initialDmg = prevDmg or 0 + xs.append(minTime) + ys.append(initialDmg) # If current time is bigger then starting, extend plot to that time with old value - if currentX > minX: - xs.append(currentX) - ys.append(initialY) + if currentTime > minTime: + xs.append(currentTime) + ys.append(initialDmg) # If new value is different, extend it with new point to the new value - if currentY != prevY: - xs.append(currentX) - ys.append(currentY) + if currentDmg != prevDmg: + xs.append(currentTime) + ys.append(currentDmg) continue # Last data point - if currentX >= maxX: - xs.append(maxX) - ys.append(prevY) + if currentTime >= maxTime: + xs.append(maxTime) + ys.append(prevDmg) break # Anything in-between - if currentY != prevY: - if prevY is not None: - xs.append(currentX) - ys.append(prevY) - xs.append(currentX) - ys.append(currentY) + if currentDmg != prevDmg: + if prevDmg is not None: + xs.append(currentTime) + ys.append(prevDmg) + xs.append(currentTime) + ys.append(currentDmg) return xs, ys def _tgtSpeed2dps(self, mainInput, miscInputs, fit, tgt): @@ -155,7 +154,6 @@ class FitDamageStatsGraph(FitGraph): def _generateTimeCacheDmg(self, fit, maxTime): if fit.ID in self._calcCache and 'timeDmg' in self._calcCache[fit.ID]: return - fitCache = self._calcCache.setdefault(fit.ID, {}) cache = fitCache['timeDmg'] = {} @@ -171,8 +169,6 @@ class FitDamageStatsGraph(FitGraph): for time in (t for t in cache if t >= addedTime): cache[time] += addedDmg - # We'll handle calculations in milliseconds - maxTime = maxTime * 1000 for mod in fit.modules: if not mod.isDealingDamage(): continue @@ -181,17 +177,17 @@ class FitDamageStatsGraph(FitGraph): continue currentTime = 0 nonstopCycles = 0 - for cycleTime, inactiveTime in cycleParams.iterCycles(): + for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles(): volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True)) - for volleyTime, volley in volleyParams.items(): - addDmg(currentTime + volleyTime, volley.total) - if inactiveTime == 0: + for volleyTimeMs, volley in volleyParams.items(): + addDmg(currentTime + volleyTimeMs / 1000, volley.total) + if inactiveTimeMs == 0: nonstopCycles += 1 else: nonstopCycles = 0 if currentTime > maxTime: break - currentTime += cycleTime + inactiveTime + currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 for drone in fit.drones: if not drone.isDealingDamage(): continue @@ -200,12 +196,12 @@ class FitDamageStatsGraph(FitGraph): continue currentTime = 0 volleyParams = drone.getVolleyParameters() - for cycleTime, inactiveTime in cycleParams.iterCycles(): - for volleyTime, volley in volleyParams.items(): - addDmg(currentTime + volleyTime, volley.total) + for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles(): + for volleyTimeMs, volley in volleyParams.items(): + addDmg(currentTime + volleyTimeMs / 1000, volley.total) if currentTime > maxTime: break - currentTime += cycleTime + inactiveTime + currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 for fighter in fit.fighters: if not fighter.isDealingDamage(): continue @@ -218,12 +214,12 @@ class FitDamageStatsGraph(FitGraph): continue currentTime = 0 abilityVolleyParams = volleyParams[effectID] - for cycleTime, inactiveTime in abilityCycleParams.iterCycles(): - for volleyTime, volley in abilityVolleyParams.items(): - addDmg(currentTime + volleyTime, volley.total) + for cycleTimeMs, inactiveTimeMs in abilityCycleParams.iterCycles(): + for volleyTimeMs, volley in abilityVolleyParams.items(): + addDmg(currentTime + volleyTimeMs / 1000, volley.total) if currentTime > maxTime: break - currentTime += cycleTime + inactiveTime + currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 From 4e7580b27785ab0eb69205245f500d0895e762b4 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Sat, 29 Jun 2019 23:49:43 +0300 Subject: [PATCH 62/93] Move dps vs time functionality to new graph --- eos/graph/fitDpsVsTime.py | 165 ---------------------------- gui/builtinGraphs/fitDamageStats.py | 122 +++++++++++++++++++- 2 files changed, 121 insertions(+), 166 deletions(-) delete mode 100644 eos/graph/fitDpsVsTime.py diff --git a/eos/graph/fitDpsVsTime.py b/eos/graph/fitDpsVsTime.py deleted file mode 100644 index e93e8a731..000000000 --- a/eos/graph/fitDpsVsTime.py +++ /dev/null @@ -1,165 +0,0 @@ -# =============================================================================== -# Copyright (C) 2010 Diego Duclos -# -# This file is part of eos. -# -# eos is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# eos 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with eos. If not, see . -# =============================================================================== - - -from itertools import chain - -from eos.utils.spoolSupport import SpoolType, SpoolOptions -from gui.utils.numberFormatter import roundToPrec -from .base import Graph - - -class FitDpsVsTimeGraph(Graph): - - def getPlotPoints(self, fit, extraData, xRange, xAmount): - # We deliberately ignore xAmount here to build graph which will reflect - # all steps of building up the damage - minX, maxX = self._limitXRange(xRange, fit, extraData) - if fit.ID not in self._cache: - self.__generateCache(fit, maxX) - currentY = None - prevY = None - xs = [] - ys = [] - cache = self._cache[fit.ID] - for time in sorted(cache): - prevY = currentY - currentX = time / 1000 - currentY = roundToPrec(cache[time], 6) - if currentX < minX: - continue - # First set of data points - if not xs: - # Start at exactly requested time, at last known value - initialY = prevY or 0 - xs.append(minX) - ys.append(initialY) - # If current time is bigger then starting, extend plot to that time with old value - if currentX > minX: - xs.append(currentX) - ys.append(initialY) - # If new value is different, extend it with new point to the new value - if currentY != prevY: - xs.append(currentX) - ys.append(currentY) - continue - # Last data point - if currentX >= maxX: - xs.append(maxX) - ys.append(prevY) - break - # Anything in-between - if currentY != prevY: - if prevY is not None: - xs.append(currentX) - ys.append(prevY) - xs.append(currentX) - ys.append(currentY) - if max(xs) < maxX: - xs.append(maxX) - ys.append(currentY or 0) - return xs, ys - - def getYForX(self, fit, extraData, x): - time = x * 1000 - cache = self._cache[fit.ID] - closestTime = max((t for t in cache if t <= time), default=None) - if closestTime is None: - return 0 - return roundToPrec(cache[closestTime], 6) - - def _getXLimits(self, fit, extraData): - return 0, 2500 - - def __generateCache(self, fit, maxTime): - cache = [] - - def addDmg(addedTimeStart, addedTimeFinish, addedDmg): - if addedDmg == 0: - return - addedDps = 1000 * addedDmg / (addedTimeFinish - addedTimeStart) - cache.append((addedTimeStart, addedTimeFinish, addedDps)) - - # We'll handle calculations in milliseconds - maxTime = maxTime * 1000 - for mod in fit.modules: - if not mod.isDealingDamage(): - continue - cycleParams = mod.getCycleParameters(reloadOverride=True) - if cycleParams is None: - continue - currentTime = 0 - nonstopCycles = 0 - for cycleTime, inactiveTime in cycleParams.iterCycles(): - cycleDamage = 0 - volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True)) - for volleyTime, volley in volleyParams.items(): - cycleDamage += volley.total - addDmg(currentTime, currentTime + cycleTime, cycleDamage) - currentTime += cycleTime + inactiveTime - if inactiveTime > 0: - nonstopCycles = 0 - else: - nonstopCycles += 1 - if currentTime > maxTime: - break - for drone in fit.drones: - if not drone.isDealingDamage(): - continue - cycleParams = drone.getCycleParameters(reloadOverride=True) - if cycleParams is None: - continue - currentTime = 0 - for cycleTime, inactiveTime in cycleParams.iterCycles(): - cycleDamage = 0 - volleyParams = drone.getVolleyParameters() - for volleyTime, volley in volleyParams.items(): - cycleDamage += volley.total - addDmg(currentTime, currentTime + cycleTime, cycleDamage) - currentTime += cycleTime + inactiveTime - if currentTime > maxTime: - break - for fighter in fit.fighters: - if not fighter.isDealingDamage(): - continue - cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True) - if cycleParams is None: - continue - volleyParams = fighter.getVolleyParametersPerEffect() - for effectID, abilityCycleParams in cycleParams.items(): - if effectID not in volleyParams: - continue - abilityVolleyParams = volleyParams[effectID] - currentTime = 0 - for cycleTime, inactiveTime in abilityCycleParams.iterCycles(): - cycleDamage = 0 - for volleyTime, volley in abilityVolleyParams.items(): - cycleDamage += volley.total - addDmg(currentTime, currentTime + cycleTime, cycleDamage) - currentTime += cycleTime + inactiveTime - if currentTime > maxTime: - break - - # Post-process cache - finalCache = {} - for time in sorted(set(chain((i[0] for i in cache), (i[1] for i in cache)))): - entries = (e for e in cache if e[0] <= time < e[1]) - dps = sum(e[2] for e in entries) - finalCache[time] = dps - self._cache[fit.ID] = finalCache diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index d1242c0bd..e6fab9970 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -18,6 +18,8 @@ # ============================================================================= +from itertools import chain + from eos.utils.spoolSupport import SpoolType, SpoolOptions from gui.utils.numberFormatter import roundToPrec from .base import FitGraph, XDef, YDef, Input, VectorDef @@ -72,7 +74,48 @@ class FitDamageStatsGraph(FitGraph): return [], [] def _time2dps(self, mainInput, miscInputs, fit, tgt): - return [], [] + xs = [] + ys = [] + minTime, maxTime = mainInput[1] + self._generateTimeCacheDps(fit, maxTime) + cache = self._calcCache[fit.ID]['timeDps'] + currentDps = None + for currentTime in sorted(cache): + prevDps = currentDps + currentDps = roundToPrec(cache[currentTime], 6) + if currentTime < minTime: + continue + # First set of data points + if not xs: + # Start at exactly requested time, at last known value + initialDps = prevDps or 0 + xs.append(minTime) + ys.append(initialDps) + # If current time is bigger then starting, extend plot to that time with old value + if currentTime > minTime: + xs.append(currentTime) + ys.append(initialDps) + # If new value is different, extend it with new point to the new value + if currentDps != prevDps: + xs.append(currentTime) + ys.append(currentDps) + continue + # Last data point + if currentTime >= maxTime: + xs.append(maxTime) + ys.append(prevDps) + break + # Anything in-between + if currentDps != prevDps: + if prevDps is not None: + xs.append(currentTime) + ys.append(prevDps) + xs.append(currentTime) + ys.append(currentDps) + if max(xs) < maxTime: + xs.append(maxTime) + ys.append(currentDps or 0) + return xs, ys def _time2volley(self, mainInput, miscInputs, fit, tgt): return [], [] @@ -221,6 +264,83 @@ class FitDamageStatsGraph(FitGraph): break currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 + def _generateTimeCacheDps(self, fit, maxTime): + if fit.ID in self._calcCache and 'timeDps' in self._calcCache[fit.ID]: + return + intermediateCache = [] + + def addDmg(addedTimeStart, addedTimeFinish, addedDmg): + if addedDmg == 0: + return + addedDps = addedDmg / (addedTimeFinish - addedTimeStart) + intermediateCache.append((addedTimeStart, addedTimeFinish, addedDps)) + + for mod in fit.modules: + if not mod.isDealingDamage(): + continue + cycleParams = mod.getCycleParameters(reloadOverride=True) + if cycleParams is None: + continue + currentTime = 0 + nonstopCycles = 0 + for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles(): + cycleDamage = 0 + volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True)) + for volleyTimeMs, volley in volleyParams.items(): + cycleDamage += volley.total + addDmg(currentTime, currentTime + cycleTimeMs / 1000, cycleDamage) + currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 + if inactiveTimeMs > 0: + nonstopCycles = 0 + else: + nonstopCycles += 1 + if currentTime > maxTime: + break + for drone in fit.drones: + if not drone.isDealingDamage(): + continue + cycleParams = drone.getCycleParameters(reloadOverride=True) + if cycleParams is None: + continue + currentTime = 0 + for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles(): + cycleDamage = 0 + volleyParams = drone.getVolleyParameters() + for volleyTimeMs, volley in volleyParams.items(): + cycleDamage += volley.total + addDmg(currentTime, currentTime + cycleTimeMs / 1000, cycleDamage) + currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 + if currentTime > maxTime: + break + for fighter in fit.fighters: + if not fighter.isDealingDamage(): + continue + cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True) + if cycleParams is None: + continue + volleyParams = fighter.getVolleyParametersPerEffect() + for effectID, abilityCycleParams in cycleParams.items(): + if effectID not in volleyParams: + continue + abilityVolleyParams = volleyParams[effectID] + currentTime = 0 + for cycleTimeMs, inactiveTimeMs in abilityCycleParams.iterCycles(): + cycleDamage = 0 + for volleyTimeMs, volley in abilityVolleyParams.items(): + cycleDamage += volley.total + addDmg(currentTime, currentTime + cycleTimeMs / 1000, cycleDamage) + currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 + if currentTime > maxTime: + break + + # Post-process cache + finalCache = {} + for time in sorted(set(chain((i[0] for i in intermediateCache), (i[1] for i in intermediateCache)))): + entries = (e for e in intermediateCache if e[0] <= time < e[1]) + dps = sum(e[2] for e in entries) + finalCache[time] = dps + fitCache = self._calcCache.setdefault(fit.ID, {}) + fitCache['timeDps'] = finalCache FitDamageStatsGraph.register() From c3efa819f467534f8a2dc266f9cbf34057ee7c99 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Sun, 30 Jun 2019 11:32:31 +0300 Subject: [PATCH 63/93] Implement fallback for case when we convert relative value into absolute and then when converting it back to relative fails --- gui/builtinGraphs/base.py | 26 +++++++++++++++++++------- gui/builtinGraphs/fitDamageStats.py | 8 ++++---- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index 0cb41ab8b..982b22d35 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -81,7 +81,7 @@ class FitGraph(metaclass=ABCMeta): def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt=None): cacheKey = (fit.ID, None, tgt) try: - plotData = self._plotCache[fit.ID][cacheKey] + plotData = self._plotCache[cacheKey][(ySpec, xSpec)] except KeyError: plotData = self._calcPlotPoints(mainInput, miscInputs, xSpec, ySpec, fit, tgt) self._plotCache.setdefault(cacheKey, {})[(ySpec, xSpec)] = plotData @@ -109,10 +109,22 @@ class FitGraph(metaclass=ABCMeta): # Calculation stuff def _calcPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt): - mainInput, miscInputs = self._normalizeParams(mainInput, miscInputs, fit, tgt) - mainInput, miscInputs = self._limitParams(mainInput, miscInputs, fit, tgt) - xs, ys = self._getPoints(mainInput, miscInputs, xSpec, ySpec, fit, tgt) - xs = self._denormalizeValues(xs, xSpec, fit, tgt) + mainParam, miscParams = self._normalizeParams(mainInput, miscInputs, fit, tgt) + mainParam, miscParams = self._limitParams(mainParam, miscParams, fit, tgt) + xs, ys = self._getPoints(mainParam, miscParams, xSpec, ySpec, fit, tgt) + # Sometimes denormalizer may fail (e.g. during conversion of 0 ship speed to %). + # If both inputs and outputs are in %, do some extra processing to at least have + # proper graph which shows that ship has the same value over whole specified + # relative parameter range + try: + xs = self._denormalizeValues(xs, xSpec, fit, tgt) + except KeyboardInterrupt: + raise + except: + if mainInput.unit == xSpec.unit == '%' and len(xs) >= 2: + xs = list(self._iterLinear(mainInput.value, segments=len(xs) - 1)) + else: + raise ys = self._denormalizeValues(ys, ySpec, fit, tgt) return xs, ys @@ -183,11 +195,11 @@ class FitGraph(metaclass=ABCMeta): values = [denormalizer(v, fit, tgt) for v in values] return values - def _iterLinear(self, valRange, resolution=200): + def _iterLinear(self, valRange, segments=200): rangeLow = min(valRange) rangeHigh = max(valRange) # Amount is amount of ranges between points here, not amount of points - step = (rangeHigh - rangeLow) / resolution + step = (rangeHigh - rangeLow) / segments if step == 0: yield rangeLow else: diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index e6fab9970..e30f9c89c 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -53,15 +53,15 @@ class FitDamageStatsGraph(FitGraph): _normalizers = { ('distance', 'km'): lambda v, fit, tgt: v * 1000, ('atkSpeed', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('maxVelocity'), - #('tgtSpeed', '%'): lambda v, fit, tgt: v / 100 * tgt.ship.getModifiedItemAttr('maxVelocity'), - #('tgtSigRad', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('signatureRadius') + ('tgtSpeed', '%'): lambda v, fit, tgt: v / 100 * tgt.ship.getModifiedItemAttr('maxVelocity'), + ('tgtSigRad', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('signatureRadius') } _limiters = { 'time': lambda fit, tgt: (0, 2500)} _denormalizers = { ('distance', 'km'): lambda v, fit, tgt: v / 1000, - #('tgtSpeed', '%'): lambda v, fit, tgt: v * 100 / tgt.ship.getModifiedItemAttr('maxVelocity'), - #('tgtSigRad', '%'): lambda v, fit, tgt: v * 100 / fit.ship.getModifiedItemAttr('signatureRadius') + ('tgtSpeed', '%'): lambda v, fit, tgt: v * 100 / tgt.ship.getModifiedItemAttr('maxVelocity'), + ('tgtSigRad', '%'): lambda v, fit, tgt: v * 100 / fit.ship.getModifiedItemAttr('signatureRadius') } def _distance2dps(self, mainInput, miscInputs, fit, tgt): From c5951955190fa39540ff41e286c0f43ff9deeb27 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Mon, 1 Jul 2019 12:32:28 +0300 Subject: [PATCH 64/93] Run special failover only on zero division errors --- gui/builtinGraphs/base.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index 982b22d35..1c651dc35 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -114,13 +114,11 @@ class FitGraph(metaclass=ABCMeta): xs, ys = self._getPoints(mainParam, miscParams, xSpec, ySpec, fit, tgt) # Sometimes denormalizer may fail (e.g. during conversion of 0 ship speed to %). # If both inputs and outputs are in %, do some extra processing to at least have - # proper graph which shows that ship has the same value over whole specified + # proper graph which shows that fit has the same value over whole specified # relative parameter range try: xs = self._denormalizeValues(xs, xSpec, fit, tgt) - except KeyboardInterrupt: - raise - except: + except ZeroDivisionError: if mainInput.unit == xSpec.unit == '%' and len(xs) >= 2: xs = list(self._iterLinear(mainInput.value, segments=len(xs) - 1)) else: From 494c9b08cb2d14a23584959db881a0227b4d6656 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Mon, 1 Jul 2019 20:11:30 +0300 Subject: [PATCH 65/93] Start implementation of generic damage-time cache generator --- gui/builtinGraphs/fitDamageStats.py | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index e30f9c89c..17651c8be 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -21,6 +21,7 @@ from itertools import chain from eos.utils.spoolSupport import SpoolType, SpoolOptions +from eos.utils.stats import DmgTypes from gui.utils.numberFormatter import roundToPrec from .base import FitGraph, XDef, YDef, Input, VectorDef @@ -194,6 +195,52 @@ class FitDamageStatsGraph(FitGraph): ('tgtSigRad', 'damage'): _tgtSigRad2damage} # Cache generation + def _generateTimeCache(self, fit, maxTime): + # Time is none means that time parameter has to be ignored, we do not + # need cache for that + if maxTime is None: + return + # If old cache covers passed time value, do not generate anything + try: + cacheTime = self._calcCache[fit.ID]['timeCache']['maxTime'] + except KeyError: + pass + else: + if maxTime <= cacheTime: + return + timeCache = self._calcCache.setdefault(fit.ID, {})['timeCache'] = {'maxTime': maxTime} + intCacheDps = {} + intCacheVolley = {} + intCacheDmg = {} + + # Modules + for mod in fit.modules: + if not mod.isDealingDamage(): + continue + cycleParams = mod.getCycleParameters(reloadOverride=True) + if cycleParams is None: + continue + currentTime = 0 + nonstopCycles = 0 + # Damage + modCacheDmg = intCacheDmg[mod] = {} + currentDmg = DmgTypes(0, 0, 0, 0) + for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles(): + volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True)) + for volleyTimeMs, volley in volleyParams.items(): + # Damage + if volley.total > 0: + currentDmg += volley + modCacheDmg[currentTime + volleyTimeMs / 1000] = currentDmg + if inactiveTimeMs == 0: + nonstopCycles += 1 + else: + nonstopCycles = 0 + if currentTime > maxTime: + break + currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 + + def _generateTimeCacheDmg(self, fit, maxTime): if fit.ID in self._calcCache and 'timeDmg' in self._calcCache[fit.ID]: return From b3027532ff7e0b7419b05f22344e8851dc647602 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 2 Jul 2019 02:08:29 +0300 Subject: [PATCH 66/93] Collect all intermediate dps/volley/damage stats for all items --- eos/utils/stats.py | 15 ++++++ gui/builtinGraphs/fitDamageStats.py | 82 +++++++++++++++++++++++++---- 2 files changed, 86 insertions(+), 11 deletions(-) diff --git a/eos/utils/stats.py b/eos/utils/stats.py index cec0a2bdd..c3005d596 100644 --- a/eos/utils/stats.py +++ b/eos/utils/stats.py @@ -68,3 +68,18 @@ class DmgTypes: self.explosive += other.explosive self._calcTotal() return self + + def __truediv__(self, div): + return type(self)( + em=self.em + div, + thermal=self.thermal + div, + kinetic=self.kinetic + div, + explosive=self.explosive + div) + + def __itruediv__(self, div): + self.em /= div + self.thermal /= div + self.kinetic /= div + self.explosive /= div + self._calcTotal() + return self diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 17651c8be..9fe422fb5 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -213,6 +213,30 @@ class FitDamageStatsGraph(FitGraph): intCacheVolley = {} intCacheDmg = {} + def addDpsVolley(ddKey, addedTimeStart, addedTimeFinish, addedVolleys): + if not addedVolleys: + return + addedDps = sum(addedVolleys) / (addedTimeFinish - addedTimeStart) + if addedDps.total > 0: + ddCacheDps = intCacheDps.setdefault(ddKey, []) + ddCacheDps.append((addedTimeStart, addedTimeFinish, addedDps)) + bestVolley = max(addedVolleys, key=lambda v: v.total) + if bestVolley.total > 0: + ddCacheVolley = intCacheVolley.setdefault(ddKey, []) + ddCacheVolley.append((addedTimeStart, addedTimeFinish, bestVolley)) + + def addDmg(ddKey, addedTime, addedDmg): + if addedDmg.total == 0: + return + ddCache = intCacheDmg.setdefault(ddKey, {}) + try: + maxTime = max(ddCache) + except ValueError: + ddCache[addedTime] = addedDmg + return + prevDmg = ddCache[maxTime] + ddCache[addedTime] = prevDmg + addedTime + # Modules for mod in fit.modules: if not mod.isDealingDamage(): @@ -222,24 +246,60 @@ class FitDamageStatsGraph(FitGraph): continue currentTime = 0 nonstopCycles = 0 - # Damage - modCacheDmg = intCacheDmg[mod] = {} - currentDmg = DmgTypes(0, 0, 0, 0) for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles(): + cycleVolleys = [] volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True)) for volleyTimeMs, volley in volleyParams.items(): - # Damage - if volley.total > 0: - currentDmg += volley - modCacheDmg[currentTime + volleyTimeMs / 1000] = currentDmg - if inactiveTimeMs == 0: - nonstopCycles += 1 - else: + cycleVolleys.append(volley) + addDmg(mod, currentTime + volleyTimeMs / 1000, volley) + addDpsVolley(mod, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys) + if inactiveTimeMs > 0: nonstopCycles = 0 + else: + nonstopCycles += 1 if currentTime > maxTime: break currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 - + # Drones + for drone in fit.drones: + if not drone.isDealingDamage(): + continue + cycleParams = drone.getCycleParameters(reloadOverride=True) + if cycleParams is None: + continue + currentTime = 0 + volleyParams = drone.getVolleyParameters() + for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles(): + cycleVolleys = [] + for volleyTimeMs, volley in volleyParams.items(): + cycleVolleys.append(volley) + addDmg(drone, currentTime + volleyTimeMs / 1000, volley) + addDpsVolley(drone, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys) + if currentTime > maxTime: + break + currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 + # Fighters + for fighter in fit.fighters: + if not fighter.isDealingDamage(): + continue + cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True) + if cycleParams is None: + continue + volleyParams = fighter.getVolleyParametersPerEffect() + for effectID, abilityCycleParams in cycleParams.items(): + if effectID not in volleyParams: + continue + currentTime = 0 + abilityVolleyParams = volleyParams[effectID] + for cycleTimeMs, inactiveTimeMs in abilityCycleParams.iterCycles(): + cycleVolleys = [] + for volleyTimeMs, volley in abilityVolleyParams.items(): + cycleVolleys.append(volley) + addDmg((fighter, effectID), currentTime + volleyTimeMs / 1000, volley) + addDpsVolley((fighter, effectID), currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys) + if currentTime > maxTime: + break + currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 def _generateTimeCacheDmg(self, fit, maxTime): if fit.ID in self._calcCache and 'timeDmg' in self._calcCache[fit.ID]: From ab6b9759b03c4a62fd3d23949f6254c9fba78677 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 2 Jul 2019 13:40:48 +0300 Subject: [PATCH 67/93] Generate proper final dmg-time cache --- eos/utils/stats.py | 15 ++- gui/builtinGraphs/fitDamageStats.py | 150 +++++++++++----------------- 2 files changed, 69 insertions(+), 96 deletions(-) diff --git a/eos/utils/stats.py b/eos/utils/stats.py index c3005d596..4d41300b4 100644 --- a/eos/utils/stats.py +++ b/eos/utils/stats.py @@ -18,6 +18,9 @@ # =============================================================================== +from utils.repr import makeReprStr + + class DmgTypes: """Container for damage data stats.""" @@ -71,10 +74,10 @@ class DmgTypes: def __truediv__(self, div): return type(self)( - em=self.em + div, - thermal=self.thermal + div, - kinetic=self.kinetic + div, - explosive=self.explosive + div) + em=self.em / div, + thermal=self.thermal / div, + kinetic=self.kinetic / div, + explosive=self.explosive / div) def __itruediv__(self, div): self.em /= div @@ -83,3 +86,7 @@ class DmgTypes: self.explosive /= div self._calcTotal() return self + + def __repr__(self): + spec = ['em', 'thermal', 'kinetic', 'explosive', 'total'] + return makeReprStr(self, spec) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 9fe422fb5..8d087fff8 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -18,6 +18,7 @@ # ============================================================================= +from copy import copy from itertools import chain from eos.utils.spoolSupport import SpoolType, SpoolOptions @@ -195,35 +196,63 @@ class FitDamageStatsGraph(FitGraph): ('tgtSigRad', 'damage'): _tgtSigRad2damage} # Cache generation - def _generateTimeCache(self, fit, maxTime): - # Time is none means that time parameter has to be ignored, we do not - # need cache for that + def _generateTimeCacheDpsVolley(self, fit, maxTime): + # Time is none means that time parameter has to be ignored, + # we do not need cache for that + if maxTime is None: + return True + self._generateTimeCacheIntermediate(fit, maxTime) + + def _generateTimeCacheDmg(self, fit, maxTime): + # Time is none means that time parameter has to be ignored, + # we do not need cache for that if maxTime is None: return - # If old cache covers passed time value, do not generate anything - try: - cacheTime = self._calcCache[fit.ID]['timeCache']['maxTime'] - except KeyError: - pass - else: - if maxTime <= cacheTime: - return + self._generateTimeCacheIntermediate(fit, maxTime) + timeCache = self._calcCache[fit.ID]['timeCache'] + # Final cache has been generated already, don't do anything + if 'finalDmg' in timeCache: + return + # Here we convert cache in form of: + # {time: {key: damage done by key by this time}} + intCache = timeCache['intermediateDmg'] + finalCache = timeCache['finalDmg'] = {} + changesMap = {} + for key, dmgMap in intCache.items(): + for time in dmgMap: + changesMap.setdefault(time, []).append(key) + timeDmgData = {} + for time in sorted(changesMap): + timeDmgData = copy(timeDmgData) + for key in changesMap[time]: + keyDmg = intCache[key][time] + if key in timeDmgData: + timeDmgData[key] = timeDmgData[key] + keyDmg + else: + timeDmgData[key] = keyDmg + finalCache[time] = timeDmgData + # We do not need intermediate cache once we have final + del timeCache['intermediateDmg'] + + def _generateTimeCacheIntermediate(self, fit, maxTime): + if self._isTimeCacheValid(fit, maxTime): + return timeCache = self._calcCache.setdefault(fit.ID, {})['timeCache'] = {'maxTime': maxTime} - intCacheDps = {} - intCacheVolley = {} - intCacheDmg = {} + intCacheDpsVolley = timeCache['intermediateDpsVolley'] = {} + intCacheDmg = timeCache['intermediateDmg'] = {} def addDpsVolley(ddKey, addedTimeStart, addedTimeFinish, addedVolleys): if not addedVolleys: return - addedDps = sum(addedVolleys) / (addedTimeFinish - addedTimeStart) - if addedDps.total > 0: - ddCacheDps = intCacheDps.setdefault(ddKey, []) - ddCacheDps.append((addedTimeStart, addedTimeFinish, addedDps)) - bestVolley = max(addedVolleys, key=lambda v: v.total) - if bestVolley.total > 0: - ddCacheVolley = intCacheVolley.setdefault(ddKey, []) - ddCacheVolley.append((addedTimeStart, addedTimeFinish, bestVolley)) + volleySum = sum(addedVolleys, DmgTypes(0, 0, 0, 0)) + if volleySum.total > 0: + addedDps = volleySum / (addedTimeFinish - addedTimeStart) + # We can take "just best" volley, no matter target resistances, because all + # known items have the same damage type ratio throughout their cycle - and + # applying resistances doesn't change final outcome + bestVolley = max(addedVolleys, key=lambda v: v.total) + ddCacheDps = intCacheDpsVolley.setdefault(ddKey, []) + ddCacheDps.append((addedTimeStart, addedTimeFinish, addedDps, bestVolley)) def addDmg(ddKey, addedTime, addedDmg): if addedDmg.total == 0: @@ -235,7 +264,7 @@ class FitDamageStatsGraph(FitGraph): ddCache[addedTime] = addedDmg return prevDmg = ddCache[maxTime] - ddCache[addedTime] = prevDmg + addedTime + ddCache[addedTime] = prevDmg + addedDmg # Modules for mod in fit.modules: @@ -301,75 +330,12 @@ class FitDamageStatsGraph(FitGraph): break currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 - def _generateTimeCacheDmg(self, fit, maxTime): - if fit.ID in self._calcCache and 'timeDmg' in self._calcCache[fit.ID]: - return - fitCache = self._calcCache.setdefault(fit.ID, {}) - cache = fitCache['timeDmg'] = {} - - def addDmg(addedTime, addedDmg): - if addedDmg == 0: - return - if addedTime not in cache: - prevTime = max((t for t in cache if t < addedTime), default=None) - if prevTime is None: - cache[addedTime] = 0 - else: - cache[addedTime] = cache[prevTime] - for time in (t for t in cache if t >= addedTime): - cache[time] += addedDmg - - for mod in fit.modules: - if not mod.isDealingDamage(): - continue - cycleParams = mod.getCycleParameters(reloadOverride=True) - if cycleParams is None: - continue - currentTime = 0 - nonstopCycles = 0 - for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles(): - volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True)) - for volleyTimeMs, volley in volleyParams.items(): - addDmg(currentTime + volleyTimeMs / 1000, volley.total) - if inactiveTimeMs == 0: - nonstopCycles += 1 - else: - nonstopCycles = 0 - if currentTime > maxTime: - break - currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 - for drone in fit.drones: - if not drone.isDealingDamage(): - continue - cycleParams = drone.getCycleParameters(reloadOverride=True) - if cycleParams is None: - continue - currentTime = 0 - volleyParams = drone.getVolleyParameters() - for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles(): - for volleyTimeMs, volley in volleyParams.items(): - addDmg(currentTime + volleyTimeMs / 1000, volley.total) - if currentTime > maxTime: - break - currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 - for fighter in fit.fighters: - if not fighter.isDealingDamage(): - continue - cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True) - if cycleParams is None: - continue - volleyParams = fighter.getVolleyParametersPerEffect() - for effectID, abilityCycleParams in cycleParams.items(): - if effectID not in volleyParams: - continue - currentTime = 0 - abilityVolleyParams = volleyParams[effectID] - for cycleTimeMs, inactiveTimeMs in abilityCycleParams.iterCycles(): - for volleyTimeMs, volley in abilityVolleyParams.items(): - addDmg(currentTime + volleyTimeMs / 1000, volley.total) - if currentTime > maxTime: - break - currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 + def _isTimeCacheValid(self, fit, maxTime): + try: + cacheMaxTime = self._calcCache[fit.ID]['timeCache']['maxTime'] + except KeyError: + return False + return maxTime <= cacheMaxTime def _generateTimeCacheDps(self, fit, maxTime): if fit.ID in self._calcCache and 'timeDps' in self._calcCache[fit.ID]: From f51979b69a849dd374af2ea83bc66b3eea06d15c Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 2 Jul 2019 13:49:29 +0300 Subject: [PATCH 68/93] Plug new cache format into dmg vs time graph --- gui/builtinGraphs/fitDamageStats.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 8d087fff8..dc9cea7ff 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -125,13 +125,17 @@ class FitDamageStatsGraph(FitGraph): def _time2damage(self, mainInput, miscInputs, fit, tgt): xs = [] ys = [] + + def calcDamageTmp(timeDmg): + return roundToPrec(sum(dt.total for dt in timeDmg.values()), 6) + minTime, maxTime = mainInput[1] self._generateTimeCacheDmg(fit, maxTime) - cache = self._calcCache[fit.ID]['timeDmg'] + cache = self._calcCache[fit.ID]['timeCache']['finalDmg'] currentDmg = None for currentTime in sorted(cache): prevDmg = currentDmg - currentDmg = roundToPrec(cache[currentTime], 6) + currentDmg = calcDamageTmp(cache[currentTime]) if currentTime < minTime: continue # First set of data points From c04c672f118b8d5a6f9e2d26578ad2d52ac24d86 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 2 Jul 2019 14:38:01 +0300 Subject: [PATCH 69/93] Fix incorrect intermediate-to-final cache conversion --- gui/builtinGraphs/fitDamageStats.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index dc9cea7ff..c89760783 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -229,11 +229,7 @@ class FitDamageStatsGraph(FitGraph): for time in sorted(changesMap): timeDmgData = copy(timeDmgData) for key in changesMap[time]: - keyDmg = intCache[key][time] - if key in timeDmgData: - timeDmgData[key] = timeDmgData[key] + keyDmg - else: - timeDmgData[key] = keyDmg + timeDmgData[key] = intCache[key][time] finalCache[time] = timeDmgData # We do not need intermediate cache once we have final del timeCache['intermediateDmg'] From 52490144d35a624be56c113ea80f584471596252 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 2 Jul 2019 14:42:03 +0300 Subject: [PATCH 70/93] Move some processing from intermediate method to final method to save resources when we need dps/volley, not damage --- gui/builtinGraphs/fitDamageStats.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index c89760783..55ed7c58c 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -229,7 +229,11 @@ class FitDamageStatsGraph(FitGraph): for time in sorted(changesMap): timeDmgData = copy(timeDmgData) for key in changesMap[time]: - timeDmgData[key] = intCache[key][time] + keyDmg = intCache[key][time] + if key in timeDmgData: + timeDmgData[key] = timeDmgData[key] + keyDmg + else: + timeDmgData[key] = keyDmg finalCache[time] = timeDmgData # We do not need intermediate cache once we have final del timeCache['intermediateDmg'] @@ -257,14 +261,7 @@ class FitDamageStatsGraph(FitGraph): def addDmg(ddKey, addedTime, addedDmg): if addedDmg.total == 0: return - ddCache = intCacheDmg.setdefault(ddKey, {}) - try: - maxTime = max(ddCache) - except ValueError: - ddCache[addedTime] = addedDmg - return - prevDmg = ddCache[maxTime] - ddCache[addedTime] = prevDmg + addedDmg + intCacheDmg.setdefault(ddKey, {})[addedTime] = addedDmg # Modules for mod in fit.modules: From aae2e7c531d07562fd49916ffa6747e95927a816 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 2 Jul 2019 16:30:24 +0300 Subject: [PATCH 71/93] Enable all dps graphs over time --- gui/builtinGraphs/fitDamageStats.py | 204 +++++++++++++++------------- 1 file changed, 107 insertions(+), 97 deletions(-) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 55ed7c58c..9d3cbed94 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -21,9 +21,9 @@ from copy import copy from itertools import chain +from eos.utils.float import floatUnerr from eos.utils.spoolSupport import SpoolType, SpoolOptions from eos.utils.stats import DmgTypes -from gui.utils.numberFormatter import roundToPrec from .base import FitGraph, XDef, YDef, Input, VectorDef @@ -56,15 +56,13 @@ class FitDamageStatsGraph(FitGraph): ('distance', 'km'): lambda v, fit, tgt: v * 1000, ('atkSpeed', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('maxVelocity'), ('tgtSpeed', '%'): lambda v, fit, tgt: v / 100 * tgt.ship.getModifiedItemAttr('maxVelocity'), - ('tgtSigRad', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('signatureRadius') - } + ('tgtSigRad', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('signatureRadius')} _limiters = { 'time': lambda fit, tgt: (0, 2500)} _denormalizers = { ('distance', 'km'): lambda v, fit, tgt: v / 1000, ('tgtSpeed', '%'): lambda v, fit, tgt: v * 100 / tgt.ship.getModifiedItemAttr('maxVelocity'), - ('tgtSigRad', '%'): lambda v, fit, tgt: v * 100 / fit.ship.getModifiedItemAttr('signatureRadius') - } + ('tgtSigRad', '%'): lambda v, fit, tgt: v * 100 / fit.ship.getModifiedItemAttr('signatureRadius')} def _distance2dps(self, mainInput, miscInputs, fit, tgt): return [], [] @@ -76,96 +74,22 @@ class FitDamageStatsGraph(FitGraph): return [], [] def _time2dps(self, mainInput, miscInputs, fit, tgt): - xs = [] - ys = [] - minTime, maxTime = mainInput[1] - self._generateTimeCacheDps(fit, maxTime) - cache = self._calcCache[fit.ID]['timeDps'] - currentDps = None - for currentTime in sorted(cache): - prevDps = currentDps - currentDps = roundToPrec(cache[currentTime], 6) - if currentTime < minTime: - continue - # First set of data points - if not xs: - # Start at exactly requested time, at last known value - initialDps = prevDps or 0 - xs.append(minTime) - ys.append(initialDps) - # If current time is bigger then starting, extend plot to that time with old value - if currentTime > minTime: - xs.append(currentTime) - ys.append(initialDps) - # If new value is different, extend it with new point to the new value - if currentDps != prevDps: - xs.append(currentTime) - ys.append(currentDps) - continue - # Last data point - if currentTime >= maxTime: - xs.append(maxTime) - ys.append(prevDps) - break - # Anything in-between - if currentDps != prevDps: - if prevDps is not None: - xs.append(currentTime) - ys.append(prevDps) - xs.append(currentTime) - ys.append(currentDps) - if max(xs) < maxTime: - xs.append(maxTime) - ys.append(currentDps or 0) - return xs, ys + def calcDpsTmp(timeDmg): + return floatUnerr(sum(dts[0].total for dts in timeDmg.values())) + self._generateTimeCacheDpsVolley(fit, mainInput[1][1]) + return self._composeTimeGraph(mainInput, fit, 'finalDpsVolley', calcDpsTmp) def _time2volley(self, mainInput, miscInputs, fit, tgt): - return [], [] + def calcVolleyTmp(timeDmg): + return floatUnerr(sum(dts[1].total for dts in timeDmg.values())) + self._generateTimeCacheDpsVolley(fit, mainInput[1][1]) + return self._composeTimeGraph(mainInput, fit, 'finalDpsVolley', calcVolleyTmp) def _time2damage(self, mainInput, miscInputs, fit, tgt): - xs = [] - ys = [] - def calcDamageTmp(timeDmg): - return roundToPrec(sum(dt.total for dt in timeDmg.values()), 6) - - minTime, maxTime = mainInput[1] - self._generateTimeCacheDmg(fit, maxTime) - cache = self._calcCache[fit.ID]['timeCache']['finalDmg'] - currentDmg = None - for currentTime in sorted(cache): - prevDmg = currentDmg - currentDmg = calcDamageTmp(cache[currentTime]) - if currentTime < minTime: - continue - # First set of data points - if not xs: - # Start at exactly requested time, at last known value - initialDmg = prevDmg or 0 - xs.append(minTime) - ys.append(initialDmg) - # If current time is bigger then starting, extend plot to that time with old value - if currentTime > minTime: - xs.append(currentTime) - ys.append(initialDmg) - # If new value is different, extend it with new point to the new value - if currentDmg != prevDmg: - xs.append(currentTime) - ys.append(currentDmg) - continue - # Last data point - if currentTime >= maxTime: - xs.append(maxTime) - ys.append(prevDmg) - break - # Anything in-between - if currentDmg != prevDmg: - if prevDmg is not None: - xs.append(currentTime) - ys.append(prevDmg) - xs.append(currentTime) - ys.append(currentDmg) - return xs, ys + return floatUnerr(sum(dt.total for dt in timeDmg.values())) + self._generateTimeCacheDmg(fit, mainInput[1][1]) + return self._composeTimeGraph(mainInput, fit, 'finalDmg', calcDamageTmp) def _tgtSpeed2dps(self, mainInput, miscInputs, fit, tgt): return [], [] @@ -206,6 +130,47 @@ class FitDamageStatsGraph(FitGraph): if maxTime is None: return True self._generateTimeCacheIntermediate(fit, maxTime) + timeCache = self._calcCache[fit.ID]['timeCache'] + # Final cache has been generated already, don't do anything + if 'finalDpsVolley' in timeCache: + return + # Convert cache from segments with assigned values into points + # which are located at times when dps/volley values change + pointCache = {} + for key, dmgList in timeCache['intermediateDpsVolley'].items(): + pointData = pointCache[key] = {} + prevDps = None + prevVolley = None + prevTimeEnd = None + for timeStart, timeEnd, dps, volley in dmgList: + # First item + if not pointData: + pointData[timeStart] = (dps, volley) + # Gap between items + elif floatUnerr(prevTimeEnd) < floatUnerr(timeStart): + pointData[prevTimeEnd] = (DmgTypes(0, 0, 0, 0), DmgTypes(0, 0, 0, 0)) + pointData[timeStart] = (dps, volley) + # Changed value + elif dps != prevDps or volley != prevVolley: + pointData[timeStart] = (dps, volley) + prevDps = dps + prevVolley = volley + prevTimeEnd = timeEnd + # We have another intermediate form, do not need old one any longer + del timeCache['intermediateDpsVolley'] + changesByTime = {} + for key, dmgMap in pointCache.items(): + for time in dmgMap: + changesByTime.setdefault(time, []).append(key) + # Here we convert cache to following format: + # {time: {key: (dps, volley}} + finalCache = timeCache['finalDpsVolley'] = {} + timeDmgData = {} + for time in sorted(changesByTime): + timeDmgData = copy(timeDmgData) + for key in changesByTime[time]: + timeDmgData[key] = pointCache[key][time] + finalCache[time] = timeDmgData def _generateTimeCacheDmg(self, fit, maxTime): # Time is none means that time parameter has to be ignored, @@ -217,18 +182,18 @@ class FitDamageStatsGraph(FitGraph): # Final cache has been generated already, don't do anything if 'finalDmg' in timeCache: return - # Here we convert cache in form of: - # {time: {key: damage done by key by this time}} intCache = timeCache['intermediateDmg'] - finalCache = timeCache['finalDmg'] = {} - changesMap = {} + changesByTime = {} for key, dmgMap in intCache.items(): for time in dmgMap: - changesMap.setdefault(time, []).append(key) + changesByTime.setdefault(time, []).append(key) + # Here we convert cache to following format: + # {time: {key: damage done by key at this time}} + finalCache = timeCache['finalDmg'] = {} timeDmgData = {} - for time in sorted(changesMap): + for time in sorted(changesByTime): timeDmgData = copy(timeDmgData) - for key in changesMap[time]: + for key in changesByTime[time]: keyDmg = intCache[key][time] if key in timeDmgData: timeDmgData[key] = timeDmgData[key] + keyDmg @@ -412,5 +377,50 @@ class FitDamageStatsGraph(FitGraph): fitCache = self._calcCache.setdefault(fit.ID, {}) fitCache['timeDps'] = finalCache + def _composeTimeGraph(self, mainInput, fit, cacheName, calcFunc): + xs = [] + ys = [] + + minTime, maxTime = mainInput[1] + cache = self._calcCache[fit.ID]['timeCache'][cacheName] + currentDps = None + currentTime = None + for currentTime in sorted(cache): + prevDps = currentDps + currentDps = calcFunc(cache[currentTime]) + if currentTime < minTime: + continue + # First set of data points + if not xs: + # Start at exactly requested time, at last known value + initialDps = prevDps or 0 + xs.append(minTime) + ys.append(initialDps) + # If current time is bigger then starting, extend plot to that time with old value + if currentTime > minTime: + xs.append(currentTime) + ys.append(initialDps) + # If new value is different, extend it with new point to the new value + if currentDps != prevDps: + xs.append(currentTime) + ys.append(currentDps) + continue + # Last data point + if currentTime >= maxTime: + xs.append(maxTime) + ys.append(prevDps) + break + # Anything in-between + if currentDps != prevDps: + if prevDps is not None: + xs.append(currentTime) + ys.append(prevDps) + xs.append(currentTime) + ys.append(currentDps) + if maxTime > (currentTime or 0): + xs.append(maxTime) + ys.append(currentDps or 0) + return xs, ys + FitDamageStatsGraph.register() From c3becec8228208e0d285e6561e1405f43da3768e Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Tue, 2 Jul 2019 16:36:11 +0300 Subject: [PATCH 72/93] Refresh graph when calculation returned some error --- gui/graphFrame/frame.py | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index 033957113..716d934bb 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -220,6 +220,7 @@ class GraphFrame(wx.Frame): except Exception as ex: pyfalog.warning('Invalid values in "{0}"', fit.name) self.canvas.draw() + self.Refresh() return # Special case for when we do not show Y = 0 and have no fits From 6bcc906c4a1da855f64a42ef3519bc1462acd940 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 3 Jul 2019 08:25:27 +0300 Subject: [PATCH 73/93] Start moving some math to the new damage graph --- gui/builtinGraphs/fitDamageStats.py | 12 ++++++++ gui/builtinGraphs/fitDmgVsTime.py | 48 ----------------------------- gui/graphFrame/panel.py | 2 +- 3 files changed, 13 insertions(+), 49 deletions(-) delete mode 100644 gui/builtinGraphs/fitDmgVsTime.py diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 9d3cbed94..c8680534b 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -423,4 +423,16 @@ class FitDamageStatsGraph(FitGraph): return xs, ys +def calculateAngularVelocity(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAndle, tgtRadius): + ctcDistance = atkRadius + distance + tgtRadius + + +def calculateRangeFactor(atkOptimalRange, atkFalloffRange, distance): + return 0.5 ** ((max(0, distance - atkOptimalRange) / atkFalloffRange) ** 2) + + +def calculateTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius): + return 0.5 ** (((angularSpeed * atkOptimalSigRadius) / (atkTracking * tgtSigRadius)) ** 2) + + FitDamageStatsGraph.register() diff --git a/gui/builtinGraphs/fitDmgVsTime.py b/gui/builtinGraphs/fitDmgVsTime.py deleted file mode 100644 index cf2af9284..000000000 --- a/gui/builtinGraphs/fitDmgVsTime.py +++ /dev/null @@ -1,48 +0,0 @@ -# ============================================================================= -# Copyright (C) 2010 Diego Duclos -# -# 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 . -# ============================================================================= - - -from collections import OrderedDict - -from eos.graph.fitDmgVsTime import FitDmgVsTimeGraph as EosGraphDmg -from eos.graph.fitDpsVsTime import FitDpsVsTimeGraph as EosGraphDps -from .base import FitGraph, XDef, YDef - - -class FitDmgVsTimeGraph(FitGraph): - - name = 'Damage vs Time' - - def __init__(self): - super().__init__() - self.eosGraphDmg = EosGraphDmg() - self.eosGraphDps = EosGraphDps() - - @property - def xDef(self): - return XDef(inputDefault='0-80', inputLabel='Time (seconds)', inputIconID=1392, axisLabel='Time, s') - - @property - def yDefs(self): - return OrderedDict([ - ('damage', YDef(switchLabel='Damage inflicted', axisLabel='Damage', eosGraph='eosGraphDmg')), - ('dps', YDef(switchLabel='DPS', axisLabel='DPS', eosGraph='eosGraphDps'))]) - - -FitDmgVsTimeGraph.register() diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 8ae9a127f..4f0105387 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -85,7 +85,7 @@ class GraphControlPanel(wx.Panel): self.tgtVectorSizer = wx.BoxSizer(wx.VERTICAL) self.tgtVectorLabel = wx.StaticText(self, wx.ID_ANY, '') self.tgtVectorSizer.Add(self.tgtVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM, 5) - self.tgtVector = VectorPicker(self, style=wx.NO_BORDER, size=75, offset=-90) + self.tgtVector = VectorPicker(self, style=wx.NO_BORDER, size=75, offset=90) self.tgtVector.Bind(VectorPicker.EVT_VECTOR_CHANGED, self.OnFieldChanged) self.tgtVectorSizer.Add(self.tgtVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) graphOptsSizer.Add(self.tgtVectorSizer, 0, wx.EXPAND | wx.LEFT, 10) From 86e04321c86b3ad4547c332e40185263af91241f Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 3 Jul 2019 08:39:44 +0300 Subject: [PATCH 74/93] Add some calculations to angular velocity calculator --- gui/builtinGraphs/fitDamageStats.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index c8680534b..2994010ca 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -18,6 +18,7 @@ # ============================================================================= +import math from copy import copy from itertools import chain @@ -423,8 +424,15 @@ class FitDamageStatsGraph(FitGraph): return xs, ys -def calculateAngularVelocity(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAndle, tgtRadius): +def calculateAngularVelocity(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius): + atkAngle = atkAngle * math.pi / 180 + tgtAngle = tgtAngle * math.pi / 180 ctcDistance = atkRadius + distance + tgtRadius + atkSpeedX = atkSpeed * math.cos(atkAngle) + atkSpeedY = atkSpeed * math.sin(atkAngle) + tgtSpeedX = tgtSpeed * math.cos(tgtAngle) + tgtSpeedY = tgtSpeed * math.sin(tgtAngle) + relSpeed = math.sqrt((atkSpeedX + tgtSpeedX) ** 2 + (atkSpeedY + tgtSpeedY) ** 2) def calculateRangeFactor(atkOptimalRange, atkFalloffRange, distance): From b8d189c0ad2d49cc9c5e456b89919fdb8d490e3a Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 3 Jul 2019 10:14:55 +0300 Subject: [PATCH 75/93] Change vector behavior to be consistent with trigonometry --- gui/graphFrame/panel.py | 4 ++-- gui/graphFrame/vector.py | 14 +++++++------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 4f0105387..9e1558a83 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -77,7 +77,7 @@ class GraphControlPanel(wx.Panel): self.srcVectorSizer = wx.BoxSizer(wx.VERTICAL) self.srcVectorLabel = wx.StaticText(self, wx.ID_ANY, '') self.srcVectorSizer.Add(self.srcVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL| wx.BOTTOM, 5) - self.srcVector = VectorPicker(self, style=wx.NO_BORDER, size=75, offset=90) + self.srcVector = VectorPicker(self, style=wx.NO_BORDER, size=75, offset=0) self.srcVector.Bind(VectorPicker.EVT_VECTOR_CHANGED, self.OnFieldChanged) self.srcVectorSizer.Add(self.srcVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) graphOptsSizer.Add(self.srcVectorSizer, 0, wx.EXPAND | wx.LEFT, 15) @@ -85,7 +85,7 @@ class GraphControlPanel(wx.Panel): self.tgtVectorSizer = wx.BoxSizer(wx.VERTICAL) self.tgtVectorLabel = wx.StaticText(self, wx.ID_ANY, '') self.tgtVectorSizer.Add(self.tgtVectorLabel, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.BOTTOM, 5) - self.tgtVector = VectorPicker(self, style=wx.NO_BORDER, size=75, offset=90) + self.tgtVector = VectorPicker(self, style=wx.NO_BORDER, size=75, offset=0) self.tgtVector.Bind(VectorPicker.EVT_VECTOR_CHANGED, self.OnFieldChanged) self.tgtVectorSizer.Add(self.tgtVector, 0, wx.SHAPED | wx.ALIGN_CENTER_HORIZONTAL | wx.ALIGN_CENTER_VERTICAL | wx.ALL, 0) graphOptsSizer.Add(self.tgtVectorSizer, 0, wx.EXPAND | wx.LEFT, 10) diff --git a/gui/graphFrame/vector.py b/gui/graphFrame/vector.py index a9224367b..7b4a278ce 100644 --- a/gui/graphFrame/vector.py +++ b/gui/graphFrame/vector.py @@ -52,9 +52,9 @@ class VectorPicker(wx.Window): @property def _tooltip(self): if self._directionOnly: - return 'Click to set angle\nRight-click to snap to 15% angle' + return 'Click to set angle\nShift-click or right-click to snap to 15% angle' else: - return 'Click to set angle and velocity\nRight-click to snap to 15% angle/5% speed increments\nMouse wheel to change velocity only' + return 'Click to set angle and velocity\nShift-click or right-click to snap to 15% angle/5% speed increments\nMouse wheel to change velocity only' @property def _length(self): @@ -112,8 +112,8 @@ class VectorPicker(wx.Window): dc.SetBrush(wx.WHITE_BRUSH) dc.DrawCircle(radius + 2, radius + 2, radius) a = math.radians(self._angle + self._offset) - x = math.sin(a) * radius - y = math.cos(a) * radius + x = math.cos(a) * radius + y = math.sin(a) * radius dc.DrawLine(radius + 2, radius + 2, radius + 2 + x * self._length, radius + 2 - y * self._length) dc.SetBrush(wx.BLACK_BRUSH) dc.DrawCircle(radius + 2 + x * self._length, radius + 2 - y * self._length, 2) @@ -205,11 +205,11 @@ class VectorPicker(wx.Window): x = x - center y = center - y angle = self._angle - length = min((x * x + y * y) ** 0.5 / (center - 2), 1.0) + length = min((x ** 2 + y ** 2) ** 0.5 / (center - 2), 1.0) if length < 0.01: length = 0 else: - angle = ((math.degrees(math.atan2(x, y)) - self._offset + 180) % 360) - 180 + angle = ((math.degrees(math.atan2(y, x)) - self._offset + 180) % 360) - 180 if (self._right and not self._left) or event.ShiftDown(): angle = round(angle / 15.0) * 15.0 # floor() for length to make it easier to hit 0%, can still hit 100% outside the circle @@ -218,7 +218,7 @@ class VectorPicker(wx.Window): self._angle = angle self._length = length self.Refresh() - if self._right and not self._left: + if (self._right and not self._left) or event.ShiftDown(): self.SendChangeEvent() def SendChangeEvent(self): From 6ab79ab5c0cc9abd768cacc754e2c8a28790e64a Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 3 Jul 2019 10:19:33 +0300 Subject: [PATCH 76/93] Fix angular speed calculation --- gui/builtinGraphs/fitDamageStats.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 2994010ca..c9f5be3c3 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -428,11 +428,13 @@ def calculateAngularVelocity(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, atkAngle = atkAngle * math.pi / 180 tgtAngle = tgtAngle * math.pi / 180 ctcDistance = atkRadius + distance + tgtRadius - atkSpeedX = atkSpeed * math.cos(atkAngle) - atkSpeedY = atkSpeed * math.sin(atkAngle) - tgtSpeedX = tgtSpeed * math.cos(tgtAngle) - tgtSpeedY = tgtSpeed * math.sin(tgtAngle) - relSpeed = math.sqrt((atkSpeedX + tgtSpeedX) ** 2 + (atkSpeedY + tgtSpeedY) ** 2) + # Target is to the right of the attacker, so transversal is projection onto Y axis + transSpeed = abs(atkSpeed * math.sin(atkAngle) - tgtSpeed * math.sin(tgtAngle)) + if ctcDistance == 0: + angularSpeed = 0 if transSpeed == 0 else math.inf + else: + angularSpeed = transSpeed / ctcDistance + return angularSpeed def calculateRangeFactor(atkOptimalRange, atkFalloffRange, distance): From 120bd9aa0cd472c31463d4387fd783f83345986d Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 3 Jul 2019 10:23:12 +0300 Subject: [PATCH 77/93] Set attacker vector to 90 degrees as well to be able to transversal match with fewer clicks --- gui/graphFrame/panel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/graphFrame/panel.py b/gui/graphFrame/panel.py index 9e1558a83..6742453a1 100644 --- a/gui/graphFrame/panel.py +++ b/gui/graphFrame/panel.py @@ -321,5 +321,5 @@ class GraphControlPanel(wx.Panel): self._storedRanges.clear() def _setVectorDefaults(self): - self.srcVector.SetValue(length=0, angle=0) + self.srcVector.SetValue(length=0, angle=90) self.tgtVector.SetValue(length=1, angle=90) From d27d7656d52f1311f65a4528fb9a9a25352c2fdd Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 3 Jul 2019 10:38:06 +0300 Subject: [PATCH 78/93] Implement turret cth formula --- gui/builtinGraphs/fitDamageStats.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index c9f5be3c3..567d45f5d 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -424,7 +424,18 @@ class FitDamageStatsGraph(FitGraph): return xs, ys -def calculateAngularVelocity(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius): +def calcTurretCth( + atkSpeed, atkAngle, atkRadius, atkOptimalRange, atkFalloffRange, atkTracking, atkOptimalSigRadius, + distance, tgtSpeed, tgtAngle, tgtRadius, tgtSigRadius +): + angularSpeed = calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius) + rangeFactor = calcRangeFactor(atkOptimalRange, atkFalloffRange, distance) + trackingFactor = calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius) + cth = rangeFactor * trackingFactor + return cth + + +def calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius): atkAngle = atkAngle * math.pi / 180 tgtAngle = tgtAngle * math.pi / 180 ctcDistance = atkRadius + distance + tgtRadius @@ -437,11 +448,11 @@ def calculateAngularVelocity(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, return angularSpeed -def calculateRangeFactor(atkOptimalRange, atkFalloffRange, distance): +def calcRangeFactor(atkOptimalRange, atkFalloffRange, distance): return 0.5 ** ((max(0, distance - atkOptimalRange) / atkFalloffRange) ** 2) -def calculateTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius): +def calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius): return 0.5 ** (((angularSpeed * atkOptimalSigRadius) / (atkTracking * tgtSigRadius)) ** 2) From 405492d9d78a5aa64e7bce7ae8ffd898c44f0e1c Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 3 Jul 2019 11:38:21 +0300 Subject: [PATCH 79/93] Move all the turret calculation logic into new graph --- eos/graph/fitDpsVsRange.py | 48 ----------------------------- gui/builtinGraphs/fitDamageStats.py | 17 +++++++++- 2 files changed, 16 insertions(+), 49 deletions(-) diff --git a/eos/graph/fitDpsVsRange.py b/eos/graph/fitDpsVsRange.py index 741a7d2d8..41fa6e25d 100644 --- a/eos/graph/fitDpsVsRange.py +++ b/eos/graph/fitDpsVsRange.py @@ -108,21 +108,6 @@ class FitDpsVsRangeGraph(SmoothGraph): return min(sigRadiusFactor, velocityFactor, 1) - @classmethod - def calculateTurretMultiplier(cls, fit, mod, distance, angle, tgtSpeed, tgtSigRad): - # Source for most of turret calculation info: http://wiki.eveonline.com/en/wiki/Falloff - chanceToHit = cls.calculateTurretChanceToHit(fit, mod, distance, angle, tgtSpeed, tgtSigRad) - if chanceToHit > 0.01: - # AvgDPS = Base Damage * [ ( ChanceToHit^2 + ChanceToHit + 0.0499 ) / 2 ] - multiplier = (chanceToHit ** 2 + chanceToHit + 0.0499) / 2 - else: - # All hits are wreckings - multiplier = chanceToHit * 3 - dmgScaling = mod.getModifiedItemAttr('turretDamageScalingRadius') - if dmgScaling: - multiplier = min(1, (float(tgtSigRad) / dmgScaling) ** 2) - return multiplier - @staticmethod def calculateFighterMissileMultiplier(tgtSpeed, tgtSigRad, ability): prefix = ability.attrPrefix @@ -150,39 +135,6 @@ class FitDpsVsRangeGraph(SmoothGraph): return min(sigRadiusFactor, velocityFactor, 1) - @staticmethod - def calculateTurretChanceToHit(fit, mod, distance, angle, tgtSpeed, tgtSigRad): - tracking = mod.getModifiedItemAttr('trackingSpeed') - turretOptimal = mod.maxRange - turretFalloff = mod.falloff - turretSigRes = mod.getModifiedItemAttr('optimalSigRadius') - transversal = sin(radians(angle)) * tgtSpeed - - # Angular velocity is calculated using range from ship center to target center. - # We do not know target radius but we know attacker radius - angDistance = distance + fit.ship.getModifiedItemAttr('radius', 0) - if angDistance == 0 and transversal == 0: - angularVelocity = 0 - elif angDistance == 0 and transversal != 0: - angularVelocity = inf - else: - angularVelocity = transversal / angDistance - trackingEq = (((angularVelocity / tracking) * - (turretSigRes / tgtSigRad)) ** 2) - rangeEq = ((max(0, distance - turretOptimal)) / turretFalloff) ** 2 - - return 0.5 ** (trackingEq + rangeEq) - - @staticmethod - def calculateModuleMultiplier(mod, distance): - # Simplified formula, we make some assumptions about the module - # This is basically the calculateTurretChanceToHit without tracking values - turretOptimal = mod.maxRange - turretFalloff = mod.falloff - rangeEq = ((max(0, distance - turretOptimal)) / turretFalloff) ** 2 - - return 0.5 ** rangeEq - @staticmethod def penalizeModChain(value, mods): mods.sort(key=lambda v: -abs(v - 1)) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 567d45f5d..f8609ba9e 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -424,10 +424,25 @@ class FitDamageStatsGraph(FitGraph): return xs, ys -def calcTurretCth( +def calcTurretMult(chanceToHit): + # https://wiki.eveuniversity.org/Turret_mechanics#Damage + wreckingChance = min(chanceToHit, 0.01) + wreckingPart = wreckingChance * 3 + normalChance = chanceToHit - wreckingChance + if normalChance > 0: + avgDamageMult = (0.01 + chanceToHit) / 2 + 0.49 + normalPart = normalChance * avgDamageMult + else: + normalPart = 0 + totalMult = normalPart + wreckingPart + return totalMult + + +def calcTurretChanceToHit( atkSpeed, atkAngle, atkRadius, atkOptimalRange, atkFalloffRange, atkTracking, atkOptimalSigRadius, distance, tgtSpeed, tgtAngle, tgtRadius, tgtSigRadius ): + # https://wiki.eveuniversity.org/Turret_mechanics#Hit_Math angularSpeed = calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius) rangeFactor = calcRangeFactor(atkOptimalRange, atkFalloffRange, distance) trackingFactor = calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius) From d3ca0a961e4768d26fc99cba76735b3dd7df4966 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 3 Jul 2019 18:06:33 +0300 Subject: [PATCH 80/93] Implement various functions to calculate damage delivery to specific targets --- gui/builtinGraphs/fitDamageStats.py | 154 ++++++++++++++++++++++++++-- 1 file changed, 143 insertions(+), 11 deletions(-) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index f8609ba9e..4e68376db 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -424,7 +424,99 @@ class FitDamageStatsGraph(FitGraph): return xs, ys -def calcTurretMult(chanceToHit): +def getTurretMult(mod, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius): + cth = _calcTurretChanceToHit( + atkSpeed=atkSpeed, + atkAngle=atkAngle, + atkRadius=fit.ship.getModifiedItemAttr('radius'), + atkOptimalRange=mod.maxRange, + atkFalloffRange=mod.falloff, + atkTracking=mod.getModifiedItemAttr('trackingSpeed'), + atkOptimalSigRadius=mod.getModifiedItemAttr('optimalSigRadius'), + distance=distance, + tgtSpeed=tgtSpeed, + tgtAngle=tgtAngle, + tgtRadius=tgt.ship.getModifiedItemAttr('radius'), + tgtSigRadius=tgtSigRadius) + mult = _calcTurretMult(cth) + return mult + + +def getLauncherMult(mod, fit, distance, tgtSpeed, tgtSigRadius): + mult = _calcMissileMult( + atkRadius=fit.ship.getModifiedItemAttr('radius'), + atkRange=mod.maxRange, + atkEr=mod.getModifiedChargeAttr('aoeCloudSize'), + atkEv=mod.getModifiedChargeAttr('aoeVelocity'), + atkDrf=mod.getModifiedChargeAttr('aoeDamageReductionFactor'), + distance=distance, + tgtSpeed=tgtSpeed, + tgtSigRadius=tgtSigRadius) + return mult + + +def getDroneMult(drone, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius): + if distance > fit.extraAttributes['droneControlRange']: + return 0 + droneSpeed = drone.getModifiedItemAttr('maxVelocity') + # Hard to simulate drone behavior, so assume chance to hit is 1 + # when drone is not sentry and is faster than its target + if droneSpeed > 1 and droneSpeed >= tgtSpeed: + cth = 1 + # Otherwise put the drone into center of the ship, move it at its max speed or ship's speed + # (whichever is lower) towards direction of attacking ship and see how well it projects + else: + droneRadius = drone.getModifiedItemAttr('radius') + cth = _calcTurretChanceToHit( + atkSpeed=min(atkSpeed, droneSpeed), + atkAngle=atkAngle, + atkRadius=droneRadius, + atkOptimalRange=drone.maxRange, + atkFalloffRange=drone.falloff, + atkTracking=drone.getModifiedItemAttr('trackingSpeed'), + atkOptimalSigRadius=drone.getModifiedItemAttr('optimalSigRadius'), + # As distance is ship surface to ship surface, we adjust it according + # to attacker fit's radiuses to have drone surface to ship surface distance + distance=distance + fit.ship.getModifiedItemAttr('radius') - droneRadius, + tgtSpeed=tgtSpeed, + tgtAngle=tgtAngle, + tgtRadius=tgt.ship.getModifiedItemAttr('radius'), + tgtSigRadius=tgtSigRadius) + mult = _calcTurretMult(cth) + return mult + + +def getFighterAbilityMult(fighter, ability, fit, distance, tgtSpeed, tgtSigRadius): + fighterSpeed = fighter.getModifiedItemAttr('maxVelocity') + attrPrefix = ability.attrPrefix + if fighterSpeed >= tgtSpeed: + rangeFactor = 1 + # Same as with drones, if fighters are slower - put them to center of + # the ship and see how they apply + else: + rangeFactor = _calcRangeFactor( + atkOptimalRange=fighter.getModifiedItemAttr('{}RangeOptimal'.format(attrPrefix)), + atkFalloffRange=fighter.getModifiedItemAttr('{}RangeFalloff'.format(attrPrefix)), + distance=distance + fit.ship.getModifiedItemAttr('radius') - fighter.getModifiedItemAttr('radius')) + drf = fighter.getModifiedItemAttr('{}ReductionFactor'.format(attrPrefix), None) + if drf is None: + drf = fighter.getModifiedItemAttr('{}DamageReductionFactor'.format(attrPrefix)) + drs = fighter.getModifiedItemAttr('{}ReductionSensitivity'.format(attrPrefix), None) + if drs is None: + drs = fighter.getModifiedItemAttr('{}DamageReductionSensitivity'.format(attrPrefix)) + missileFactor = _calcMissileFactor( + atkEr=fighter.getModifiedItemAttr('{}ExplosionRadius'.format(attrPrefix)), + atkEv=fighter.getModifiedItemAttr('{}ExplosionVelocity'.format(attrPrefix)), + atkDrf=_calcAggregatedDrf(reductionFactor=drf, reductionSensitivity=drs), + tgtSpeed=tgtSpeed, + tgtSigRadius=tgtSigRadius) + mult = rangeFactor * missileFactor + return mult + + +# Turret-specific +def _calcTurretMult(chanceToHit): + """Calculate damage multiplier for turret-based weapons.""" # https://wiki.eveuniversity.org/Turret_mechanics#Damage wreckingChance = min(chanceToHit, 0.01) wreckingPart = wreckingChance * 3 @@ -438,19 +530,21 @@ def calcTurretMult(chanceToHit): return totalMult -def calcTurretChanceToHit( +def _calcTurretChanceToHit( atkSpeed, atkAngle, atkRadius, atkOptimalRange, atkFalloffRange, atkTracking, atkOptimalSigRadius, distance, tgtSpeed, tgtAngle, tgtRadius, tgtSigRadius ): + """Calculate chance to hit for turret-based weapons.""" # https://wiki.eveuniversity.org/Turret_mechanics#Hit_Math - angularSpeed = calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius) - rangeFactor = calcRangeFactor(atkOptimalRange, atkFalloffRange, distance) - trackingFactor = calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius) + angularSpeed = _calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius) + rangeFactor = _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance) + trackingFactor = _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius) cth = rangeFactor * trackingFactor return cth -def calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius): +def _calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius): + """Calculate angular speed based on mobility parameters of two ships.""" atkAngle = atkAngle * math.pi / 180 tgtAngle = tgtAngle * math.pi / 180 ctcDistance = atkRadius + distance + tgtRadius @@ -463,12 +557,50 @@ def calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle return angularSpeed -def calcRangeFactor(atkOptimalRange, atkFalloffRange, distance): - return 0.5 ** ((max(0, distance - atkOptimalRange) / atkFalloffRange) ** 2) - - -def calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius): +def _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius): + """Calculate tracking chance to hit component.""" return 0.5 ** (((angularSpeed * atkOptimalSigRadius) / (atkTracking * tgtSigRadius)) ** 2) +# Missile-specific +def _calcMissileMult(atkRadius, atkRange, atkEr, atkEv, atkDrf, distance, tgtSpeed, tgtSigRadius): + """Calculate damage multiplier for missile launcher.""" + # Missiles spawn in the center of the attacking ship + if distance + atkRadius > atkRange: + mult = 0 + else: + mult = _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius) + return mult + + +def _calcFighterMult(atkOptimalRange, atkFalloffRange, atkEr, atkEv, atkDrf, distance, tgtSpeed, tgtSigRadius): + """Calculate damage multiplier for separate fighter ability,""" + rangeFactor = _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance) + missileFactor = _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius) + mult = rangeFactor * missileFactor + return mult + + +def _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius): + """Missile application.""" + slowPart = tgtSigRadius / atkEr + fastPart = ((atkEv * tgtSigRadius) / (atkEr * tgtSpeed)) ** atkDrf + totalMult = min(1, slowPart, fastPart) + return totalMult + + +def _calcAggregatedDrf(reductionFactor, reductionSensitivity): + """ + Sometimes DRF is specified as 2 separate numbers, + here we combine them into generic form. + """ + return math.log(reductionFactor) / math.log(reductionSensitivity) + + +# Generic +def _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance): + """Range strength/chance factor, applicable to guns, ewar, RRs, etc.""" + return 0.5 ** ((max(0, distance - atkOptimalRange) / atkFalloffRange) ** 2) + + FitDamageStatsGraph.register() From 4448d7e62f14d9546dc5cd4c40d67e6023164b0a Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 3 Jul 2019 20:03:22 +0300 Subject: [PATCH 81/93] Plug turrets and missiles into dps vs range calculation --- gui/builtinGraphs/fitDamageStats.py | 42 +++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats.py index 4e68376db..470d0496b 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats.py @@ -22,6 +22,8 @@ import math from copy import copy from itertools import chain +import eos.config +from eos.const import FittingHardpoint, FittingModuleState from eos.utils.float import floatUnerr from eos.utils.spoolSupport import SpoolType, SpoolOptions from eos.utils.stats import DmgTypes @@ -66,7 +68,40 @@ class FitDamageStatsGraph(FitGraph): ('tgtSigRad', '%'): lambda v, fit, tgt: v * 100 / fit.ship.getModifiedItemAttr('signatureRadius')} def _distance2dps(self, mainInput, miscInputs, fit, tgt): - return [], [] + xs = [] + ys = [] + defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage'] + miscInputMap = dict(miscInputs) + tgtSigRad = miscInputMap.get('tgtSigRad', tgt.ship.getModifiedItemAttr('signatureRadius')) + for distance in self._iterLinear(mainInput[1]): + totalDps = 0 + for mod in fit.modules: + if not mod.isDealingDamage(): + continue + modDps = mod.getDps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)).total + if mod.hardpoint == FittingHardpoint.TURRET: + if mod.state >= FittingModuleState.ACTIVE: + totalDps += modDps * getTurretMult( + mod=mod, + fit=fit, + tgt=tgt, + atkSpeed=miscInputMap['atkSpeed'], + atkAngle=miscInputMap['atkAngle'], + distance=distance, + tgtSpeed=miscInputMap['tgtSpeed'], + tgtAngle=miscInputMap['tgtAngle'], + tgtSigRadius=tgtSigRad) + elif mod.hardpoint == FittingHardpoint.MISSILE: + if mod.state >= FittingModuleState.ACTIVE: + totalDps += modDps * getLauncherMult( + mod=mod, + fit=fit, + distance=miscInputMap['distance'], + tgtSpeed=miscInputMap['tgtSpeed'], + tgtSigRadius=tgtSigRad) + xs.append(distance) + ys.append(totalDps) + return xs, ys def _distance2volley(self, mainInput, miscInputs, fit, tgt): return [], [] @@ -443,9 +478,12 @@ def getTurretMult(mod, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngl def getLauncherMult(mod, fit, distance, tgtSpeed, tgtSigRadius): + modRange = mod.maxRange + if modRange is None: + return 0 mult = _calcMissileMult( atkRadius=fit.ship.getModifiedItemAttr('radius'), - atkRange=mod.maxRange, + atkRange=modRange, atkEr=mod.getModifiedChargeAttr('aoeCloudSize'), atkEv=mod.getModifiedChargeAttr('aoeVelocity'), atkDrf=mod.getModifiedChargeAttr('aoeDamageReductionFactor'), From 585741328501ade89ad542eddce7daec72eae7fd Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Wed, 3 Jul 2019 20:07:19 +0300 Subject: [PATCH 82/93] Get rid of float error when changing vector length via scrolling --- gui/graphFrame/vector.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gui/graphFrame/vector.py b/gui/graphFrame/vector.py index 7b4a278ce..fdb8f1673 100644 --- a/gui/graphFrame/vector.py +++ b/gui/graphFrame/vector.py @@ -23,6 +23,8 @@ import math # noinspection PyPackageRequirements import wx +from eos.utils.float import floatUnerr + class VectorPicker(wx.Window): @@ -193,7 +195,7 @@ class VectorPicker(wx.Window): def OnWheel(self, event): amount = 0.1 * event.GetWheelRotation() / event.GetWheelDelta() - self._length = min(max(self._length + amount, 0.0), 1.0) + self._length = floatUnerr(min(max(self._length + amount, 0.0), 1.0)) self.Refresh() self.SendChangeEvent() From ae110371fe001c9f2dfa6b0fd7710ed1ba16cacc Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 4 Jul 2019 09:11:55 +0300 Subject: [PATCH 83/93] Split up dps graph file a little --- gui/builtinGraphs/fitDamageStats/__init__.py | 1 + gui/builtinGraphs/fitDamageStats/cacheTime.py | 24 ++ gui/builtinGraphs/fitDamageStats/calc.py | 207 ++++++++++++++++++ .../graph.py} | 188 +--------------- 4 files changed, 235 insertions(+), 185 deletions(-) create mode 100644 gui/builtinGraphs/fitDamageStats/__init__.py create mode 100644 gui/builtinGraphs/fitDamageStats/cacheTime.py create mode 100644 gui/builtinGraphs/fitDamageStats/calc.py rename gui/builtinGraphs/{fitDamageStats.py => fitDamageStats/graph.py} (72%) diff --git a/gui/builtinGraphs/fitDamageStats/__init__.py b/gui/builtinGraphs/fitDamageStats/__init__.py new file mode 100644 index 000000000..b01f971ca --- /dev/null +++ b/gui/builtinGraphs/fitDamageStats/__init__.py @@ -0,0 +1 @@ +import gui.builtinGraphs.fitDamageStats.graph # noqa: E402,F401 diff --git a/gui/builtinGraphs/fitDamageStats/cacheTime.py b/gui/builtinGraphs/fitDamageStats/cacheTime.py new file mode 100644 index 000000000..b711ecadb --- /dev/null +++ b/gui/builtinGraphs/fitDamageStats/cacheTime.py @@ -0,0 +1,24 @@ +# ============================================================================= +# Copyright (C) 2010 Diego Duclos +# +# 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 . +# ============================================================================= + + +class TimeCache: + + def __init__(self): + self.data = {} diff --git a/gui/builtinGraphs/fitDamageStats/calc.py b/gui/builtinGraphs/fitDamageStats/calc.py new file mode 100644 index 000000000..fe7dc1124 --- /dev/null +++ b/gui/builtinGraphs/fitDamageStats/calc.py @@ -0,0 +1,207 @@ +# ============================================================================= +# Copyright (C) 2010 Diego Duclos +# +# 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 . +# ============================================================================= + + +import math + + +def getTurretMult(mod, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius): + cth = _calcTurretChanceToHit( + atkSpeed=atkSpeed, + atkAngle=atkAngle, + atkRadius=fit.ship.getModifiedItemAttr('radius'), + atkOptimalRange=mod.maxRange, + atkFalloffRange=mod.falloff, + atkTracking=mod.getModifiedItemAttr('trackingSpeed'), + atkOptimalSigRadius=mod.getModifiedItemAttr('optimalSigRadius'), + distance=distance, + tgtSpeed=tgtSpeed, + tgtAngle=tgtAngle, + tgtRadius=tgt.ship.getModifiedItemAttr('radius'), + tgtSigRadius=tgtSigRadius) + mult = _calcTurretMult(cth) + return mult + + +def getLauncherMult(mod, fit, distance, tgtSpeed, tgtSigRadius): + modRange = mod.maxRange + if modRange is None: + return 0 + mult = _calcMissileMult( + atkRadius=fit.ship.getModifiedItemAttr('radius'), + atkRange=modRange, + atkEr=mod.getModifiedChargeAttr('aoeCloudSize'), + atkEv=mod.getModifiedChargeAttr('aoeVelocity'), + atkDrf=mod.getModifiedChargeAttr('aoeDamageReductionFactor'), + distance=distance, + tgtSpeed=tgtSpeed, + tgtSigRadius=tgtSigRadius) + return mult + + +def getDroneMult(drone, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius): + if distance > fit.extraAttributes['droneControlRange']: + return 0 + droneSpeed = drone.getModifiedItemAttr('maxVelocity') + # Hard to simulate drone behavior, so assume chance to hit is 1 + # when drone is not sentry and is faster than its target + if droneSpeed > 1 and droneSpeed >= tgtSpeed: + cth = 1 + # Otherwise put the drone into center of the ship, move it at its max speed or ship's speed + # (whichever is lower) towards direction of attacking ship and see how well it projects + else: + droneRadius = drone.getModifiedItemAttr('radius') + cth = _calcTurretChanceToHit( + atkSpeed=min(atkSpeed, droneSpeed), + atkAngle=atkAngle, + atkRadius=droneRadius, + atkOptimalRange=drone.maxRange, + atkFalloffRange=drone.falloff, + atkTracking=drone.getModifiedItemAttr('trackingSpeed'), + atkOptimalSigRadius=drone.getModifiedItemAttr('optimalSigRadius'), + # As distance is ship surface to ship surface, we adjust it according + # to attacker fit's radiuses to have drone surface to ship surface distance + distance=distance + fit.ship.getModifiedItemAttr('radius') - droneRadius, + tgtSpeed=tgtSpeed, + tgtAngle=tgtAngle, + tgtRadius=tgt.ship.getModifiedItemAttr('radius'), + tgtSigRadius=tgtSigRadius) + mult = _calcTurretMult(cth) + return mult + + +def getFighterAbilityMult(fighter, ability, fit, distance, tgtSpeed, tgtSigRadius): + fighterSpeed = fighter.getModifiedItemAttr('maxVelocity') + attrPrefix = ability.attrPrefix + if fighterSpeed >= tgtSpeed: + rangeFactor = 1 + # Same as with drones, if fighters are slower - put them to center of + # the ship and see how they apply + else: + rangeFactor = _calcRangeFactor( + atkOptimalRange=fighter.getModifiedItemAttr('{}RangeOptimal'.format(attrPrefix)), + atkFalloffRange=fighter.getModifiedItemAttr('{}RangeFalloff'.format(attrPrefix)), + distance=distance + fit.ship.getModifiedItemAttr('radius') - fighter.getModifiedItemAttr('radius')) + drf = fighter.getModifiedItemAttr('{}ReductionFactor'.format(attrPrefix), None) + if drf is None: + drf = fighter.getModifiedItemAttr('{}DamageReductionFactor'.format(attrPrefix)) + drs = fighter.getModifiedItemAttr('{}ReductionSensitivity'.format(attrPrefix), None) + if drs is None: + drs = fighter.getModifiedItemAttr('{}DamageReductionSensitivity'.format(attrPrefix)) + missileFactor = _calcMissileFactor( + atkEr=fighter.getModifiedItemAttr('{}ExplosionRadius'.format(attrPrefix)), + atkEv=fighter.getModifiedItemAttr('{}ExplosionVelocity'.format(attrPrefix)), + atkDrf=_calcAggregatedDrf(reductionFactor=drf, reductionSensitivity=drs), + tgtSpeed=tgtSpeed, + tgtSigRadius=tgtSigRadius) + mult = rangeFactor * missileFactor + return mult + + +# Turret-specific +def _calcTurretMult(chanceToHit): + """Calculate damage multiplier for turret-based weapons.""" + # https://wiki.eveuniversity.org/Turret_mechanics#Damage + wreckingChance = min(chanceToHit, 0.01) + wreckingPart = wreckingChance * 3 + normalChance = chanceToHit - wreckingChance + if normalChance > 0: + avgDamageMult = (0.01 + chanceToHit) / 2 + 0.49 + normalPart = normalChance * avgDamageMult + else: + normalPart = 0 + totalMult = normalPart + wreckingPart + return totalMult + + +def _calcTurretChanceToHit( + atkSpeed, atkAngle, atkRadius, atkOptimalRange, atkFalloffRange, atkTracking, atkOptimalSigRadius, + distance, tgtSpeed, tgtAngle, tgtRadius, tgtSigRadius +): + """Calculate chance to hit for turret-based weapons.""" + # https://wiki.eveuniversity.org/Turret_mechanics#Hit_Math + angularSpeed = _calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius) + rangeFactor = _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance) + trackingFactor = _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius) + cth = rangeFactor * trackingFactor + return cth + + +def _calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius): + """Calculate angular speed based on mobility parameters of two ships.""" + atkAngle = atkAngle * math.pi / 180 + tgtAngle = tgtAngle * math.pi / 180 + ctcDistance = atkRadius + distance + tgtRadius + # Target is to the right of the attacker, so transversal is projection onto Y axis + transSpeed = abs(atkSpeed * math.sin(atkAngle) - tgtSpeed * math.sin(tgtAngle)) + if ctcDistance == 0: + angularSpeed = 0 if transSpeed == 0 else math.inf + else: + angularSpeed = transSpeed / ctcDistance + return angularSpeed + + +def _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius): + """Calculate tracking chance to hit component.""" + return 0.5 ** (((angularSpeed * atkOptimalSigRadius) / (atkTracking * tgtSigRadius)) ** 2) + + +# Missile-specific +def _calcMissileMult(atkRadius, atkRange, atkEr, atkEv, atkDrf, distance, tgtSpeed, tgtSigRadius): + """Calculate damage multiplier for missile launcher.""" + # Missiles spawn in the center of the attacking ship + if distance + atkRadius > atkRange: + mult = 0 + else: + mult = _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius) + return mult + + +def _calcFighterMult(atkOptimalRange, atkFalloffRange, atkEr, atkEv, atkDrf, distance, tgtSpeed, tgtSigRadius): + """Calculate damage multiplier for separate fighter ability,""" + rangeFactor = _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance) + missileFactor = _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius) + mult = rangeFactor * missileFactor + return mult + + +def _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius): + """Missile application.""" + factors = [1] + # "Slow" part + factors.append(tgtSigRadius / atkEr) + # "Fast" part + if tgtSpeed > 0: + factors.append(((atkEv * tgtSigRadius) / (atkEr * tgtSpeed)) ** atkDrf) + totalMult = min(factors) + return totalMult + + +def _calcAggregatedDrf(reductionFactor, reductionSensitivity): + """ + Sometimes DRF is specified as 2 separate numbers, + here we combine them into generic form. + """ + return math.log(reductionFactor) / math.log(reductionSensitivity) + + +# Generic +def _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance): + """Range strength/chance factor, applicable to guns, ewar, RRs, etc.""" + return 0.5 ** ((max(0, distance - atkOptimalRange) / atkFalloffRange) ** 2) diff --git a/gui/builtinGraphs/fitDamageStats.py b/gui/builtinGraphs/fitDamageStats/graph.py similarity index 72% rename from gui/builtinGraphs/fitDamageStats.py rename to gui/builtinGraphs/fitDamageStats/graph.py index 470d0496b..1ef75fd1c 100644 --- a/gui/builtinGraphs/fitDamageStats.py +++ b/gui/builtinGraphs/fitDamageStats/graph.py @@ -18,7 +18,6 @@ # ============================================================================= -import math from copy import copy from itertools import chain @@ -27,7 +26,8 @@ from eos.const import FittingHardpoint, FittingModuleState from eos.utils.float import floatUnerr from eos.utils.spoolSupport import SpoolType, SpoolOptions from eos.utils.stats import DmgTypes -from .base import FitGraph, XDef, YDef, Input, VectorDef +from gui.builtinGraphs.base import FitGraph, XDef, YDef, Input, VectorDef +from .calc import getTurretMult, getLauncherMult, getDroneMult, getFighterAbilityMult class FitDamageStatsGraph(FitGraph): @@ -96,7 +96,7 @@ class FitDamageStatsGraph(FitGraph): totalDps += modDps * getLauncherMult( mod=mod, fit=fit, - distance=miscInputMap['distance'], + distance=distance, tgtSpeed=miscInputMap['tgtSpeed'], tgtSigRadius=tgtSigRad) xs.append(distance) @@ -459,186 +459,4 @@ class FitDamageStatsGraph(FitGraph): return xs, ys -def getTurretMult(mod, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius): - cth = _calcTurretChanceToHit( - atkSpeed=atkSpeed, - atkAngle=atkAngle, - atkRadius=fit.ship.getModifiedItemAttr('radius'), - atkOptimalRange=mod.maxRange, - atkFalloffRange=mod.falloff, - atkTracking=mod.getModifiedItemAttr('trackingSpeed'), - atkOptimalSigRadius=mod.getModifiedItemAttr('optimalSigRadius'), - distance=distance, - tgtSpeed=tgtSpeed, - tgtAngle=tgtAngle, - tgtRadius=tgt.ship.getModifiedItemAttr('radius'), - tgtSigRadius=tgtSigRadius) - mult = _calcTurretMult(cth) - return mult - - -def getLauncherMult(mod, fit, distance, tgtSpeed, tgtSigRadius): - modRange = mod.maxRange - if modRange is None: - return 0 - mult = _calcMissileMult( - atkRadius=fit.ship.getModifiedItemAttr('radius'), - atkRange=modRange, - atkEr=mod.getModifiedChargeAttr('aoeCloudSize'), - atkEv=mod.getModifiedChargeAttr('aoeVelocity'), - atkDrf=mod.getModifiedChargeAttr('aoeDamageReductionFactor'), - distance=distance, - tgtSpeed=tgtSpeed, - tgtSigRadius=tgtSigRadius) - return mult - - -def getDroneMult(drone, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius): - if distance > fit.extraAttributes['droneControlRange']: - return 0 - droneSpeed = drone.getModifiedItemAttr('maxVelocity') - # Hard to simulate drone behavior, so assume chance to hit is 1 - # when drone is not sentry and is faster than its target - if droneSpeed > 1 and droneSpeed >= tgtSpeed: - cth = 1 - # Otherwise put the drone into center of the ship, move it at its max speed or ship's speed - # (whichever is lower) towards direction of attacking ship and see how well it projects - else: - droneRadius = drone.getModifiedItemAttr('radius') - cth = _calcTurretChanceToHit( - atkSpeed=min(atkSpeed, droneSpeed), - atkAngle=atkAngle, - atkRadius=droneRadius, - atkOptimalRange=drone.maxRange, - atkFalloffRange=drone.falloff, - atkTracking=drone.getModifiedItemAttr('trackingSpeed'), - atkOptimalSigRadius=drone.getModifiedItemAttr('optimalSigRadius'), - # As distance is ship surface to ship surface, we adjust it according - # to attacker fit's radiuses to have drone surface to ship surface distance - distance=distance + fit.ship.getModifiedItemAttr('radius') - droneRadius, - tgtSpeed=tgtSpeed, - tgtAngle=tgtAngle, - tgtRadius=tgt.ship.getModifiedItemAttr('radius'), - tgtSigRadius=tgtSigRadius) - mult = _calcTurretMult(cth) - return mult - - -def getFighterAbilityMult(fighter, ability, fit, distance, tgtSpeed, tgtSigRadius): - fighterSpeed = fighter.getModifiedItemAttr('maxVelocity') - attrPrefix = ability.attrPrefix - if fighterSpeed >= tgtSpeed: - rangeFactor = 1 - # Same as with drones, if fighters are slower - put them to center of - # the ship and see how they apply - else: - rangeFactor = _calcRangeFactor( - atkOptimalRange=fighter.getModifiedItemAttr('{}RangeOptimal'.format(attrPrefix)), - atkFalloffRange=fighter.getModifiedItemAttr('{}RangeFalloff'.format(attrPrefix)), - distance=distance + fit.ship.getModifiedItemAttr('radius') - fighter.getModifiedItemAttr('radius')) - drf = fighter.getModifiedItemAttr('{}ReductionFactor'.format(attrPrefix), None) - if drf is None: - drf = fighter.getModifiedItemAttr('{}DamageReductionFactor'.format(attrPrefix)) - drs = fighter.getModifiedItemAttr('{}ReductionSensitivity'.format(attrPrefix), None) - if drs is None: - drs = fighter.getModifiedItemAttr('{}DamageReductionSensitivity'.format(attrPrefix)) - missileFactor = _calcMissileFactor( - atkEr=fighter.getModifiedItemAttr('{}ExplosionRadius'.format(attrPrefix)), - atkEv=fighter.getModifiedItemAttr('{}ExplosionVelocity'.format(attrPrefix)), - atkDrf=_calcAggregatedDrf(reductionFactor=drf, reductionSensitivity=drs), - tgtSpeed=tgtSpeed, - tgtSigRadius=tgtSigRadius) - mult = rangeFactor * missileFactor - return mult - - -# Turret-specific -def _calcTurretMult(chanceToHit): - """Calculate damage multiplier for turret-based weapons.""" - # https://wiki.eveuniversity.org/Turret_mechanics#Damage - wreckingChance = min(chanceToHit, 0.01) - wreckingPart = wreckingChance * 3 - normalChance = chanceToHit - wreckingChance - if normalChance > 0: - avgDamageMult = (0.01 + chanceToHit) / 2 + 0.49 - normalPart = normalChance * avgDamageMult - else: - normalPart = 0 - totalMult = normalPart + wreckingPart - return totalMult - - -def _calcTurretChanceToHit( - atkSpeed, atkAngle, atkRadius, atkOptimalRange, atkFalloffRange, atkTracking, atkOptimalSigRadius, - distance, tgtSpeed, tgtAngle, tgtRadius, tgtSigRadius -): - """Calculate chance to hit for turret-based weapons.""" - # https://wiki.eveuniversity.org/Turret_mechanics#Hit_Math - angularSpeed = _calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius) - rangeFactor = _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance) - trackingFactor = _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius) - cth = rangeFactor * trackingFactor - return cth - - -def _calcAngularSpeed(atkSpeed, atkAngle, atkRadius, distance, tgtSpeed, tgtAngle, tgtRadius): - """Calculate angular speed based on mobility parameters of two ships.""" - atkAngle = atkAngle * math.pi / 180 - tgtAngle = tgtAngle * math.pi / 180 - ctcDistance = atkRadius + distance + tgtRadius - # Target is to the right of the attacker, so transversal is projection onto Y axis - transSpeed = abs(atkSpeed * math.sin(atkAngle) - tgtSpeed * math.sin(tgtAngle)) - if ctcDistance == 0: - angularSpeed = 0 if transSpeed == 0 else math.inf - else: - angularSpeed = transSpeed / ctcDistance - return angularSpeed - - -def _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRadius): - """Calculate tracking chance to hit component.""" - return 0.5 ** (((angularSpeed * atkOptimalSigRadius) / (atkTracking * tgtSigRadius)) ** 2) - - -# Missile-specific -def _calcMissileMult(atkRadius, atkRange, atkEr, atkEv, atkDrf, distance, tgtSpeed, tgtSigRadius): - """Calculate damage multiplier for missile launcher.""" - # Missiles spawn in the center of the attacking ship - if distance + atkRadius > atkRange: - mult = 0 - else: - mult = _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius) - return mult - - -def _calcFighterMult(atkOptimalRange, atkFalloffRange, atkEr, atkEv, atkDrf, distance, tgtSpeed, tgtSigRadius): - """Calculate damage multiplier for separate fighter ability,""" - rangeFactor = _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance) - missileFactor = _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius) - mult = rangeFactor * missileFactor - return mult - - -def _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius): - """Missile application.""" - slowPart = tgtSigRadius / atkEr - fastPart = ((atkEv * tgtSigRadius) / (atkEr * tgtSpeed)) ** atkDrf - totalMult = min(1, slowPart, fastPart) - return totalMult - - -def _calcAggregatedDrf(reductionFactor, reductionSensitivity): - """ - Sometimes DRF is specified as 2 separate numbers, - here we combine them into generic form. - """ - return math.log(reductionFactor) / math.log(reductionSensitivity) - - -# Generic -def _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance): - """Range strength/chance factor, applicable to guns, ewar, RRs, etc.""" - return 0.5 ** ((max(0, distance - atkOptimalRange) / atkFalloffRange) ** 2) - - FitDamageStatsGraph.register() From 193fcc60d8b1efff29569df76d6c6745122f32ad Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 4 Jul 2019 09:36:31 +0300 Subject: [PATCH 84/93] Split time cache into separate file as well --- gui/builtinGraphs/base.py | 10 +- gui/builtinGraphs/fitDamageStats/cacheTime.py | 24 -- gui/builtinGraphs/fitDamageStats/graph.py | 274 +----------------- gui/builtinGraphs/fitDamageStats/timeCache.py | 215 ++++++++++++++ 4 files changed, 231 insertions(+), 292 deletions(-) delete mode 100644 gui/builtinGraphs/fitDamageStats/cacheTime.py create mode 100644 gui/builtinGraphs/fitDamageStats/timeCache.py diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index 1c651dc35..cada75c4e 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -40,8 +40,6 @@ class FitGraph(metaclass=ABCMeta): def __init__(self): # Format: {(fit ID, target type, target ID): data} self._plotCache = {} - # Format: {fit ID: data} - self._calcCache = {} @property @abstractmethod @@ -91,7 +89,6 @@ class FitGraph(metaclass=ABCMeta): # Clear everything if fitID is None: self._plotCache.clear() - self._calcCache.clear() return # Clear plot cache plotKeysToClear = set() @@ -103,9 +100,10 @@ class FitGraph(metaclass=ABCMeta): plotKeysToClear.add(cacheKey) for cacheKey in plotKeysToClear: del self._plotCache[cacheKey] - # Clear calc cache - if fitID in self._calcCache: - del self._calcCache[fitID] + self._clearInternalCache(fitID=fitID) + + def _clearInternalCache(self, fitID): + return # Calculation stuff def _calcPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt): diff --git a/gui/builtinGraphs/fitDamageStats/cacheTime.py b/gui/builtinGraphs/fitDamageStats/cacheTime.py deleted file mode 100644 index b711ecadb..000000000 --- a/gui/builtinGraphs/fitDamageStats/cacheTime.py +++ /dev/null @@ -1,24 +0,0 @@ -# ============================================================================= -# Copyright (C) 2010 Diego Duclos -# -# 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 . -# ============================================================================= - - -class TimeCache: - - def __init__(self): - self.data = {} diff --git a/gui/builtinGraphs/fitDamageStats/graph.py b/gui/builtinGraphs/fitDamageStats/graph.py index 1ef75fd1c..3172a4701 100644 --- a/gui/builtinGraphs/fitDamageStats/graph.py +++ b/gui/builtinGraphs/fitDamageStats/graph.py @@ -18,20 +18,24 @@ # ============================================================================= -from copy import copy -from itertools import chain - import eos.config from eos.const import FittingHardpoint, FittingModuleState from eos.utils.float import floatUnerr from eos.utils.spoolSupport import SpoolType, SpoolOptions -from eos.utils.stats import DmgTypes from gui.builtinGraphs.base import FitGraph, XDef, YDef, Input, VectorDef from .calc import getTurretMult, getLauncherMult, getDroneMult, getFighterAbilityMult +from .timeCache import TimeCache class FitDamageStatsGraph(FitGraph): + def __init__(self): + super().__init__() + self._timeCache = TimeCache() + + def _clearInternalCache(self, fitID): + self._timeCache.clear(fitID) + # UI stuff name = 'Damage Stats' xDefs = [ @@ -112,19 +116,19 @@ class FitDamageStatsGraph(FitGraph): def _time2dps(self, mainInput, miscInputs, fit, tgt): def calcDpsTmp(timeDmg): return floatUnerr(sum(dts[0].total for dts in timeDmg.values())) - self._generateTimeCacheDpsVolley(fit, mainInput[1][1]) + self._timeCache.generateFinalFormDpsVolley(fit, mainInput[1][1]) return self._composeTimeGraph(mainInput, fit, 'finalDpsVolley', calcDpsTmp) def _time2volley(self, mainInput, miscInputs, fit, tgt): def calcVolleyTmp(timeDmg): return floatUnerr(sum(dts[1].total for dts in timeDmg.values())) - self._generateTimeCacheDpsVolley(fit, mainInput[1][1]) + self._timeCache.generateFinalFormDpsVolley(fit, mainInput[1][1]) return self._composeTimeGraph(mainInput, fit, 'finalDpsVolley', calcVolleyTmp) def _time2damage(self, mainInput, miscInputs, fit, tgt): def calcDamageTmp(timeDmg): return floatUnerr(sum(dt.total for dt in timeDmg.values())) - self._generateTimeCacheDmg(fit, mainInput[1][1]) + self._timeCache.generateFinalFormDmg(fit, mainInput[1][1]) return self._composeTimeGraph(mainInput, fit, 'finalDmg', calcDamageTmp) def _tgtSpeed2dps(self, mainInput, miscInputs, fit, tgt): @@ -159,266 +163,12 @@ class FitDamageStatsGraph(FitGraph): ('tgtSigRad', 'volley'): _tgtSigRad2volley, ('tgtSigRad', 'damage'): _tgtSigRad2damage} - # Cache generation - def _generateTimeCacheDpsVolley(self, fit, maxTime): - # Time is none means that time parameter has to be ignored, - # we do not need cache for that - if maxTime is None: - return True - self._generateTimeCacheIntermediate(fit, maxTime) - timeCache = self._calcCache[fit.ID]['timeCache'] - # Final cache has been generated already, don't do anything - if 'finalDpsVolley' in timeCache: - return - # Convert cache from segments with assigned values into points - # which are located at times when dps/volley values change - pointCache = {} - for key, dmgList in timeCache['intermediateDpsVolley'].items(): - pointData = pointCache[key] = {} - prevDps = None - prevVolley = None - prevTimeEnd = None - for timeStart, timeEnd, dps, volley in dmgList: - # First item - if not pointData: - pointData[timeStart] = (dps, volley) - # Gap between items - elif floatUnerr(prevTimeEnd) < floatUnerr(timeStart): - pointData[prevTimeEnd] = (DmgTypes(0, 0, 0, 0), DmgTypes(0, 0, 0, 0)) - pointData[timeStart] = (dps, volley) - # Changed value - elif dps != prevDps or volley != prevVolley: - pointData[timeStart] = (dps, volley) - prevDps = dps - prevVolley = volley - prevTimeEnd = timeEnd - # We have another intermediate form, do not need old one any longer - del timeCache['intermediateDpsVolley'] - changesByTime = {} - for key, dmgMap in pointCache.items(): - for time in dmgMap: - changesByTime.setdefault(time, []).append(key) - # Here we convert cache to following format: - # {time: {key: (dps, volley}} - finalCache = timeCache['finalDpsVolley'] = {} - timeDmgData = {} - for time in sorted(changesByTime): - timeDmgData = copy(timeDmgData) - for key in changesByTime[time]: - timeDmgData[key] = pointCache[key][time] - finalCache[time] = timeDmgData - - def _generateTimeCacheDmg(self, fit, maxTime): - # Time is none means that time parameter has to be ignored, - # we do not need cache for that - if maxTime is None: - return - self._generateTimeCacheIntermediate(fit, maxTime) - timeCache = self._calcCache[fit.ID]['timeCache'] - # Final cache has been generated already, don't do anything - if 'finalDmg' in timeCache: - return - intCache = timeCache['intermediateDmg'] - changesByTime = {} - for key, dmgMap in intCache.items(): - for time in dmgMap: - changesByTime.setdefault(time, []).append(key) - # Here we convert cache to following format: - # {time: {key: damage done by key at this time}} - finalCache = timeCache['finalDmg'] = {} - timeDmgData = {} - for time in sorted(changesByTime): - timeDmgData = copy(timeDmgData) - for key in changesByTime[time]: - keyDmg = intCache[key][time] - if key in timeDmgData: - timeDmgData[key] = timeDmgData[key] + keyDmg - else: - timeDmgData[key] = keyDmg - finalCache[time] = timeDmgData - # We do not need intermediate cache once we have final - del timeCache['intermediateDmg'] - - def _generateTimeCacheIntermediate(self, fit, maxTime): - if self._isTimeCacheValid(fit, maxTime): - return - timeCache = self._calcCache.setdefault(fit.ID, {})['timeCache'] = {'maxTime': maxTime} - intCacheDpsVolley = timeCache['intermediateDpsVolley'] = {} - intCacheDmg = timeCache['intermediateDmg'] = {} - - def addDpsVolley(ddKey, addedTimeStart, addedTimeFinish, addedVolleys): - if not addedVolleys: - return - volleySum = sum(addedVolleys, DmgTypes(0, 0, 0, 0)) - if volleySum.total > 0: - addedDps = volleySum / (addedTimeFinish - addedTimeStart) - # We can take "just best" volley, no matter target resistances, because all - # known items have the same damage type ratio throughout their cycle - and - # applying resistances doesn't change final outcome - bestVolley = max(addedVolleys, key=lambda v: v.total) - ddCacheDps = intCacheDpsVolley.setdefault(ddKey, []) - ddCacheDps.append((addedTimeStart, addedTimeFinish, addedDps, bestVolley)) - - def addDmg(ddKey, addedTime, addedDmg): - if addedDmg.total == 0: - return - intCacheDmg.setdefault(ddKey, {})[addedTime] = addedDmg - - # Modules - for mod in fit.modules: - if not mod.isDealingDamage(): - continue - cycleParams = mod.getCycleParameters(reloadOverride=True) - if cycleParams is None: - continue - currentTime = 0 - nonstopCycles = 0 - for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles(): - cycleVolleys = [] - volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True)) - for volleyTimeMs, volley in volleyParams.items(): - cycleVolleys.append(volley) - addDmg(mod, currentTime + volleyTimeMs / 1000, volley) - addDpsVolley(mod, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys) - if inactiveTimeMs > 0: - nonstopCycles = 0 - else: - nonstopCycles += 1 - if currentTime > maxTime: - break - currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 - # Drones - for drone in fit.drones: - if not drone.isDealingDamage(): - continue - cycleParams = drone.getCycleParameters(reloadOverride=True) - if cycleParams is None: - continue - currentTime = 0 - volleyParams = drone.getVolleyParameters() - for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles(): - cycleVolleys = [] - for volleyTimeMs, volley in volleyParams.items(): - cycleVolleys.append(volley) - addDmg(drone, currentTime + volleyTimeMs / 1000, volley) - addDpsVolley(drone, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys) - if currentTime > maxTime: - break - currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 - # Fighters - for fighter in fit.fighters: - if not fighter.isDealingDamage(): - continue - cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True) - if cycleParams is None: - continue - volleyParams = fighter.getVolleyParametersPerEffect() - for effectID, abilityCycleParams in cycleParams.items(): - if effectID not in volleyParams: - continue - currentTime = 0 - abilityVolleyParams = volleyParams[effectID] - for cycleTimeMs, inactiveTimeMs in abilityCycleParams.iterCycles(): - cycleVolleys = [] - for volleyTimeMs, volley in abilityVolleyParams.items(): - cycleVolleys.append(volley) - addDmg((fighter, effectID), currentTime + volleyTimeMs / 1000, volley) - addDpsVolley((fighter, effectID), currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys) - if currentTime > maxTime: - break - currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 - - def _isTimeCacheValid(self, fit, maxTime): - try: - cacheMaxTime = self._calcCache[fit.ID]['timeCache']['maxTime'] - except KeyError: - return False - return maxTime <= cacheMaxTime - - def _generateTimeCacheDps(self, fit, maxTime): - if fit.ID in self._calcCache and 'timeDps' in self._calcCache[fit.ID]: - return - intermediateCache = [] - - def addDmg(addedTimeStart, addedTimeFinish, addedDmg): - if addedDmg == 0: - return - addedDps = addedDmg / (addedTimeFinish - addedTimeStart) - intermediateCache.append((addedTimeStart, addedTimeFinish, addedDps)) - - for mod in fit.modules: - if not mod.isDealingDamage(): - continue - cycleParams = mod.getCycleParameters(reloadOverride=True) - if cycleParams is None: - continue - currentTime = 0 - nonstopCycles = 0 - for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles(): - cycleDamage = 0 - volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True)) - for volleyTimeMs, volley in volleyParams.items(): - cycleDamage += volley.total - addDmg(currentTime, currentTime + cycleTimeMs / 1000, cycleDamage) - currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 - if inactiveTimeMs > 0: - nonstopCycles = 0 - else: - nonstopCycles += 1 - if currentTime > maxTime: - break - for drone in fit.drones: - if not drone.isDealingDamage(): - continue - cycleParams = drone.getCycleParameters(reloadOverride=True) - if cycleParams is None: - continue - currentTime = 0 - for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles(): - cycleDamage = 0 - volleyParams = drone.getVolleyParameters() - for volleyTimeMs, volley in volleyParams.items(): - cycleDamage += volley.total - addDmg(currentTime, currentTime + cycleTimeMs / 1000, cycleDamage) - currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 - if currentTime > maxTime: - break - for fighter in fit.fighters: - if not fighter.isDealingDamage(): - continue - cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True) - if cycleParams is None: - continue - volleyParams = fighter.getVolleyParametersPerEffect() - for effectID, abilityCycleParams in cycleParams.items(): - if effectID not in volleyParams: - continue - abilityVolleyParams = volleyParams[effectID] - currentTime = 0 - for cycleTimeMs, inactiveTimeMs in abilityCycleParams.iterCycles(): - cycleDamage = 0 - for volleyTimeMs, volley in abilityVolleyParams.items(): - cycleDamage += volley.total - addDmg(currentTime, currentTime + cycleTimeMs / 1000, cycleDamage) - currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 - if currentTime > maxTime: - break - - # Post-process cache - finalCache = {} - for time in sorted(set(chain((i[0] for i in intermediateCache), (i[1] for i in intermediateCache)))): - entries = (e for e in intermediateCache if e[0] <= time < e[1]) - dps = sum(e[2] for e in entries) - finalCache[time] = dps - fitCache = self._calcCache.setdefault(fit.ID, {}) - fitCache['timeDps'] = finalCache - def _composeTimeGraph(self, mainInput, fit, cacheName, calcFunc): xs = [] ys = [] minTime, maxTime = mainInput[1] - cache = self._calcCache[fit.ID]['timeCache'][cacheName] + cache = self._timeCache.getData(fit.ID, cacheName) currentDps = None currentTime = None for currentTime in sorted(cache): diff --git a/gui/builtinGraphs/fitDamageStats/timeCache.py b/gui/builtinGraphs/fitDamageStats/timeCache.py new file mode 100644 index 000000000..8a8a905ab --- /dev/null +++ b/gui/builtinGraphs/fitDamageStats/timeCache.py @@ -0,0 +1,215 @@ +# ============================================================================= +# Copyright (C) 2010 Diego Duclos +# +# 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 . +# ============================================================================= + + +from copy import copy + +from eos.utils.float import floatUnerr +from eos.utils.spoolSupport import SpoolType, SpoolOptions +from eos.utils.stats import DmgTypes + + +class TimeCache: + + def __init__(self): + self._data = {} + + def clear(self, fitID): + if fitID is None: + self._data.clear() + elif fitID in self._data: + del self._data[fitID] + + def getData(self, fitID, cacheType): + return self._data[fitID][cacheType] + + def generateFinalFormDpsVolley(self, fit, maxTime): + # Time is none means that time parameter has to be ignored, + # we do not need cache for that + if maxTime is None: + return True + self._generateInternalForm(fit, maxTime) + fitCache = self._data[fit.ID] + # Final cache has been generated already, don't do anything + if 'finalDpsVolley' in fitCache: + return + # Convert cache from segments with assigned values into points + # which are located at times when dps/volley values change + pointCache = {} + for key, dmgList in fitCache['internalDpsVolley'].items(): + pointData = pointCache[key] = {} + prevDps = None + prevVolley = None + prevTimeEnd = None + for timeStart, timeEnd, dps, volley in dmgList: + # First item + if not pointData: + pointData[timeStart] = (dps, volley) + # Gap between items + elif floatUnerr(prevTimeEnd) < floatUnerr(timeStart): + pointData[prevTimeEnd] = (DmgTypes(0, 0, 0, 0), DmgTypes(0, 0, 0, 0)) + pointData[timeStart] = (dps, volley) + # Changed value + elif dps != prevDps or volley != prevVolley: + pointData[timeStart] = (dps, volley) + prevDps = dps + prevVolley = volley + prevTimeEnd = timeEnd + # We have data in another form, do not need old one any longer + del fitCache['internalDpsVolley'] + changesByTime = {} + for key, dmgMap in pointCache.items(): + for time in dmgMap: + changesByTime.setdefault(time, []).append(key) + # Here we convert cache to following format: + # {time: {key: (dps, volley}} + finalCache = fitCache['finalDpsVolley'] = {} + timeDmgData = {} + for time in sorted(changesByTime): + timeDmgData = copy(timeDmgData) + for key in changesByTime[time]: + timeDmgData[key] = pointCache[key][time] + finalCache[time] = timeDmgData + + def generateFinalFormDmg(self, fit, maxTime): + # Time is none means that time parameter has to be ignored, + # we do not need cache for that + if maxTime is None: + return + self._generateInternalForm(fit, maxTime) + fitCache = self._data[fit.ID] + # Final cache has been generated already, don't do anything + if 'finalDmg' in fitCache: + return + intCache = fitCache['internalDmg'] + changesByTime = {} + for key, dmgMap in intCache.items(): + for time in dmgMap: + changesByTime.setdefault(time, []).append(key) + # Here we convert cache to following format: + # {time: {key: damage done by key at this time}} + finalCache = fitCache['finalDmg'] = {} + timeDmgData = {} + for time in sorted(changesByTime): + timeDmgData = copy(timeDmgData) + for key in changesByTime[time]: + keyDmg = intCache[key][time] + if key in timeDmgData: + timeDmgData[key] = timeDmgData[key] + keyDmg + else: + timeDmgData[key] = keyDmg + finalCache[time] = timeDmgData + # We do not need internal cache once we have final + del fitCache['internalDmg'] + + def _generateInternalForm(self, fit, maxTime): + if self._isTimeCacheValid(fit, maxTime): + return + fitCache = self._data[fit.ID] = {'maxTime': maxTime} + intCacheDpsVolley = fitCache['internalDpsVolley'] = {} + intCacheDmg = fitCache['internalDmg'] = {} + + def addDpsVolley(ddKey, addedTimeStart, addedTimeFinish, addedVolleys): + if not addedVolleys: + return + volleySum = sum(addedVolleys, DmgTypes(0, 0, 0, 0)) + if volleySum.total > 0: + addedDps = volleySum / (addedTimeFinish - addedTimeStart) + # We can take "just best" volley, no matter target resistances, because all + # known items have the same damage type ratio throughout their cycle - and + # applying resistances doesn't change final outcome + bestVolley = max(addedVolleys, key=lambda v: v.total) + ddCacheDps = intCacheDpsVolley.setdefault(ddKey, []) + ddCacheDps.append((addedTimeStart, addedTimeFinish, addedDps, bestVolley)) + + def addDmg(ddKey, addedTime, addedDmg): + if addedDmg.total == 0: + return + intCacheDmg.setdefault(ddKey, {})[addedTime] = addedDmg + + # Modules + for mod in fit.modules: + if not mod.isDealingDamage(): + continue + cycleParams = mod.getCycleParameters(reloadOverride=True) + if cycleParams is None: + continue + currentTime = 0 + nonstopCycles = 0 + for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles(): + cycleVolleys = [] + volleyParams = mod.getVolleyParameters(spoolOptions=SpoolOptions(SpoolType.CYCLES, nonstopCycles, True)) + for volleyTimeMs, volley in volleyParams.items(): + cycleVolleys.append(volley) + addDmg(mod, currentTime + volleyTimeMs / 1000, volley) + addDpsVolley(mod, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys) + if inactiveTimeMs > 0: + nonstopCycles = 0 + else: + nonstopCycles += 1 + if currentTime > maxTime: + break + currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 + # Drones + for drone in fit.drones: + if not drone.isDealingDamage(): + continue + cycleParams = drone.getCycleParameters(reloadOverride=True) + if cycleParams is None: + continue + currentTime = 0 + volleyParams = drone.getVolleyParameters() + for cycleTimeMs, inactiveTimeMs in cycleParams.iterCycles(): + cycleVolleys = [] + for volleyTimeMs, volley in volleyParams.items(): + cycleVolleys.append(volley) + addDmg(drone, currentTime + volleyTimeMs / 1000, volley) + addDpsVolley(drone, currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys) + if currentTime > maxTime: + break + currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 + # Fighters + for fighter in fit.fighters: + if not fighter.isDealingDamage(): + continue + cycleParams = fighter.getCycleParametersPerEffectOptimizedDps(reloadOverride=True) + if cycleParams is None: + continue + volleyParams = fighter.getVolleyParametersPerEffect() + for effectID, abilityCycleParams in cycleParams.items(): + if effectID not in volleyParams: + continue + currentTime = 0 + abilityVolleyParams = volleyParams[effectID] + for cycleTimeMs, inactiveTimeMs in abilityCycleParams.iterCycles(): + cycleVolleys = [] + for volleyTimeMs, volley in abilityVolleyParams.items(): + cycleVolleys.append(volley) + addDmg((fighter, effectID), currentTime + volleyTimeMs / 1000, volley) + addDpsVolley((fighter, effectID), currentTime, currentTime + cycleTimeMs / 1000, cycleVolleys) + if currentTime > maxTime: + break + currentTime += cycleTimeMs / 1000 + inactiveTimeMs / 1000 + + def _isTimeCacheValid(self, fit, maxTime): + try: + cacheMaxTime = self._data[fit.ID]['maxTime'] + except KeyError: + return False + return maxTime <= cacheMaxTime From 15b6a848e81aab1fb353d7f24ca0ba774ff78bda Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 4 Jul 2019 11:40:38 +0300 Subject: [PATCH 85/93] Move warp time subwarp speed calculation to separate cache as well --- gui/builtinGraphs/base.py | 12 +++++++++++ gui/builtinGraphs/fitDamageStats/timeCache.py | 12 ++--------- gui/builtinGraphs/fitWarpTime.py | 21 +++++++++++++------ 3 files changed, 29 insertions(+), 16 deletions(-) diff --git a/gui/builtinGraphs/base.py b/gui/builtinGraphs/base.py index cada75c4e..2f00ac20e 100644 --- a/gui/builtinGraphs/base.py +++ b/gui/builtinGraphs/base.py @@ -207,5 +207,17 @@ class FitGraph(metaclass=ABCMeta): current += step +class FitDataCache: + + def __init__(self): + self._data = {} + + def clear(self, fitID): + if fitID is None: + self._data.clear() + elif fitID in self._data: + del self._data[fitID] + + # noinspection PyUnresolvedReferences from gui.builtinGraphs import * diff --git a/gui/builtinGraphs/fitDamageStats/timeCache.py b/gui/builtinGraphs/fitDamageStats/timeCache.py index 8a8a905ab..3f8eae4f7 100644 --- a/gui/builtinGraphs/fitDamageStats/timeCache.py +++ b/gui/builtinGraphs/fitDamageStats/timeCache.py @@ -23,18 +23,10 @@ from copy import copy from eos.utils.float import floatUnerr from eos.utils.spoolSupport import SpoolType, SpoolOptions from eos.utils.stats import DmgTypes +from gui.builtinGraphs.base import FitDataCache -class TimeCache: - - def __init__(self): - self._data = {} - - def clear(self, fitID): - if fitID is None: - self._data.clear() - elif fitID in self._data: - del self._data[fitID] +class TimeCache(FitDataCache): def getData(self, fitID, cacheType): return self._data[fitID][cacheType] diff --git a/gui/builtinGraphs/fitWarpTime.py b/gui/builtinGraphs/fitWarpTime.py index 955fa63b6..a5720c17b 100644 --- a/gui/builtinGraphs/fitWarpTime.py +++ b/gui/builtinGraphs/fitWarpTime.py @@ -21,7 +21,7 @@ import math from eos.const import FittingModuleState -from .base import FitGraph, XDef, YDef, Input +from .base import FitGraph, XDef, YDef, Input, FitDataCache AU_METERS = 149597870700 @@ -29,6 +29,13 @@ AU_METERS = 149597870700 class FitWarpTimeGraph(FitGraph): + def __init__(self): + super().__init__() + self._subspeedCache = SubwarpSpeedCache() + + def _clearInternalCache(self, fitID): + self._subspeedCache.clear(fitID) + # UI stuff name = 'Warp Time' xDefs = [ @@ -53,7 +60,7 @@ class FitWarpTimeGraph(FitGraph): def _distance2time(self, mainInput, miscInputs, fit, tgt): xs = [] ys = [] - subwarpSpeed = self.__getSubwarpSpeed(fit) + subwarpSpeed = self._subspeedCache.getSubwarpSpeed(fit) warpSpeed = fit.warpSpeed for distance in self._iterLinear(mainInput[1]): time = calculate_time_in_warp(max_subwarp_speed=subwarpSpeed, max_warp_speed=warpSpeed, warp_dist=distance) @@ -64,10 +71,12 @@ class FitWarpTimeGraph(FitGraph): _getters = { ('distance', 'time'): _distance2time} - # Cache generation - def __getSubwarpSpeed(self, fit): + +class SubwarpSpeedCache(FitDataCache): + + def getSubwarpSpeed(self, fit): try: - subwarpSpeed = self._calcCache[fit.ID] + subwarpSpeed = self._data[fit.ID] except KeyError: modStates = {} for mod in fit.modules: @@ -97,7 +106,7 @@ class FitWarpTimeGraph(FitGraph): fighter.active = False fit.calculateModifiedAttributes() subwarpSpeed = fit.ship.getModifiedItemAttr('maxVelocity') - self._calcCache[fit.ID] = subwarpSpeed + self._data[fit.ID] = subwarpSpeed for projInfo, state in projFitStates.items(): projInfo.active = state for mod, state in modStates.items(): From d2b838e9d5fe20a87a23daac10055df3e3b15785 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 4 Jul 2019 13:10:45 +0300 Subject: [PATCH 86/93] Rework interface between dps graph and time cache --- gui/builtinGraphs/fitDamageStats/graph.py | 20 ++-- gui/builtinGraphs/fitDamageStats/timeCache.py | 98 +++++++++++-------- 2 files changed, 68 insertions(+), 50 deletions(-) diff --git a/gui/builtinGraphs/fitDamageStats/graph.py b/gui/builtinGraphs/fitDamageStats/graph.py index 3172a4701..748f21db7 100644 --- a/gui/builtinGraphs/fitDamageStats/graph.py +++ b/gui/builtinGraphs/fitDamageStats/graph.py @@ -115,21 +115,21 @@ class FitDamageStatsGraph(FitGraph): def _time2dps(self, mainInput, miscInputs, fit, tgt): def calcDpsTmp(timeDmg): - return floatUnerr(sum(dts[0].total for dts in timeDmg.values())) - self._timeCache.generateFinalFormDpsVolley(fit, mainInput[1][1]) - return self._composeTimeGraph(mainInput, fit, 'finalDpsVolley', calcDpsTmp) + return floatUnerr(sum(dts.total for dts in timeDmg.values())) + self._timeCache.prepareDpsData(fit, mainInput[1][1]) + return self._composeTimeGraph(mainInput, fit, self._timeCache.getDpsData, calcDpsTmp) def _time2volley(self, mainInput, miscInputs, fit, tgt): def calcVolleyTmp(timeDmg): - return floatUnerr(sum(dts[1].total for dts in timeDmg.values())) - self._timeCache.generateFinalFormDpsVolley(fit, mainInput[1][1]) - return self._composeTimeGraph(mainInput, fit, 'finalDpsVolley', calcVolleyTmp) + return floatUnerr(sum(dts.total for dts in timeDmg.values())) + self._timeCache.prepareVolleyData(fit, mainInput[1][1]) + return self._composeTimeGraph(mainInput, fit, self._timeCache.getVolleyData, calcVolleyTmp) def _time2damage(self, mainInput, miscInputs, fit, tgt): def calcDamageTmp(timeDmg): return floatUnerr(sum(dt.total for dt in timeDmg.values())) - self._timeCache.generateFinalFormDmg(fit, mainInput[1][1]) - return self._composeTimeGraph(mainInput, fit, 'finalDmg', calcDamageTmp) + self._timeCache.prepareDmgData(fit, mainInput[1][1]) + return self._composeTimeGraph(mainInput, fit, self._timeCache.getDmgData, calcDamageTmp) def _tgtSpeed2dps(self, mainInput, miscInputs, fit, tgt): return [], [] @@ -163,12 +163,12 @@ class FitDamageStatsGraph(FitGraph): ('tgtSigRad', 'volley'): _tgtSigRad2volley, ('tgtSigRad', 'damage'): _tgtSigRad2damage} - def _composeTimeGraph(self, mainInput, fit, cacheName, calcFunc): + def _composeTimeGraph(self, mainInput, fit, cacheFunc, calcFunc): xs = [] ys = [] minTime, maxTime = mainInput[1] - cache = self._timeCache.getData(fit.ID, cacheName) + cache = cacheFunc(fit) currentDps = None currentTime = None for currentTime in sorted(cache): diff --git a/gui/builtinGraphs/fitDamageStats/timeCache.py b/gui/builtinGraphs/fitDamageStats/timeCache.py index 3f8eae4f7..726f1d474 100644 --- a/gui/builtinGraphs/fitDamageStats/timeCache.py +++ b/gui/builtinGraphs/fitDamageStats/timeCache.py @@ -28,10 +28,53 @@ from gui.builtinGraphs.base import FitDataCache class TimeCache(FitDataCache): - def getData(self, fitID, cacheType): - return self._data[fitID][cacheType] + def getDpsData(self, fit): + return self._data[fit.ID]['finalDps'] - def generateFinalFormDpsVolley(self, fit, maxTime): + def getVolleyData(self, fit): + return self._data[fit.ID]['finalVolley'] + + def getDmgData(self, fit): + return self._data[fit.ID]['finalDmg'] + + def prepareDpsData(self, fit, maxTime): + self._prepareDpsVolleyData(fit, maxTime) + + def prepareVolleyData(self, fit, maxTime): + self._prepareDpsVolleyData(fit, maxTime) + + def prepareDmgData(self, fit, maxTime): + # Time is none means that time parameter has to be ignored, + # we do not need cache for that + if maxTime is None: + return + self._generateInternalForm(fit, maxTime) + fitCache = self._data[fit.ID] + # Final cache has been generated already, don't do anything + if 'finalDmg' in fitCache: + return + intCache = fitCache['internalDmg'] + changesByTime = {} + for key, dmgMap in intCache.items(): + for time in dmgMap: + changesByTime.setdefault(time, []).append(key) + # Here we convert cache to following format: + # {time: {key: damage done by key at this time}} + finalCache = fitCache['finalDmg'] = {} + timeDmgData = {} + for time in sorted(changesByTime): + timeDmgData = copy(timeDmgData) + for key in changesByTime[time]: + keyDmg = intCache[key][time] + if key in timeDmgData: + timeDmgData[key] = timeDmgData[key] + keyDmg + else: + timeDmgData[key] = keyDmg + finalCache[time] = timeDmgData + # We do not need internal cache once we have final + del fitCache['internalDmg'] + + def _prepareDpsVolleyData(self, fit, maxTime): # Time is none means that time parameter has to be ignored, # we do not need cache for that if maxTime is None: @@ -39,7 +82,7 @@ class TimeCache(FitDataCache): self._generateInternalForm(fit, maxTime) fitCache = self._data[fit.ID] # Final cache has been generated already, don't do anything - if 'finalDpsVolley' in fitCache: + if 'finalDps' in fitCache and 'finalVolley' in fitCache: return # Convert cache from segments with assigned values into points # which are located at times when dps/volley values change @@ -71,44 +114,19 @@ class TimeCache(FitDataCache): changesByTime.setdefault(time, []).append(key) # Here we convert cache to following format: # {time: {key: (dps, volley}} - finalCache = fitCache['finalDpsVolley'] = {} - timeDmgData = {} + finalDpsCache = fitCache['finalDps'] = {} + finalVolleyCache = fitCache['finalVolley'] = {} + timeDpsData = {} + timeVolleyData = {} for time in sorted(changesByTime): - timeDmgData = copy(timeDmgData) + timeDpsData = copy(timeDpsData) + timeVolleyData = copy(timeVolleyData) for key in changesByTime[time]: - timeDmgData[key] = pointCache[key][time] - finalCache[time] = timeDmgData - - def generateFinalFormDmg(self, fit, maxTime): - # Time is none means that time parameter has to be ignored, - # we do not need cache for that - if maxTime is None: - return - self._generateInternalForm(fit, maxTime) - fitCache = self._data[fit.ID] - # Final cache has been generated already, don't do anything - if 'finalDmg' in fitCache: - return - intCache = fitCache['internalDmg'] - changesByTime = {} - for key, dmgMap in intCache.items(): - for time in dmgMap: - changesByTime.setdefault(time, []).append(key) - # Here we convert cache to following format: - # {time: {key: damage done by key at this time}} - finalCache = fitCache['finalDmg'] = {} - timeDmgData = {} - for time in sorted(changesByTime): - timeDmgData = copy(timeDmgData) - for key in changesByTime[time]: - keyDmg = intCache[key][time] - if key in timeDmgData: - timeDmgData[key] = timeDmgData[key] + keyDmg - else: - timeDmgData[key] = keyDmg - finalCache[time] = timeDmgData - # We do not need internal cache once we have final - del fitCache['internalDmg'] + dps, volley = pointCache[key][time] + timeDpsData[key] = dps + timeVolleyData[key] = volley + finalDpsCache[time] = timeDpsData + finalVolleyCache[time] = timeVolleyData def _generateInternalForm(self, fit, maxTime): if self._isTimeCacheValid(fit, maxTime): From 185cf4f6257a66cd8f28f86e541f4eb30a252ff3 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 4 Jul 2019 13:40:04 +0300 Subject: [PATCH 87/93] Add drones to dps-range calculation --- gui/builtinGraphs/fitDamageStats/calc.py | 5 +++++ gui/builtinGraphs/fitDamageStats/graph.py | 15 +++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/gui/builtinGraphs/fitDamageStats/calc.py b/gui/builtinGraphs/fitDamageStats/calc.py index fe7dc1124..14b22ce4a 100644 --- a/gui/builtinGraphs/fitDamageStats/calc.py +++ b/gui/builtinGraphs/fitDamageStats/calc.py @@ -19,6 +19,7 @@ import math +from functools import lru_cache def getTurretMult(mod, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius): @@ -115,6 +116,7 @@ def getFighterAbilityMult(fighter, ability, fit, distance, tgtSpeed, tgtSigRadiu # Turret-specific +@lru_cache(maxsize=50) def _calcTurretMult(chanceToHit): """Calculate damage multiplier for turret-based weapons.""" # https://wiki.eveuniversity.org/Turret_mechanics#Damage @@ -130,6 +132,7 @@ def _calcTurretMult(chanceToHit): return totalMult +@lru_cache(maxsize=1000) def _calcTurretChanceToHit( atkSpeed, atkAngle, atkRadius, atkOptimalRange, atkFalloffRange, atkTracking, atkOptimalSigRadius, distance, tgtSpeed, tgtAngle, tgtRadius, tgtSigRadius @@ -163,6 +166,7 @@ def _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRa # Missile-specific +@lru_cache(maxsize=200) def _calcMissileMult(atkRadius, atkRange, atkEr, atkEv, atkDrf, distance, tgtSpeed, tgtSigRadius): """Calculate damage multiplier for missile launcher.""" # Missiles spawn in the center of the attacking ship @@ -173,6 +177,7 @@ def _calcMissileMult(atkRadius, atkRange, atkEr, atkEv, atkDrf, distance, tgtSpe return mult +@lru_cache(maxsize=200) def _calcFighterMult(atkOptimalRange, atkFalloffRange, atkEr, atkEv, atkDrf, distance, tgtSpeed, tgtSigRadius): """Calculate damage multiplier for separate fighter ability,""" rangeFactor = _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance) diff --git a/gui/builtinGraphs/fitDamageStats/graph.py b/gui/builtinGraphs/fitDamageStats/graph.py index 748f21db7..1e7016b9b 100644 --- a/gui/builtinGraphs/fitDamageStats/graph.py +++ b/gui/builtinGraphs/fitDamageStats/graph.py @@ -103,6 +103,21 @@ class FitDamageStatsGraph(FitGraph): distance=distance, tgtSpeed=miscInputMap['tgtSpeed'], tgtSigRadius=tgtSigRad) + for drone in fit.drones: + if not drone.isDealingDamage(): + continue + droneDps = drone.getDps().total + totalDps += droneDps * getDroneMult( + drone=drone, + fit=fit, + tgt=tgt, + atkSpeed=miscInputMap['atkSpeed'], + atkAngle=miscInputMap['atkAngle'], + distance=distance, + tgtSpeed=miscInputMap['tgtSpeed'], + tgtAngle=miscInputMap['tgtAngle'], + tgtSigRadius=tgtSigRad) + xs.append(distance) ys.append(totalDps) return xs, ys From c4f225003a6b75855f049a1dc35abef3f46fb919 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 4 Jul 2019 14:33:28 +0300 Subject: [PATCH 88/93] Add fighters to dps vs range graph --- eos/saveddata/fighter.py | 4 ++++ gui/builtinGraphs/fitDamageStats/graph.py | 14 +++++++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/eos/saveddata/fighter.py b/eos/saveddata/fighter.py index 9a079bbcd..333ce8def 100644 --- a/eos/saveddata/fighter.py +++ b/eos/saveddata/fighter.py @@ -151,6 +151,10 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def abilities(self): return self.__abilities or [] + @property + def abilityMap(self): + return {a.effectID: a for a in self.abilities} + @property def charge(self): return self.__charge diff --git a/gui/builtinGraphs/fitDamageStats/graph.py b/gui/builtinGraphs/fitDamageStats/graph.py index 1e7016b9b..54b7bc139 100644 --- a/gui/builtinGraphs/fitDamageStats/graph.py +++ b/gui/builtinGraphs/fitDamageStats/graph.py @@ -117,7 +117,19 @@ class FitDamageStatsGraph(FitGraph): tgtSpeed=miscInputMap['tgtSpeed'], tgtAngle=miscInputMap['tgtAngle'], tgtSigRadius=tgtSigRad) - + for fighter in fit.fighters: + if not fighter.isDealingDamage(): + continue + abilityMap = fighter.abilityMap + for effectID, abilityDps in fighter.getDpsPerEffect().items(): + ability = abilityMap[effectID] + totalDps += abilityDps.total * getFighterAbilityMult( + fighter=fighter, + ability=ability, + fit=fit, + distance=distance, + tgtSpeed=miscInputMap['tgtSpeed'], + tgtSigRadius=tgtSigRad) xs.append(distance) ys.append(totalDps) return xs, ys From 63c45c5060c38270c57e0f1856404978297eaf04 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 4 Jul 2019 17:22:23 +0300 Subject: [PATCH 89/93] Plug in all calculations besides where X is time --- eos/saveddata/fighter.py | 11 +- eos/utils/stats.py | 19 ++ gui/builtinGraphs/fitDamageStats/calc.py | 26 +- gui/builtinGraphs/fitDamageStats/graph.py | 285 +++++++++++++----- gui/builtinGraphs/fitDamageStats/timeCache.py | 29 ++ 5 files changed, 271 insertions(+), 99 deletions(-) diff --git a/eos/saveddata/fighter.py b/eos/saveddata/fighter.py index 333ce8def..32867587f 100644 --- a/eos/saveddata/fighter.py +++ b/eos/saveddata/fighter.py @@ -151,10 +151,6 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): def abilities(self): return self.__abilities or [] - @property - def abilityMap(self): - return {a.effectID: a for a in self.abilities} - @property def charge(self): return self.__charge @@ -206,6 +202,13 @@ class Fighter(HandledItem, HandledCharge, ItemAttrShortcut, ChargeAttrShortcut): explosive=volleyValue.explosive * (1 - getattr(targetResists, "explosiveAmount", 0))) return adjustedVolley + def getVolleyPerEffect(self, targetResists=None): + volleyParams = self.getVolleyParametersPerEffect(targetResists=targetResists) + volleyMap = {} + for effectID, volleyData in volleyParams.items(): + volleyMap[effectID] = volleyData[0] + return volleyMap + def getVolley(self, targetResists=None): volleyParams = self.getVolleyParametersPerEffect(targetResists=targetResists) em = 0 diff --git a/eos/utils/stats.py b/eos/utils/stats.py index 4d41300b4..4b97ea2d9 100644 --- a/eos/utils/stats.py +++ b/eos/utils/stats.py @@ -72,6 +72,23 @@ class DmgTypes: self._calcTotal() return self + def __mul__(self, mul): + return type(self)( + em=self.em * mul, + thermal=self.thermal * mul, + kinetic=self.kinetic * mul, + explosive=self.explosive * mul) + + def __imul__(self, mul): + if mul == 1: + return + self.em *= mul + self.thermal *= mul + self.kinetic *= mul + self.explosive *= mul + self._calcTotal() + return self + def __truediv__(self, div): return type(self)( em=self.em / div, @@ -80,6 +97,8 @@ class DmgTypes: explosive=self.explosive / div) def __itruediv__(self, div): + if div == 1: + return self.em /= div self.thermal /= div self.kinetic /= div diff --git a/gui/builtinGraphs/fitDamageStats/calc.py b/gui/builtinGraphs/fitDamageStats/calc.py index 14b22ce4a..f31d398aa 100644 --- a/gui/builtinGraphs/fitDamageStats/calc.py +++ b/gui/builtinGraphs/fitDamageStats/calc.py @@ -44,13 +44,12 @@ def getLauncherMult(mod, fit, distance, tgtSpeed, tgtSigRadius): modRange = mod.maxRange if modRange is None: return 0 - mult = _calcMissileMult( - atkRadius=fit.ship.getModifiedItemAttr('radius'), - atkRange=modRange, + if distance + fit.ship.getModifiedItemAttr('radius') > modRange: + return 0 + mult = _calcMissileFactor( atkEr=mod.getModifiedChargeAttr('aoeCloudSize'), atkEv=mod.getModifiedChargeAttr('aoeVelocity'), atkDrf=mod.getModifiedChargeAttr('aoeDamageReductionFactor'), - distance=distance, tgtSpeed=tgtSpeed, tgtSigRadius=tgtSigRadius) return mult @@ -167,25 +166,6 @@ def _calcTrackingFactor(atkTracking, atkOptimalSigRadius, angularSpeed, tgtSigRa # Missile-specific @lru_cache(maxsize=200) -def _calcMissileMult(atkRadius, atkRange, atkEr, atkEv, atkDrf, distance, tgtSpeed, tgtSigRadius): - """Calculate damage multiplier for missile launcher.""" - # Missiles spawn in the center of the attacking ship - if distance + atkRadius > atkRange: - mult = 0 - else: - mult = _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius) - return mult - - -@lru_cache(maxsize=200) -def _calcFighterMult(atkOptimalRange, atkFalloffRange, atkEr, atkEv, atkDrf, distance, tgtSpeed, tgtSigRadius): - """Calculate damage multiplier for separate fighter ability,""" - rangeFactor = _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance) - missileFactor = _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius) - mult = rangeFactor * missileFactor - return mult - - def _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius): """Missile application.""" factors = [1] diff --git a/gui/builtinGraphs/fitDamageStats/graph.py b/gui/builtinGraphs/fitDamageStats/graph.py index 54b7bc139..15f4d2d27 100644 --- a/gui/builtinGraphs/fitDamageStats/graph.py +++ b/gui/builtinGraphs/fitDamageStats/graph.py @@ -19,9 +19,10 @@ import eos.config -from eos.const import FittingHardpoint, FittingModuleState +from eos.const import FittingHardpoint from eos.utils.float import floatUnerr from eos.utils.spoolSupport import SpoolType, SpoolOptions +from eos.utils.stats import DmgTypes from gui.builtinGraphs.base import FitGraph, XDef, YDef, Input, VectorDef from .calc import getTurretMult, getLauncherMult, getDroneMult, getFighterAbilityMult from .timeCache import TimeCache @@ -63,82 +64,28 @@ class FitDamageStatsGraph(FitGraph): ('distance', 'km'): lambda v, fit, tgt: v * 1000, ('atkSpeed', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('maxVelocity'), ('tgtSpeed', '%'): lambda v, fit, tgt: v / 100 * tgt.ship.getModifiedItemAttr('maxVelocity'), - ('tgtSigRad', '%'): lambda v, fit, tgt: v / 100 * fit.ship.getModifiedItemAttr('signatureRadius')} + ('tgtSigRad', '%'): lambda v, fit, tgt: v / 100 * tgt.ship.getModifiedItemAttr('signatureRadius')} _limiters = { 'time': lambda fit, tgt: (0, 2500)} _denormalizers = { ('distance', 'km'): lambda v, fit, tgt: v / 1000, ('tgtSpeed', '%'): lambda v, fit, tgt: v * 100 / tgt.ship.getModifiedItemAttr('maxVelocity'), - ('tgtSigRad', '%'): lambda v, fit, tgt: v * 100 / fit.ship.getModifiedItemAttr('signatureRadius')} + ('tgtSigRad', '%'): lambda v, fit, tgt: v * 100 / tgt.ship.getModifiedItemAttr('signatureRadius')} def _distance2dps(self, mainInput, miscInputs, fit, tgt): - xs = [] - ys = [] - defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage'] - miscInputMap = dict(miscInputs) - tgtSigRad = miscInputMap.get('tgtSigRad', tgt.ship.getModifiedItemAttr('signatureRadius')) - for distance in self._iterLinear(mainInput[1]): - totalDps = 0 - for mod in fit.modules: - if not mod.isDealingDamage(): - continue - modDps = mod.getDps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)).total - if mod.hardpoint == FittingHardpoint.TURRET: - if mod.state >= FittingModuleState.ACTIVE: - totalDps += modDps * getTurretMult( - mod=mod, - fit=fit, - tgt=tgt, - atkSpeed=miscInputMap['atkSpeed'], - atkAngle=miscInputMap['atkAngle'], - distance=distance, - tgtSpeed=miscInputMap['tgtSpeed'], - tgtAngle=miscInputMap['tgtAngle'], - tgtSigRadius=tgtSigRad) - elif mod.hardpoint == FittingHardpoint.MISSILE: - if mod.state >= FittingModuleState.ACTIVE: - totalDps += modDps * getLauncherMult( - mod=mod, - fit=fit, - distance=distance, - tgtSpeed=miscInputMap['tgtSpeed'], - tgtSigRadius=tgtSigRad) - for drone in fit.drones: - if not drone.isDealingDamage(): - continue - droneDps = drone.getDps().total - totalDps += droneDps * getDroneMult( - drone=drone, - fit=fit, - tgt=tgt, - atkSpeed=miscInputMap['atkSpeed'], - atkAngle=miscInputMap['atkAngle'], - distance=distance, - tgtSpeed=miscInputMap['tgtSpeed'], - tgtAngle=miscInputMap['tgtAngle'], - tgtSigRadius=tgtSigRad) - for fighter in fit.fighters: - if not fighter.isDealingDamage(): - continue - abilityMap = fighter.abilityMap - for effectID, abilityDps in fighter.getDpsPerEffect().items(): - ability = abilityMap[effectID] - totalDps += abilityDps.total * getFighterAbilityMult( - fighter=fighter, - ability=ability, - fit=fit, - distance=distance, - tgtSpeed=miscInputMap['tgtSpeed'], - tgtSigRadius=tgtSigRad) - xs.append(distance) - ys.append(totalDps) - return xs, ys + return self._xDistanceGetter( + mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, + dmgFunc=self._getDpsPerKey, timeCacheFunc=self._timeCache.prepareDpsData) def _distance2volley(self, mainInput, miscInputs, fit, tgt): - return [], [] + return self._xDistanceGetter( + mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, + dmgFunc=self._getVolleyPerKey, timeCacheFunc=self._timeCache.prepareVolleyData) def _distance2damage(self, mainInput, miscInputs, fit, tgt): - return [], [] + return self._xDistanceGetter( + mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, + dmgFunc=self._getDmgPerKey, timeCacheFunc=self._timeCache.prepareDmgData) def _time2dps(self, mainInput, miscInputs, fit, tgt): def calcDpsTmp(timeDmg): @@ -159,22 +106,34 @@ class FitDamageStatsGraph(FitGraph): return self._composeTimeGraph(mainInput, fit, self._timeCache.getDmgData, calcDamageTmp) def _tgtSpeed2dps(self, mainInput, miscInputs, fit, tgt): - return [], [] + return self._xTgtSpeedGetter( + mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, + dmgFunc=self._getDpsPerKey, timeCacheFunc=self._timeCache.prepareDpsData) def _tgtSpeed2volley(self, mainInput, miscInputs, fit, tgt): - return [], [] + return self._xTgtSpeedGetter( + mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, + dmgFunc=self._getVolleyPerKey, timeCacheFunc=self._timeCache.prepareVolleyData) def _tgtSpeed2damage(self, mainInput, miscInputs, fit, tgt): - return [], [] + return self._xTgtSpeedGetter( + mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, + dmgFunc=self._getDmgPerKey, timeCacheFunc=self._timeCache.prepareDmgData) def _tgtSigRad2dps(self, mainInput, miscInputs, fit, tgt): - return [], [] + return self._xTgtSigRadiusGetter( + mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, + dmgFunc=self._getDpsPerKey, timeCacheFunc=self._timeCache.prepareDpsData) def _tgtSigRad2volley(self, mainInput, miscInputs, fit, tgt): - return [], [] + return self._xTgtSigRadiusGetter( + mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, + dmgFunc=self._getVolleyPerKey, timeCacheFunc=self._timeCache.prepareVolleyData) def _tgtSigRad2damage(self, mainInput, miscInputs, fit, tgt): - return [], [] + return self._xTgtSigRadiusGetter( + mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, + dmgFunc=self._getDmgPerKey, timeCacheFunc=self._timeCache.prepareDmgData) _getters = { ('distance', 'dps'): _distance2dps, @@ -190,6 +149,188 @@ class FitDamageStatsGraph(FitGraph): ('tgtSigRad', 'volley'): _tgtSigRad2volley, ('tgtSigRad', 'damage'): _tgtSigRad2damage} + # Point getter helpers + def _xDistanceGetter(self, mainInput, miscInputs, fit, tgt, dmgFunc, timeCacheFunc): + xs = [] + ys = [] + tgtSigRadius = tgt.ship.getModifiedItemAttr('signatureRadius') + # Process inputs into more convenient form + miscInputMap = dict(miscInputs) + # Get all data we need for all distances into maps/caches + timeCacheFunc(fit, miscInputMap['time']) + dmgMap = dmgFunc(fit=fit, time=miscInputMap['time']) + # Go through distances and calculate distance-dependent data + for distance in self._iterLinear(mainInput[1]): + applicationMap = self._getApplicationPerKey( + fit=fit, + tgt=tgt, + atkSpeed=miscInputMap['atkSpeed'], + atkAngle=miscInputMap['atkAngle'], + distance=distance, + tgtSpeed=miscInputMap['tgtSpeed'], + tgtAngle=miscInputMap['tgtAngle'], + tgtSigRadius=tgtSigRadius) + dmg = self._aggregate(dmgMap=dmgMap, applicationMap=applicationMap).total + xs.append(distance) + ys.append(dmg) + return xs, ys + + def _xTgtSpeedGetter(self, mainInput, miscInputs, fit, tgt, dmgFunc, timeCacheFunc): + xs = [] + ys = [] + tgtSigRadius = tgt.ship.getModifiedItemAttr('signatureRadius') + # Process inputs into more convenient form + miscInputMap = dict(miscInputs) + # Get all data we need for all target speeds into maps/caches + timeCacheFunc(fit, miscInputMap['time']) + dmgMap = dmgFunc(fit=fit, time=miscInputMap['time']) + # Go through target speeds and calculate distance-dependent data + for tgtSpeed in self._iterLinear(mainInput[1]): + applicationMap = self._getApplicationPerKey( + fit=fit, + tgt=tgt, + atkSpeed=miscInputMap['atkSpeed'], + atkAngle=miscInputMap['atkAngle'], + distance=miscInputMap['distance'], + tgtSpeed=tgtSpeed, + tgtAngle=miscInputMap['tgtAngle'], + tgtSigRadius=tgtSigRadius) + dmg = self._aggregate(dmgMap=dmgMap, applicationMap=applicationMap).total + xs.append(tgtSpeed) + ys.append(dmg) + return xs, ys + + def _xTgtSigRadiusGetter(self, mainInput, miscInputs, fit, tgt, dmgFunc, timeCacheFunc): + xs = [] + ys = [] + # Process inputs into more convenient form + miscInputMap = dict(miscInputs) + # Get all data we need for all target speeds into maps/caches + timeCacheFunc(fit, miscInputMap['time']) + dmgMap = dmgFunc(fit=fit, time=miscInputMap['time']) + # Go through target speeds and calculate distance-dependent data + for tgtSigRadius in self._iterLinear(mainInput[1]): + applicationMap = self._getApplicationPerKey( + fit=fit, + tgt=tgt, + atkSpeed=miscInputMap['atkSpeed'], + atkAngle=miscInputMap['atkAngle'], + distance=miscInputMap['distance'], + tgtSpeed=miscInputMap['tgtSpeed'], + tgtAngle=miscInputMap['tgtAngle'], + tgtSigRadius=tgtSigRadius) + dmg = self._aggregate(dmgMap=dmgMap, applicationMap=applicationMap).total + xs.append(tgtSigRadius) + ys.append(dmg) + return xs, ys + + # Damage data per key getters + def _getDpsPerKey(self, fit, time): + if time is not None: + return self._timeCache.getDpsDataPoint(fit, time) + dpsMap = {} + defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage'] + for mod in fit.modules: + if not mod.isDealingDamage(): + continue + dpsMap[mod] = mod.getDps(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)) + for drone in fit.drones: + if not drone.isDealingDamage(): + continue + dpsMap[drone] = drone.getDps() + for fighter in fit.fighters: + if not fighter.isDealingDamage(): + continue + for effectID, effectDps in fighter.getDpsPerEffect().items(): + dpsMap[(fighter, effectID)] = effectDps + return dpsMap + + def _getVolleyPerKey(self, fit, time): + if time is not None: + return self._timeCache.getVolleyDataPoint(fit, time) + volleyMap = {} + defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage'] + for mod in fit.modules: + if not mod.isDealingDamage(): + continue + volleyMap[mod] = mod.getVolley(spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)) + for drone in fit.drones: + if not drone.isDealingDamage(): + continue + volleyMap[drone] = drone.getVolley() + for fighter in fit.fighters: + if not fighter.isDealingDamage(): + continue + for effectID, effectVolley in fighter.getVolleyPerEffect().items(): + volleyMap[(fighter, effectID)] = effectVolley + return volleyMap + + def _getDmgPerKey(self, fit, time): + # Damage inflicted makes no sense without time specified + if time is None: + raise ValueError + return self._timeCache.getDmgDataPoint(fit, time) + + # Application getter + def _getApplicationPerKey(self, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius): + applicationMap = {} + for mod in fit.modules: + if not mod.isDealingDamage(): + continue + if mod.hardpoint == FittingHardpoint.TURRET: + applicationMap[mod] = getTurretMult( + mod=mod, + fit=fit, + tgt=tgt, + atkSpeed=atkSpeed, + atkAngle=atkAngle, + distance=distance, + tgtSpeed=tgtSpeed, + tgtAngle=tgtAngle, + tgtSigRadius=tgtSigRadius) + elif mod.hardpoint == FittingHardpoint.MISSILE: + applicationMap[mod] = getLauncherMult( + mod=mod, + fit=fit, + distance=distance, + tgtSpeed=tgtSpeed, + tgtSigRadius=tgtSigRadius) + for drone in fit.drones: + if not drone.isDealingDamage(): + continue + applicationMap[drone] = getDroneMult( + drone=drone, + fit=fit, + tgt=tgt, + atkSpeed=atkSpeed, + atkAngle=atkAngle, + distance=distance, + tgtSpeed=tgtSpeed, + tgtAngle=tgtAngle, + tgtSigRadius=tgtSigRadius) + for fighter in fit.fighters: + if not fighter.isDealingDamage(): + continue + for ability in fighter.abilities: + if not ability.dealsDamage or not ability.active: + continue + applicationMap[(fighter, ability.effectID)] = getFighterAbilityMult( + fighter=fighter, + ability=ability, + fit=fit, + distance=distance, + tgtSpeed=tgtSpeed, + tgtSigRadius=tgtSigRadius) + return applicationMap + + # Calculate damage from maps + def _aggregate(self, dmgMap, applicationMap): + total = DmgTypes(0, 0, 0, 0) + for key, dmg in dmgMap.items(): + total += dmg * applicationMap.get(key, 1) + return total + + ############# TO REFACTOR: time graph stuff def _composeTimeGraph(self, mainInput, fit, cacheFunc, calcFunc): xs = [] ys = [] diff --git a/gui/builtinGraphs/fitDamageStats/timeCache.py b/gui/builtinGraphs/fitDamageStats/timeCache.py index 726f1d474..c557eaecf 100644 --- a/gui/builtinGraphs/fitDamageStats/timeCache.py +++ b/gui/builtinGraphs/fitDamageStats/timeCache.py @@ -28,15 +28,33 @@ from gui.builtinGraphs.base import FitDataCache class TimeCache(FitDataCache): + # Whole data getters def getDpsData(self, fit): + """Return DPS data in {time: {key: dps}} format.""" return self._data[fit.ID]['finalDps'] def getVolleyData(self, fit): + """Return volley data in {time: {key: volley}} format.""" return self._data[fit.ID]['finalVolley'] def getDmgData(self, fit): + """Return inflicted damage data in {time: {key: damage}} format.""" return self._data[fit.ID]['finalDmg'] + # Specific data point getters + def getDpsDataPoint(self, fit, time): + """Get DPS data by specified time in {key: dps} format.""" + return self._getDataPoint(fit, time, self.getDpsData) + + def getVolleyDataPoint(self, fit, time): + """Get volley data by specified time in {key: volley} format.""" + return self._getDataPoint(fit, time, self.getVolleyData) + + def getDmgDataPoint(self, fit, time): + """Get inflicted damage data by specified time in {key: dmg} format.""" + return self._getDataPoint(fit, time, self.getDmgData) + + # Preparation functions def prepareDpsData(self, fit, maxTime): self._prepareDpsVolleyData(fit, maxTime) @@ -74,6 +92,7 @@ class TimeCache(FitDataCache): # We do not need internal cache once we have final del fitCache['internalDmg'] + # Private stuff def _prepareDpsVolleyData(self, fit, maxTime): # Time is none means that time parameter has to be ignored, # we do not need cache for that @@ -223,3 +242,13 @@ class TimeCache(FitDataCache): except KeyError: return False return maxTime <= cacheMaxTime + + def _getDataPoint(self, fit, time, dataFunc): + data = dataFunc(fit) + timesBefore = [t for t in data if floatUnerr(t) <= floatUnerr(time)] + try: + time = max(timesBefore) + except ValueError: + return {} + else: + return data[time] From 78d2dff0d87f0635026e559d7d71852a1aec3bfd Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 4 Jul 2019 17:37:45 +0300 Subject: [PATCH 90/93] Move x time graphs to new methods --- gui/builtinGraphs/fitDamageStats/graph.py | 157 ++++++++++++---------- 1 file changed, 84 insertions(+), 73 deletions(-) diff --git a/gui/builtinGraphs/fitDamageStats/graph.py b/gui/builtinGraphs/fitDamageStats/graph.py index 15f4d2d27..58af602fd 100644 --- a/gui/builtinGraphs/fitDamageStats/graph.py +++ b/gui/builtinGraphs/fitDamageStats/graph.py @@ -75,65 +75,62 @@ class FitDamageStatsGraph(FitGraph): def _distance2dps(self, mainInput, miscInputs, fit, tgt): return self._xDistanceGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, - dmgFunc=self._getDpsPerKey, timeCacheFunc=self._timeCache.prepareDpsData) + dmgFunc=self._getDpsPerKey, timeCachePrepFunc=self._timeCache.prepareDpsData) def _distance2volley(self, mainInput, miscInputs, fit, tgt): return self._xDistanceGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, - dmgFunc=self._getVolleyPerKey, timeCacheFunc=self._timeCache.prepareVolleyData) + dmgFunc=self._getVolleyPerKey, timeCachePrepFunc=self._timeCache.prepareVolleyData) def _distance2damage(self, mainInput, miscInputs, fit, tgt): return self._xDistanceGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, - dmgFunc=self._getDmgPerKey, timeCacheFunc=self._timeCache.prepareDmgData) + dmgFunc=self._getDmgPerKey, timeCachePrepFunc=self._timeCache.prepareDmgData) def _time2dps(self, mainInput, miscInputs, fit, tgt): - def calcDpsTmp(timeDmg): - return floatUnerr(sum(dts.total for dts in timeDmg.values())) - self._timeCache.prepareDpsData(fit, mainInput[1][1]) - return self._composeTimeGraph(mainInput, fit, self._timeCache.getDpsData, calcDpsTmp) + return self._xTimeGetter( + mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, + timeCachePrepFunc=self._timeCache.prepareDpsData, timeCacheGetFunc=self._timeCache.getDpsData) def _time2volley(self, mainInput, miscInputs, fit, tgt): - def calcVolleyTmp(timeDmg): - return floatUnerr(sum(dts.total for dts in timeDmg.values())) - self._timeCache.prepareVolleyData(fit, mainInput[1][1]) - return self._composeTimeGraph(mainInput, fit, self._timeCache.getVolleyData, calcVolleyTmp) + return self._xTimeGetter( + mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, + timeCachePrepFunc=self._timeCache.prepareVolleyData, timeCacheGetFunc=self._timeCache.getVolleyData) def _time2damage(self, mainInput, miscInputs, fit, tgt): - def calcDamageTmp(timeDmg): - return floatUnerr(sum(dt.total for dt in timeDmg.values())) - self._timeCache.prepareDmgData(fit, mainInput[1][1]) - return self._composeTimeGraph(mainInput, fit, self._timeCache.getDmgData, calcDamageTmp) + return self._xTimeGetter( + mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, + timeCachePrepFunc=self._timeCache.prepareDmgData, timeCacheGetFunc=self._timeCache.getDmgData) def _tgtSpeed2dps(self, mainInput, miscInputs, fit, tgt): return self._xTgtSpeedGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, - dmgFunc=self._getDpsPerKey, timeCacheFunc=self._timeCache.prepareDpsData) + dmgFunc=self._getDpsPerKey, timeCachePrepFunc=self._timeCache.prepareDpsData) def _tgtSpeed2volley(self, mainInput, miscInputs, fit, tgt): return self._xTgtSpeedGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, - dmgFunc=self._getVolleyPerKey, timeCacheFunc=self._timeCache.prepareVolleyData) + dmgFunc=self._getVolleyPerKey, timeCachePrepFunc=self._timeCache.prepareVolleyData) def _tgtSpeed2damage(self, mainInput, miscInputs, fit, tgt): return self._xTgtSpeedGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, - dmgFunc=self._getDmgPerKey, timeCacheFunc=self._timeCache.prepareDmgData) + dmgFunc=self._getDmgPerKey, timeCachePrepFunc=self._timeCache.prepareDmgData) def _tgtSigRad2dps(self, mainInput, miscInputs, fit, tgt): return self._xTgtSigRadiusGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, - dmgFunc=self._getDpsPerKey, timeCacheFunc=self._timeCache.prepareDpsData) + dmgFunc=self._getDpsPerKey, timeCachePrepFunc=self._timeCache.prepareDpsData) def _tgtSigRad2volley(self, mainInput, miscInputs, fit, tgt): return self._xTgtSigRadiusGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, - dmgFunc=self._getVolleyPerKey, timeCacheFunc=self._timeCache.prepareVolleyData) + dmgFunc=self._getVolleyPerKey, timeCachePrepFunc=self._timeCache.prepareVolleyData) def _tgtSigRad2damage(self, mainInput, miscInputs, fit, tgt): return self._xTgtSigRadiusGetter( mainInput=mainInput, miscInputs=miscInputs, fit=fit, tgt=tgt, - dmgFunc=self._getDmgPerKey, timeCacheFunc=self._timeCache.prepareDmgData) + dmgFunc=self._getDmgPerKey, timeCachePrepFunc=self._timeCache.prepareDmgData) _getters = { ('distance', 'dps'): _distance2dps, @@ -150,14 +147,14 @@ class FitDamageStatsGraph(FitGraph): ('tgtSigRad', 'damage'): _tgtSigRad2damage} # Point getter helpers - def _xDistanceGetter(self, mainInput, miscInputs, fit, tgt, dmgFunc, timeCacheFunc): + def _xDistanceGetter(self, mainInput, miscInputs, fit, tgt, dmgFunc, timeCachePrepFunc): xs = [] ys = [] tgtSigRadius = tgt.ship.getModifiedItemAttr('signatureRadius') # Process inputs into more convenient form miscInputMap = dict(miscInputs) # Get all data we need for all distances into maps/caches - timeCacheFunc(fit, miscInputMap['time']) + timeCachePrepFunc(fit, miscInputMap['time']) dmgMap = dmgFunc(fit=fit, time=miscInputMap['time']) # Go through distances and calculate distance-dependent data for distance in self._iterLinear(mainInput[1]): @@ -175,14 +172,74 @@ class FitDamageStatsGraph(FitGraph): ys.append(dmg) return xs, ys - def _xTgtSpeedGetter(self, mainInput, miscInputs, fit, tgt, dmgFunc, timeCacheFunc): + def _xTimeGetter(self, mainInput, miscInputs, fit, tgt, timeCachePrepFunc, timeCacheGetFunc): + xs = [] + ys = [] + minTime, maxTime = mainInput[1] + tgtSigRadius = tgt.ship.getModifiedItemAttr('signatureRadius') + # Process inputs into more convenient form + miscInputMap = dict(miscInputs) + # Get all data we need for all times into maps/caches + applicationMap = self._getApplicationPerKey( + fit=fit, + tgt=tgt, + atkSpeed=miscInputMap['atkSpeed'], + atkAngle=miscInputMap['atkAngle'], + distance=miscInputMap['distance'], + tgtSpeed=miscInputMap['tgtSpeed'], + tgtAngle=miscInputMap['tgtAngle'], + tgtSigRadius=tgtSigRadius) + timeCachePrepFunc(fit, maxTime) + timeCache = timeCacheGetFunc(fit) + # Custom iteration for time graph to show all data points + currentDmg = None + currentTime = None + for currentTime in sorted(timeCache): + prevDmg = currentDmg + currentDmgData = timeCache[currentTime] + currentDmg = self._aggregate(dmgMap=currentDmgData, applicationMap=applicationMap).total + if currentTime < minTime: + continue + # First set of data points + if not xs: + # Start at exactly requested time, at last known value + initialDmg = prevDmg or 0 + xs.append(minTime) + ys.append(initialDmg) + # If current time is bigger then starting, extend plot to that time with old value + if currentTime > minTime: + xs.append(currentTime) + ys.append(initialDmg) + # If new value is different, extend it with new point to the new value + if currentDmg != prevDmg: + xs.append(currentTime) + ys.append(currentDmg) + continue + # Last data point + if currentTime >= maxTime: + xs.append(maxTime) + ys.append(prevDmg) + break + # Anything in-between + if currentDmg != prevDmg: + if prevDmg is not None: + xs.append(currentTime) + ys.append(prevDmg) + xs.append(currentTime) + ys.append(currentDmg) + if maxTime > (currentTime or 0): + xs.append(maxTime) + ys.append(currentDmg or 0) + return xs, ys + + def _xTgtSpeedGetter(self, mainInput, miscInputs, fit, tgt, dmgFunc, timeCachePrepFunc): xs = [] ys = [] tgtSigRadius = tgt.ship.getModifiedItemAttr('signatureRadius') # Process inputs into more convenient form miscInputMap = dict(miscInputs) # Get all data we need for all target speeds into maps/caches - timeCacheFunc(fit, miscInputMap['time']) + timeCachePrepFunc(fit, miscInputMap['time']) dmgMap = dmgFunc(fit=fit, time=miscInputMap['time']) # Go through target speeds and calculate distance-dependent data for tgtSpeed in self._iterLinear(mainInput[1]): @@ -200,13 +257,13 @@ class FitDamageStatsGraph(FitGraph): ys.append(dmg) return xs, ys - def _xTgtSigRadiusGetter(self, mainInput, miscInputs, fit, tgt, dmgFunc, timeCacheFunc): + def _xTgtSigRadiusGetter(self, mainInput, miscInputs, fit, tgt, dmgFunc, timeCachePrepFunc): xs = [] ys = [] # Process inputs into more convenient form miscInputMap = dict(miscInputs) # Get all data we need for all target speeds into maps/caches - timeCacheFunc(fit, miscInputMap['time']) + timeCachePrepFunc(fit, miscInputMap['time']) dmgMap = dmgFunc(fit=fit, time=miscInputMap['time']) # Go through target speeds and calculate distance-dependent data for tgtSigRadius in self._iterLinear(mainInput[1]): @@ -330,51 +387,5 @@ class FitDamageStatsGraph(FitGraph): total += dmg * applicationMap.get(key, 1) return total - ############# TO REFACTOR: time graph stuff - def _composeTimeGraph(self, mainInput, fit, cacheFunc, calcFunc): - xs = [] - ys = [] - - minTime, maxTime = mainInput[1] - cache = cacheFunc(fit) - currentDps = None - currentTime = None - for currentTime in sorted(cache): - prevDps = currentDps - currentDps = calcFunc(cache[currentTime]) - if currentTime < minTime: - continue - # First set of data points - if not xs: - # Start at exactly requested time, at last known value - initialDps = prevDps or 0 - xs.append(minTime) - ys.append(initialDps) - # If current time is bigger then starting, extend plot to that time with old value - if currentTime > minTime: - xs.append(currentTime) - ys.append(initialDps) - # If new value is different, extend it with new point to the new value - if currentDps != prevDps: - xs.append(currentTime) - ys.append(currentDps) - continue - # Last data point - if currentTime >= maxTime: - xs.append(maxTime) - ys.append(prevDps) - break - # Anything in-between - if currentDps != prevDps: - if prevDps is not None: - xs.append(currentTime) - ys.append(prevDps) - xs.append(currentTime) - ys.append(currentDps) - if maxTime > (currentTime or 0): - xs.append(maxTime) - ys.append(currentDps or 0) - return xs, ys - FitDamageStatsGraph.register() From 417e478d271f9005da2ac11fa3a6b2eedb783204 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 4 Jul 2019 18:37:26 +0300 Subject: [PATCH 91/93] Add smartbombs to calculation --- gui/builtinGraphs/fitDamageStats/calc.py | 9 +++++++++ gui/builtinGraphs/fitDamageStats/graph.py | 7 +++++-- gui/graphFrame/frame.py | 5 ++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/gui/builtinGraphs/fitDamageStats/calc.py b/gui/builtinGraphs/fitDamageStats/calc.py index f31d398aa..883c6f220 100644 --- a/gui/builtinGraphs/fitDamageStats/calc.py +++ b/gui/builtinGraphs/fitDamageStats/calc.py @@ -114,6 +114,15 @@ def getFighterAbilityMult(fighter, ability, fit, distance, tgtSpeed, tgtSigRadiu return mult +def getSmartbombMult(mod, distance): + modRange = mod.maxRange + if modRange is None: + return 0 + if distance > modRange: + return 0 + return 1 + + # Turret-specific @lru_cache(maxsize=50) def _calcTurretMult(chanceToHit): diff --git a/gui/builtinGraphs/fitDamageStats/graph.py b/gui/builtinGraphs/fitDamageStats/graph.py index 58af602fd..56514e463 100644 --- a/gui/builtinGraphs/fitDamageStats/graph.py +++ b/gui/builtinGraphs/fitDamageStats/graph.py @@ -20,11 +20,10 @@ import eos.config from eos.const import FittingHardpoint -from eos.utils.float import floatUnerr from eos.utils.spoolSupport import SpoolType, SpoolOptions from eos.utils.stats import DmgTypes from gui.builtinGraphs.base import FitGraph, XDef, YDef, Input, VectorDef -from .calc import getTurretMult, getLauncherMult, getDroneMult, getFighterAbilityMult +from .calc import getTurretMult, getLauncherMult, getDroneMult, getFighterAbilityMult, getSmartbombMult from .timeCache import TimeCache @@ -352,6 +351,10 @@ class FitDamageStatsGraph(FitGraph): distance=distance, tgtSpeed=tgtSpeed, tgtSigRadius=tgtSigRadius) + elif mod.item.group.name == 'Smart Bomb': + applicationMap[mod] = getSmartbombMult( + mod=mod, + distance=distance) for drone in fit.drones: if not drone.isDealingDamage(): continue diff --git a/gui/graphFrame/frame.py b/gui/graphFrame/frame.py index 716d934bb..92f58b25d 100644 --- a/gui/graphFrame/frame.py +++ b/gui/graphFrame/frame.py @@ -216,7 +216,10 @@ class GraphFrame(wx.Frame): max_y = max(max_y, max_y_this) self.subplot.plot(xs, ys) - legend.append('{} ({})'.format(fit.name, fit.ship.item.getShortName())) + if target is None: + legend.append('{} ({})'.format(fit.name, fit.ship.item.getShortName())) + else: + legend.append('{} ({}) vs {} ({})'.format(fit.name, fit.ship.item.getShortName(), target.name, target.ship.item.getShortName())) except Exception as ex: pyfalog.warning('Invalid values in "{0}"', fit.name) self.canvas.draw() From 78b6eb42838a8cb99df767101e92471e9b799c81 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 4 Jul 2019 19:14:46 +0300 Subject: [PATCH 92/93] Add regular and guided bombs to graphs --- gui/builtinGraphs/fitDamageStats/calc.py | 52 +++++++++++++++++++---- gui/builtinGraphs/fitDamageStats/graph.py | 19 +++++++-- 2 files changed, 59 insertions(+), 12 deletions(-) diff --git a/gui/builtinGraphs/fitDamageStats/calc.py b/gui/builtinGraphs/fitDamageStats/calc.py index 883c6f220..3ce00378c 100644 --- a/gui/builtinGraphs/fitDamageStats/calc.py +++ b/gui/builtinGraphs/fitDamageStats/calc.py @@ -55,6 +55,49 @@ def getLauncherMult(mod, fit, distance, tgtSpeed, tgtSigRadius): return mult +def getSmartbombMult(mod, distance): + modRange = mod.maxRange + if modRange is None: + return 0 + if distance > modRange: + return 0 + return 1 + + +def getBombMult(mod, fit, tgt, distance, tgtSigRadius): + modRange = mod.maxRange + if modRange is None: + return 0 + blastRadius = mod.getModifiedChargeAttr('explosionRange') + atkRadius = fit.ship.getModifiedItemAttr('radius') + tgtRadius = tgt.ship.getModifiedItemAttr('radius') + # Bomb starts in the center of the ship + # Also here we assume that it affects target as long as blast + # touches its surface, not center - I did not check this + if distance < max(0, modRange - atkRadius - tgtRadius - blastRadius): + return 0 + if distance > max(0, modRange - atkRadius + tgtRadius + blastRadius): + return 0 + eR = mod.getModifiedChargeAttr('aoeCloudSize') + if eR == 0: + return 1 + else: + return min(1, tgtSigRadius / eR) + + +def getGuidedBombMult(mod, fit, distance, tgtSigRadius): + modRange = mod.maxRange + if modRange is None: + return 0 + if distance > modRange - fit.ship.getModifiedItemAttr('radius'): + return 0 + eR = mod.getModifiedChargeAttr('aoeCloudSize') + if eR == 0: + return 1 + else: + return min(1, tgtSigRadius / eR) + + def getDroneMult(drone, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAngle, tgtSigRadius): if distance > fit.extraAttributes['droneControlRange']: return 0 @@ -114,15 +157,6 @@ def getFighterAbilityMult(fighter, ability, fit, distance, tgtSpeed, tgtSigRadiu return mult -def getSmartbombMult(mod, distance): - modRange = mod.maxRange - if modRange is None: - return 0 - if distance > modRange: - return 0 - return 1 - - # Turret-specific @lru_cache(maxsize=50) def _calcTurretMult(chanceToHit): diff --git a/gui/builtinGraphs/fitDamageStats/graph.py b/gui/builtinGraphs/fitDamageStats/graph.py index 56514e463..3aaf2758e 100644 --- a/gui/builtinGraphs/fitDamageStats/graph.py +++ b/gui/builtinGraphs/fitDamageStats/graph.py @@ -23,7 +23,7 @@ from eos.const import FittingHardpoint from eos.utils.spoolSupport import SpoolType, SpoolOptions from eos.utils.stats import DmgTypes from gui.builtinGraphs.base import FitGraph, XDef, YDef, Input, VectorDef -from .calc import getTurretMult, getLauncherMult, getDroneMult, getFighterAbilityMult, getSmartbombMult +from .calc import getTurretMult, getLauncherMult, getDroneMult, getFighterAbilityMult, getSmartbombMult, getBombMult, getGuidedBombMult from .timeCache import TimeCache @@ -351,10 +351,23 @@ class FitDamageStatsGraph(FitGraph): distance=distance, tgtSpeed=tgtSpeed, tgtSigRadius=tgtSigRadius) - elif mod.item.group.name == 'Smart Bomb': + elif mod.item.group.name in ('Smart Bomb', 'Structure Area Denial Module'): applicationMap[mod] = getSmartbombMult( mod=mod, distance=distance) + elif mod.item.group.name == 'Missile Launcher Bomb': + applicationMap[mod] = getBombMult( + mod=mod, + fit=fit, + tgt=tgt, + distance=distance, + tgtSigRadius=tgtSigRadius) + elif mod.item.group.name == 'Structure Guided Bomb Launcher': + applicationMap[mod] = getGuidedBombMult( + mod=mod, + fit=fit, + distance=distance, + tgtSigRadius=tgtSigRadius) for drone in fit.drones: if not drone.isDealingDamage(): continue @@ -387,7 +400,7 @@ class FitDamageStatsGraph(FitGraph): def _aggregate(self, dmgMap, applicationMap): total = DmgTypes(0, 0, 0, 0) for key, dmg in dmgMap.items(): - total += dmg * applicationMap.get(key, 1) + total += dmg * applicationMap.get(key, 0) return total From 1f94b28b878d99922e30546269d03a9e2d3c36d3 Mon Sep 17 00:00:00 2001 From: DarkPhoenix Date: Thu, 4 Jul 2019 19:30:02 +0300 Subject: [PATCH 93/93] Add fighter bomb support --- eos/graph/__init__.py | 18 --- eos/graph/base.py | 76 ------------ eos/graph/fitDpsVsRange.py | 149 ----------------------- gui/builtinGraphs/fitDamageStats/calc.py | 25 +++- 4 files changed, 19 insertions(+), 249 deletions(-) delete mode 100644 eos/graph/__init__.py delete mode 100644 eos/graph/base.py delete mode 100644 eos/graph/fitDpsVsRange.py diff --git a/eos/graph/__init__.py b/eos/graph/__init__.py deleted file mode 100644 index 8ac11a9c5..000000000 --- a/eos/graph/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -# =============================================================================== -# Copyright (C) 2010 Diego Duclos -# -# This file is part of eos. -# -# eos is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# eos 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with eos. If not, see . -# =============================================================================== diff --git a/eos/graph/base.py b/eos/graph/base.py deleted file mode 100644 index ed1fd3445..000000000 --- a/eos/graph/base.py +++ /dev/null @@ -1,76 +0,0 @@ -# =============================================================================== -# Copyright (C) 2010 Diego Duclos -# -# This file is part of eos. -# -# eos is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# eos 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with eos. If not, see . -# =============================================================================== - - -import math -from abc import ABCMeta, abstractmethod - - -class Graph(metaclass=ABCMeta): - - def __init__(self): - self._cache = {} - - - - ### Old stuff - - def getYForX(self, x, miscInputs, xSpec, ySpec, fit, tgt): - raise NotImplementedError - - def _xIter(self, fit, extraData, xRange, xAmount): - rangeLow, rangeHigh = self._limitXRange(xRange, fit, extraData) - # Amount is amount of ranges between points here, not amount of points - step = (rangeHigh - rangeLow) / xAmount - if step == 0: - yield xRange[0] - else: - current = rangeLow - # Take extra half step to make sure end of range is always included - # despite any possible float errors - while current <= (rangeHigh + step / 2): - yield current - current += step - - def _limitXRange(self, xRange, fit, extraData): - rangeLow, rangeHigh = sorted(xRange) - limitLow, limitHigh = self._getXLimits(fit, extraData) - rangeLow = max(limitLow, rangeLow) - rangeHigh = min(limitHigh, rangeHigh) - return rangeLow, rangeHigh - - def _getInputLimits(self, inputHandle, inputUnit, fit): - return -math.inf, math.inf - - def clearCache(self, key=None): - if key is None: - self._cache.clear() - elif key in self._cache: - del self._cache[key] - - -class SmoothGraph(Graph, metaclass=ABCMeta): - - def getPlotPoints(self, mainInput, miscInputs, xSpec, ySpec, fit, tgt): - xs = [] - ys = [] - for x in self._xIter(fit, extraData, xRange, xAmount): - xs.append(x) - ys.append(self.getYForX(fit, extraData, x)) - return xs, ys diff --git a/eos/graph/fitDpsVsRange.py b/eos/graph/fitDpsVsRange.py deleted file mode 100644 index 41fa6e25d..000000000 --- a/eos/graph/fitDpsVsRange.py +++ /dev/null @@ -1,149 +0,0 @@ -# =============================================================================== -# Copyright (C) 2010 Diego Duclos -# -# This file is part of eos. -# -# eos is free software: you can redistribute it and/or modify -# it under the terms of the GNU Lesser General Public License as published by -# the Free Software Foundation, either version 2 of the License, or -# (at your option) any later version. -# -# eos 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 Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public License -# along with eos. If not, see . -# =============================================================================== - - -from math import exp, log, radians, sin, inf - -from logbook import Logger - -import eos.config -from eos.const import FittingHardpoint, FittingModuleState -from eos.utils.spoolSupport import SpoolType, SpoolOptions -from .base import SmoothGraph - - -pyfalog = Logger(__name__) - - -class FitDpsVsRangeGraph(SmoothGraph): - - def getYForX(self, fit, extraData, distance): - tgtSpeed = extraData['speed'] - tgtSigRad = extraData['signatureRadius'] if extraData['signatureRadius'] is not None else inf - angle = extraData['angle'] - tgtSigRadMods = [] - tgtSpeedMods = [] - total = 0 - distance = distance * 1000 - - for mod in fit.modules: - if not mod.isEmpty and mod.state >= FittingModuleState.ACTIVE: - if "remoteTargetPaintFalloff" in mod.item.effects or "structureModuleEffectTargetPainter" in mod.item.effects: - tgtSigRadMods.append( - 1 + (mod.getModifiedItemAttr("signatureRadiusBonus") / 100) - * self.calculateModuleMultiplier(mod, distance)) - if "remoteWebifierFalloff" in mod.item.effects or "structureModuleEffectStasisWebifier" in mod.item.effects: - if distance <= mod.getModifiedItemAttr("maxRange"): - tgtSpeedMods.append(1 + (mod.getModifiedItemAttr("speedFactor") / 100)) - elif mod.getModifiedItemAttr("falloffEffectiveness") > 0: - # I am affected by falloff - tgtSpeedMods.append( - 1 + (mod.getModifiedItemAttr("speedFactor") / 100) * - self.calculateModuleMultiplier(mod, distance)) - - tgtSpeed = self.penalizeModChain(tgtSpeed, tgtSpeedMods) - tgtSigRad = self.penalizeModChain(tgtSigRad, tgtSigRadMods) - attRad = fit.ship.getModifiedItemAttr('radius', 0) - defaultSpoolValue = eos.config.settings['globalDefaultSpoolupPercentage'] - - for mod in fit.modules: - dps = mod.getDps(targetResists=fit.targetResists, spoolOptions=SpoolOptions(SpoolType.SCALE, defaultSpoolValue, False)).total - if mod.hardpoint == FittingHardpoint.TURRET: - if mod.state >= FittingModuleState.ACTIVE: - total += dps * self.calculateTurretMultiplier(fit, mod, distance, angle, tgtSpeed, tgtSigRad) - - elif mod.hardpoint == FittingHardpoint.MISSILE: - if mod.state >= FittingModuleState.ACTIVE and mod.maxRange is not None and (mod.maxRange - attRad) >= distance: - total += dps * self.calculateMissileMultiplier(mod, tgtSpeed, tgtSigRad) - - if distance <= fit.extraAttributes['droneControlRange']: - for drone in fit.drones: - multiplier = 1 if drone.getModifiedItemAttr('maxVelocity') > 1 else self.calculateTurretMultiplier( - fit, drone, distance, angle, tgtSpeed, tgtSigRad) - dps = drone.getDps(targetResists=fit.targetResists).total - total += dps * multiplier - - # this is janky as fuck - for fighter in fit.fighters: - if not fighter.active: - continue - fighterDpsMap = fighter.getDpsPerEffect(targetResists=fit.targetResists) - for ability in fighter.abilities: - if ability.dealsDamage and ability.active: - if ability.effectID not in fighterDpsMap: - continue - multiplier = self.calculateFighterMissileMultiplier(tgtSpeed, tgtSigRad, ability) - dps = fighterDpsMap[ability.effectID].total - total += dps * multiplier - - return total - - @staticmethod - def calculateMissileMultiplier(mod, tgtSpeed, tgtSigRad): - explosionRadius = mod.getModifiedChargeAttr('aoeCloudSize') - explosionVelocity = mod.getModifiedChargeAttr('aoeVelocity') - damageReductionFactor = mod.getModifiedChargeAttr('aoeDamageReductionFactor') - - sigRadiusFactor = tgtSigRad / explosionRadius - if tgtSpeed: - velocityFactor = (explosionVelocity / explosionRadius * tgtSigRad / tgtSpeed) ** damageReductionFactor - else: - velocityFactor = 1 - - return min(sigRadiusFactor, velocityFactor, 1) - - @staticmethod - def calculateFighterMissileMultiplier(tgtSpeed, tgtSigRad, ability): - prefix = ability.attrPrefix - - explosionRadius = ability.fighter.getModifiedItemAttr('{}ExplosionRadius'.format(prefix)) - explosionVelocity = ability.fighter.getModifiedItemAttr('{}ExplosionVelocity'.format(prefix)) - damageReductionFactor = ability.fighter.getModifiedItemAttr('{}ReductionFactor'.format(prefix), None) - - # the following conditionals are because CCP can't keep a decent naming convention, as if fighter implementation - # wasn't already fucked. - if damageReductionFactor is None: - damageReductionFactor = ability.fighter.getModifiedItemAttr('{}DamageReductionFactor'.format(prefix)) - - damageReductionSensitivity = ability.fighter.getModifiedItemAttr('{}ReductionSensitivity'.format(prefix), None) - if damageReductionSensitivity is None: - damageReductionSensitivity = ability.fighter.getModifiedItemAttr('{}DamageReductionSensitivity'.format(prefix)) - - sigRadiusFactor = tgtSigRad / explosionRadius - - if tgtSpeed: - velocityFactor = (explosionVelocity / explosionRadius * tgtSigRad / tgtSpeed) ** ( - log(damageReductionFactor) / log(damageReductionSensitivity)) - else: - velocityFactor = 1 - - return min(sigRadiusFactor, velocityFactor, 1) - - @staticmethod - def penalizeModChain(value, mods): - mods.sort(key=lambda v: -abs(v - 1)) - try: - for i in range(len(mods)): - bonus = mods[i] - value *= 1 + (bonus - 1) * exp(- i ** 2 / 7.1289) - return value - except Exception as e: - pyfalog.critical('Caught exception when penalizing modifier chain.') - pyfalog.critical(e) - return value diff --git a/gui/builtinGraphs/fitDamageStats/calc.py b/gui/builtinGraphs/fitDamageStats/calc.py index 3ce00378c..ca9f85003 100644 --- a/gui/builtinGraphs/fitDamageStats/calc.py +++ b/gui/builtinGraphs/fitDamageStats/calc.py @@ -78,11 +78,9 @@ def getBombMult(mod, fit, tgt, distance, tgtSigRadius): return 0 if distance > max(0, modRange - atkRadius + tgtRadius + blastRadius): return 0 - eR = mod.getModifiedChargeAttr('aoeCloudSize') - if eR == 0: - return 1 - else: - return min(1, tgtSigRadius / eR) + return _calcBombFactor( + atkEr=mod.getModifiedChargeAttr('aoeCloudSize'), + tgtSigRadius=tgtSigRadius) def getGuidedBombMult(mod, fit, distance, tgtSigRadius): @@ -132,6 +130,13 @@ def getDroneMult(drone, fit, tgt, atkSpeed, atkAngle, distance, tgtSpeed, tgtAng def getFighterAbilityMult(fighter, ability, fit, distance, tgtSpeed, tgtSigRadius): fighterSpeed = fighter.getModifiedItemAttr('maxVelocity') attrPrefix = ability.attrPrefix + # It's bomb attack + if attrPrefix == 'fighterAbilityLaunchBomb': + # Just assume we can land bomb anywhere + return _calcBombFactor( + atkEr=fighter.getModifiedChargeAttr('aoeCloudSize'), + tgtSigRadius=tgtSigRadius) + # It's regular missile-based attack if fighterSpeed >= tgtSpeed: rangeFactor = 1 # Same as with drones, if fighters are slower - put them to center of @@ -213,7 +218,8 @@ def _calcMissileFactor(atkEr, atkEv, atkDrf, tgtSpeed, tgtSigRadius): """Missile application.""" factors = [1] # "Slow" part - factors.append(tgtSigRadius / atkEr) + if atkEr > 0: + factors.append(tgtSigRadius / atkEr) # "Fast" part if tgtSpeed > 0: factors.append(((atkEv * tgtSigRadius) / (atkEr * tgtSpeed)) ** atkDrf) @@ -233,3 +239,10 @@ def _calcAggregatedDrf(reductionFactor, reductionSensitivity): def _calcRangeFactor(atkOptimalRange, atkFalloffRange, distance): """Range strength/chance factor, applicable to guns, ewar, RRs, etc.""" return 0.5 ** ((max(0, distance - atkOptimalRange) / atkFalloffRange) ** 2) + + +def _calcBombFactor(atkEr, tgtSigRadius): + if atkEr == 0: + return 1 + else: + return min(1, tgtSigRadius / atkEr)