Browse Source

Merge branch 'master' into master

Konstantinos Karmas 4 years ago
parent
commit
374a6b7b68

+ 13 - 0
.github/no-response.yml

@@ -0,0 +1,13 @@
+# Configuration for probot-no-response - https://github.com/probot/no-response
+
+# Number of days of inactivity before an Issue is closed for lack of response
+daysUntilClose: 14
+# Label requiring a response
+responseRequiredLabel: 'Status: Needs Info'
+# Comment to post when closing an Issue for lack of response. Set to `false` to disable
+closeComment: >
+  This issue has been automatically closed because there has been no response
+  to our request for more information from the original author. With only the
+  information that is currently in the issue, we don't have enough information
+  to take action. Please reach out if you have or find the answers we need so
+  that we can investigate further.

+ 3 - 1
README.md

@@ -1,6 +1,8 @@
 Cura
 ====
-This is the new, shiny frontend for Cura. Check [daid/LegacyCura](https://github.com/daid/LegacyCura) for the legacy Cura that everyone knows and loves/hates. We re-worked the whole GUI code at Ultimaker, because the old code started to become unmaintainable.
+Ultimaker Cura is a state-of-the-art slicer application to prepare your 3D models for printing with a 3D printer. With hundreds of settings and hundreds of community-managed print profiles, Ultimaker Cura is sure to lead your next project to a success.
+
+![Screenshot](screenshot.png)
 
 Logging Issues
 ------------

+ 13 - 9
cmake/mod_bundled_packages_json.py

@@ -11,11 +11,13 @@ import os
 import sys
 
 
-## Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths.
-#
-#  \param work_dir The directory to look for JSON files recursively.
-#  \return A list of JSON files in absolute paths that are found in the given directory.
 def find_json_files(work_dir: str) -> list:
+    """Finds all JSON files in the given directory recursively and returns a list of those files in absolute paths.
+
+    :param work_dir: The directory to look for JSON files recursively.
+    :return: A list of JSON files in absolute paths that are found in the given directory.
+    """
+
     json_file_list = []
     for root, dir_names, file_names in os.walk(work_dir):
         for file_name in file_names:
@@ -24,12 +26,14 @@ def find_json_files(work_dir: str) -> list:
     return json_file_list
 
 
-## Removes the given entries from the given JSON file. The file will modified in-place.
-#
-#  \param file_path The JSON file to modify.
-#  \param entries A list of strings as entries to remove.
-#  \return None
 def remove_entries_from_json_file(file_path: str, entries: list) -> None:
+    """Removes the given entries from the given JSON file. The file will modified in-place.
+
+    :param file_path: The JSON file to modify.
+    :param entries: A list of strings as entries to remove.
+    :return: None
+    """
+
     try:
         with open(file_path, "r", encoding = "utf-8") as f:
             package_dict = json.load(f, object_hook = collections.OrderedDict)

+ 68 - 30
cura/API/Account.py

@@ -10,7 +10,7 @@ from UM.Message import Message
 from UM.i18n import i18nCatalog
 from cura.OAuth2.AuthorizationService import AuthorizationService
 from cura.OAuth2.Models import OAuth2Settings
-from cura.UltimakerCloud import UltimakerCloudAuthentication
+from cura.UltimakerCloud import UltimakerCloudConstants
 
 if TYPE_CHECKING:
     from cura.CuraApplication import CuraApplication
@@ -23,24 +23,29 @@ class SyncState:
     SYNCING = 0
     SUCCESS = 1
     ERROR = 2
+    IDLE = 3
 
-
-##  The account API provides a version-proof bridge to use Ultimaker Accounts
-#
-#   Usage:
-#       ``from cura.API import CuraAPI
-#       api = CuraAPI()
-#       api.account.login()
-#       api.account.logout()
-#       api.account.userProfile # Who is logged in``
-#
 class Account(QObject):
+    """The account API provides a version-proof bridge to use Ultimaker Accounts
+
+    Usage:
+
+    .. code-block:: python
+
+      from cura.API import CuraAPI
+      api = CuraAPI()
+      api.account.login()
+      api.account.logout()
+      api.account.userProfile    # Who is logged in
+    """
+
     # The interval in which sync services are automatically triggered
     SYNC_INTERVAL = 30.0  # seconds
     Q_ENUMS(SyncState)
 
-    # Signal emitted when user logged in or out.
     loginStateChanged = pyqtSignal(bool)
+    """Signal emitted when user logged in or out"""
+
     accessTokenChanged = pyqtSignal()
     syncRequested = pyqtSignal()
     """Sync services may connect to this signal to receive sync triggers.
@@ -50,6 +55,7 @@ class Account(QObject):
     """
     lastSyncDateTimeChanged = pyqtSignal()
     syncStateChanged = pyqtSignal(int)  # because SyncState is an int Enum
+    manualSyncEnabledChanged = pyqtSignal(bool)
 
     def __init__(self, application: "CuraApplication", parent = None) -> None:
         super().__init__(parent)
@@ -58,11 +64,12 @@ class Account(QObject):
 
         self._error_message = None  # type: Optional[Message]
         self._logged_in = False
-        self._sync_state = SyncState.SUCCESS
+        self._sync_state = SyncState.IDLE
+        self._manual_sync_enabled = False
         self._last_sync_str = "-"
 
         self._callback_port = 32118
-        self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
+        self._oauth_root = UltimakerCloudConstants.CuraCloudAccountAPIRoot
 
         self._oauth_settings = OAuth2Settings(
             OAUTH_SERVER_URL= self._oauth_root,
@@ -96,6 +103,11 @@ class Account(QObject):
         self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged)
         self._authorization_service.loadAuthDataFromPreferences()
 
+
+    @pyqtProperty(int, notify=syncStateChanged)
+    def syncState(self):
+        return self._sync_state
+
     def setSyncState(self, service_name: str, state: int) -> None:
         """ Can be used to register sync services and update account sync states
 
@@ -105,17 +117,19 @@ class Account(QObject):
         :param service_name: A unique name for your service, such as `plugins` or `backups`
         :param state: One of SyncState
         """
-
         prev_state = self._sync_state
 
         self._sync_services[service_name] = state
 
         if any(val == SyncState.SYNCING for val in self._sync_services.values()):
             self._sync_state = SyncState.SYNCING
+            self._setManualSyncEnabled(False)
         elif any(val == SyncState.ERROR for val in self._sync_services.values()):
             self._sync_state = SyncState.ERROR
+            self._setManualSyncEnabled(True)
         else:
             self._sync_state = SyncState.SUCCESS
+            self._setManualSyncEnabled(False)
 
         if self._sync_state != prev_state:
             self.syncStateChanged.emit(self._sync_state)
@@ -132,9 +146,10 @@ class Account(QObject):
     def _onAccessTokenChanged(self):
         self.accessTokenChanged.emit()
 
-    ## Returns a boolean indicating whether the given authentication is applied against staging or not.
     @property
     def is_staging(self) -> bool:
+        """Indication whether the given authentication is applied against staging or not."""
+
         return "staging" in self._oauth_root
 
     @pyqtProperty(bool, notify=loginStateChanged)
@@ -157,11 +172,31 @@ class Account(QObject):
             self._logged_in = logged_in
             self.loginStateChanged.emit(logged_in)
             if logged_in:
-                self.sync()
+                self._setManualSyncEnabled(False)
+                self._sync()
             else:
                 if self._update_timer.isActive():
                     self._update_timer.stop()
 
+    def _sync(self) -> None:
+        """Signals all sync services to start syncing
+
+        This can be considered a forced sync: even when a
+        sync is currently running, a sync will be requested.
+        """
+
+        if self._update_timer.isActive():
+            self._update_timer.stop()
+        elif self._sync_state == SyncState.SYNCING:
+            Logger.warning("Starting a new sync while previous sync was not completed\n{}", str(self._sync_services))
+
+        self.syncRequested.emit()
+
+    def _setManualSyncEnabled(self, enabled: bool) -> None:
+        if self._manual_sync_enabled != enabled:
+            self._manual_sync_enabled = enabled
+            self.manualSyncEnabledChanged.emit(enabled)
+
     @pyqtSlot()
     @pyqtSlot(bool)
     def login(self, force_logout_before_login: bool = False) -> None:
@@ -199,10 +234,10 @@ class Account(QObject):
     def accessToken(self) -> Optional[str]:
         return self._authorization_service.getAccessToken()
 
-    #   Get the profile of the logged in user
-    #   @returns None if no user is logged in, a dict containing user_id, username and profile_image_url
     @pyqtProperty("QVariantMap", notify = loginStateChanged)
     def userProfile(self) -> Optional[Dict[str, Optional[str]]]:
+        """None if no user is logged in otherwise the logged in  user as a dict containing containing user_id, username and profile_image_url """
+
         user_profile = self._authorization_service.getUserProfile()
         if not user_profile:
             return None
@@ -212,20 +247,23 @@ class Account(QObject):
     def lastSyncDateTime(self) -> str:
         return self._last_sync_str
 
-    @pyqtSlot()
-    def sync(self) -> None:
-        """Signals all sync services to start syncing
+    @pyqtProperty(bool, notify=manualSyncEnabledChanged)
+    def manualSyncEnabled(self) -> bool:
+        return self._manual_sync_enabled
 
-        This can be considered a forced sync: even when a
-        sync is currently running, a sync will be requested.
-        """
+    @pyqtSlot()
+    @pyqtSlot(bool)
+    def sync(self, user_initiated: bool = False) -> None:
+        if user_initiated:
+            self._setManualSyncEnabled(False)
 
-        if self._update_timer.isActive():
-            self._update_timer.stop()
-        elif self._sync_state == SyncState.SYNCING:
-            Logger.warning("Starting a new sync while previous sync was not completed\n{}", str(self._sync_services))
+        self._sync()
 
-        self.syncRequested.emit()
+    @pyqtSlot()
+    def popupOpened(self) -> None:
+        self._setManualSyncEnabled(True)
+        self._sync_state = SyncState.IDLE
+        self.syncStateChanged.emit(self._sync_state)
 
     @pyqtSlot()
     def logout(self) -> None:

+ 24 - 15
cura/API/Backups.py

@@ -8,28 +8,37 @@ if TYPE_CHECKING:
     from cura.CuraApplication import CuraApplication
 
 
-##  The back-ups API provides a version-proof bridge between Cura's
-#   BackupManager and plug-ins that hook into it.
-#
-#   Usage:
-#       ``from cura.API import CuraAPI
-#       api = CuraAPI()
-#       api.backups.createBackup()
-#       api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})``
 class Backups:
+    """The back-ups API provides a version-proof bridge between Cura's
+
+    BackupManager and plug-ins that hook into it.
+
+    Usage:
+
+    .. code-block:: python
+
+       from cura.API import CuraAPI
+       api = CuraAPI()
+       api.backups.createBackup()
+       api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})
+    """
 
     def __init__(self, application: "CuraApplication") -> None:
         self.manager = BackupsManager(application)
 
-    ##  Create a new back-up using the BackupsManager.
-    #   \return Tuple containing a ZIP file with the back-up data and a dict
-    #   with metadata about the back-up.
     def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]:
+        """Create a new back-up using the BackupsManager.
+
+        :return: Tuple containing a ZIP file with the back-up data and a dict with metadata about the back-up.
+        """
+
         return self.manager.createBackup()
 
-    ##  Restore a back-up using the BackupsManager.
-    #   \param zip_file A ZIP file containing the actual back-up data.
-    #   \param meta_data Some metadata needed for restoring a back-up, like the
-    #   Cura version number.
     def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None:
+        """Restore a back-up using the BackupsManager.
+
+        :param zip_file: A ZIP file containing the actual back-up data.
+        :param meta_data: Some metadata needed for restoring a back-up, like the Cura version number.
+        """
+
         return self.manager.restoreBackup(zip_file, meta_data)

+ 41 - 0
cura/API/ConnectionStatus.py

@@ -0,0 +1,41 @@
+from typing import Optional
+
+from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
+
+from UM.TaskManagement.HttpRequestManager import HttpRequestManager
+
+
+class ConnectionStatus(QObject):
+    """Provides an estimation of whether internet is reachable
+
+    Estimation is updated with every request through HttpRequestManager.
+    Acts as a proxy to HttpRequestManager.internetReachableChanged without
+    exposing the HttpRequestManager in its entirety.
+    """
+
+    __instance = None  # type: Optional[ConnectionStatus]
+
+    internetReachableChanged = pyqtSignal()
+
+    @classmethod
+    def getInstance(cls, *args, **kwargs) -> "ConnectionStatus":
+        if cls.__instance is None:
+            cls.__instance = cls(*args, **kwargs)
+        return cls.__instance
+
+    def __init__(self, parent: Optional["QObject"] = None) -> None:
+        super().__init__(parent)
+
+        manager = HttpRequestManager.getInstance()
+        self._is_internet_reachable = manager.isInternetReachable  # type: bool
+        manager.internetReachableChanged.connect(self._onInternetReachableChanged)
+
+    @pyqtProperty(bool, notify = internetReachableChanged)
+    def isInternetReachable(self) -> bool:
+        return self._is_internet_reachable
+
+    def _onInternetReachableChanged(self, reachable: bool):
+        if reachable != self._is_internet_reachable:
+            self._is_internet_reachable = reachable
+            self.internetReachableChanged.emit()
+

+ 30 - 19
cura/API/Interface/Settings.py

@@ -7,32 +7,43 @@ if TYPE_CHECKING:
     from cura.CuraApplication import CuraApplication
 
 
-##  The Interface.Settings API provides a version-proof bridge between Cura's
-#   (currently) sidebar UI and plug-ins that hook into it.
-#
-#   Usage:
-#       ``from cura.API import CuraAPI
-#       api = CuraAPI()
-#       api.interface.settings.getContextMenuItems()
-#       data = {
-#           "name": "My Plugin Action",
-#           "iconName": "my-plugin-icon",
-#           "actions": my_menu_actions,
-#           "menu_item": MyPluginAction(self)
-#       }
-#       api.interface.settings.addContextMenuItem(data)``
-
 class Settings:
+    """The Interface.Settings API provides a version-proof bridge
+     between Cura's
+
+    (currently) sidebar UI and plug-ins that hook into it.
+
+    Usage:
+
+    .. code-block:: python
+
+       from cura.API import CuraAPI
+       api = CuraAPI()
+       api.interface.settings.getContextMenuItems()
+       data = {
+       "name": "My Plugin Action",
+       "iconName": "my-plugin-icon",
+       "actions": my_menu_actions,
+       "menu_item": MyPluginAction(self)
+       }
+       api.interface.settings.addContextMenuItem(data)
+    """
 
     def __init__(self, application: "CuraApplication") -> None:
         self.application = application
 
-    ##  Add items to the sidebar context menu.
-    #   \param menu_item dict containing the menu item to add.
     def addContextMenuItem(self, menu_item: dict) -> None:
+        """Add items to the sidebar context menu.
+
+        :param menu_item: dict containing the menu item to add.
+        """
+
         self.application.addSidebarCustomMenuItem(menu_item)
 
-    ##  Get all custom items currently added to the sidebar context menu.
-    #   \return List containing all custom context menu items.
     def getContextMenuItems(self) -> list:
+        """Get all custom items currently added to the sidebar context menu.
+
+        :return: List containing all custom context menu items.
+        """
+
         return self.application.getSidebarCustomMenuItems()

+ 15 - 11
cura/API/Interface/__init__.py

@@ -9,18 +9,22 @@ if TYPE_CHECKING:
     from cura.CuraApplication import CuraApplication
 
 
-##  The Interface class serves as a common root for the specific API
-#   methods for each interface element.
-#
-#   Usage:
-#       ``from cura.API import CuraAPI
-#       api = CuraAPI()
-#       api.interface.settings.addContextMenuItem()
-#       api.interface.viewport.addOverlay() # Not implemented, just a hypothetical
-#       api.interface.toolbar.getToolButtonCount() # Not implemented, just a hypothetical
-#       # etc.``
-
 class Interface:
+    """The Interface class serves as a common root for the specific API
+
+    methods for each interface element.
+
+    Usage:
+
+    .. code-block:: python
+
+       from cura.API import CuraAPI
+       api = CuraAPI()
+       api.interface.settings.addContextMenuItem()
+       api.interface.viewport.addOverlay()    # Not implemented, just a hypothetical
+       api.interface.toolbar.getToolButtonCount()   #  Not implemented, just a hypothetical
+       # etc
+    """
 
     def __init__(self, application: "CuraApplication") -> None:
         # API methods specific to the settings portion of the UI

+ 20 - 8
cura/API/__init__.py

@@ -5,6 +5,7 @@ from typing import Optional, TYPE_CHECKING
 from PyQt5.QtCore import QObject, pyqtProperty
 
 from cura.API.Backups import Backups
+from cura.API.ConnectionStatus import ConnectionStatus
 from cura.API.Interface import Interface
 from cura.API.Account import Account
 
@@ -12,13 +13,14 @@ if TYPE_CHECKING:
     from cura.CuraApplication import CuraApplication
 
 
-##  The official Cura API that plug-ins can use to interact with Cura.
-#
-#   Python does not technically prevent talking to other classes as well, but
-#   this API provides a version-safe interface with proper deprecation warnings
-#   etc. Usage of any other methods than the ones provided in this API can cause
-#   plug-ins to be unstable.
 class CuraAPI(QObject):
+    """The official Cura API that plug-ins can use to interact with Cura.
+
+    Python does not technically prevent talking to other classes as well, but this API provides a version-safe
+    interface with proper deprecation warnings etc. Usage of any other methods than the ones provided in this API can
+    cause plug-ins to be unstable.
+    """
+
 
     # For now we use the same API version to be consistent.
     __instance = None  # type: "CuraAPI"
@@ -39,12 +41,12 @@ class CuraAPI(QObject):
     def __init__(self, application: Optional["CuraApplication"] = None) -> None:
         super().__init__(parent = CuraAPI._application)
 
-        # Accounts API
         self._account = Account(self._application)
 
-        # Backups API
         self._backups = Backups(self._application)
 
+        self._connectionStatus = ConnectionStatus()
+
         # Interface API
         self._interface = Interface(self._application)
 
@@ -53,12 +55,22 @@ class CuraAPI(QObject):
 
     @pyqtProperty(QObject, constant = True)
     def account(self) -> "Account":
+        """Accounts API"""
+
         return self._account
 
+    @pyqtProperty(QObject, constant = True)
+    def connectionStatus(self) -> "ConnectionStatus":
+        return self._connectionStatus
+
     @property
     def backups(self) -> "Backups":
+        """Backups API"""
+
         return self._backups
 
     @property
     def interface(self) -> "Interface":
+        """Interface API"""
+
         return self._interface

+ 50 - 43
cura/Arranging/Arrange.py

@@ -16,17 +16,20 @@ from collections import namedtuple
 import numpy
 import copy
 
-##  Return object for  bestSpot
 LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points", "priority"])
+"""Return object for  bestSpot"""
 
 
 class Arrange:
     """
-    The Arrange classed is used together with ShapeArray. Use it to find good locations for objects that you try to put
+    The Arrange classed is used together with :py:class:`cura.Arranging.ShapeArray.ShapeArray`. Use it to find good locations for objects that you try to put
     on a build place. Different priority schemes can be defined so it alters the behavior while using the same logic.
 
-    Note: Make sure the scale is the same between ShapeArray objects and the Arrange instance.
+    .. note::
+
+       Make sure the scale is the same between :py:class:`cura.Arranging.ShapeArray.ShapeArray` objects and the :py:class:`cura.Arranging.Arrange.Arrange` instance.
     """
+
     build_volume = None  # type: Optional[BuildVolume]
 
     def __init__(self, x, y, offset_x, offset_y, scale = 0.5):
@@ -42,20 +45,20 @@ class Arrange:
         self._is_empty = True
 
     @classmethod
-    def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8):
-        """
-        Helper to create an Arranger instance
+    def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250, min_offset = 8) -> "Arrange":
+        """Helper to create an :py:class:`cura.Arranging.Arrange.Arrange` instance
 
         Either fill in scene_root and create will find all sliceable nodes by itself, or use fixed_nodes to provide the
         nodes yourself.
-        :param scene_root: Root for finding all scene nodes
-        :param fixed_nodes: Scene nodes to be placed
-        :param scale:
-        :param x:
-        :param y:
-        :param min_offset:
-        :return:
+
+        :param scene_root: Root for finding all scene nodes default = None
+        :param fixed_nodes: Scene nodes to be placed default = None
+        :param scale: default = 0.5
+        :param x: default = 350
+        :param y: default = 250
+        :param min_offset: default = 8
         """
+
         arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
         arranger.centerFirst()
 
@@ -77,8 +80,11 @@ class Arrange:
             # After scaling (like up to 0.1 mm) the node might not have points
             if not points.size:
                 continue
-
-            shape_arr = ShapeArray.fromPolygon(points, scale = scale)
+            try:
+                shape_arr = ShapeArray.fromPolygon(points, scale = scale)
+            except ValueError:
+                Logger.logException("w", "Unable to create polygon")
+                continue
             arranger.place(0, 0, shape_arr)
 
         # If a build volume was set, add the disallowed areas
@@ -90,19 +96,21 @@ class Arrange:
                 arranger.place(0, 0, shape_arr, update_empty = False)
         return arranger
 
-    ##  This resets the optimization for finding location based on size
     def resetLastPriority(self):
+        """This resets the optimization for finding location based on size"""
+
         self._last_priority = 0
 
-    def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1):
-        """
-        Find placement for a node (using offset shape) and place it (using hull shape)
-        :param node:
-        :param offset_shape_arr: hapeArray with offset, for placing the shape
-        :param hull_shape_arr: ShapeArray without offset, used to find location
-        :param step:
+    def findNodePlacement(self, node: SceneNode, offset_shape_arr: ShapeArray, hull_shape_arr: ShapeArray, step = 1) -> bool:
+        """Find placement for a node (using offset shape) and place it (using hull shape)
+
+        :param node: The node to be placed
+        :param offset_shape_arr: shape array with offset, for placing the shape
+        :param hull_shape_arr: shape array without offset, used to find location
+        :param step: default = 1
         :return: the nodes that should be placed
         """
+
         best_spot = self.bestSpot(
             hull_shape_arr, start_prio = self._last_priority, step = step)
         x, y = best_spot.x, best_spot.y
@@ -129,10 +137,8 @@ class Arrange:
         return found_spot
 
     def centerFirst(self):
-        """
-        Fill priority, center is best. Lower value is better.
-        :return:
-        """
+        """Fill priority, center is best. Lower value is better. """
+
         # Square distance: creates a more round shape
         self._priority = numpy.fromfunction(
             lambda j, i: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self._shape, dtype=numpy.int32)
@@ -140,23 +146,22 @@ class Arrange:
         self._priority_unique_values.sort()
 
     def backFirst(self):
-        """
-        Fill priority, back is best. Lower value is better
-        :return:
-        """
+        """Fill priority, back is best. Lower value is better """
+
         self._priority = numpy.fromfunction(
             lambda j, i: 10 * j + abs(self._offset_x - i), self._shape, dtype=numpy.int32)
         self._priority_unique_values = numpy.unique(self._priority)
         self._priority_unique_values.sort()
 
-    def checkShape(self, x, y, shape_arr):
-        """
-        Return the amount of "penalty points" for polygon, which is the sum of priority
+    def checkShape(self, x, y, shape_arr) -> Optional[numpy.ndarray]:
+        """Return the amount of "penalty points" for polygon, which is the sum of priority
+
         :param x: x-coordinate to check shape
-        :param y:
-        :param shape_arr: the ShapeArray object to place
+        :param y: y-coordinate to check shape
+        :param shape_arr: the shape array object to place
         :return: None if occupied
         """
+
         x = int(self._scale * x)
         y = int(self._scale * y)
         offset_x = x + self._offset_x + shape_arr.offset_x
@@ -180,14 +185,15 @@ class Arrange:
             offset_x:offset_x + shape_arr.arr.shape[1]]
         return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
 
-    def bestSpot(self, shape_arr, start_prio = 0, step = 1):
-        """
-        Find "best" spot for ShapeArray
-        :param shape_arr:
+    def bestSpot(self, shape_arr, start_prio = 0, step = 1) -> LocationSuggestion:
+        """Find "best" spot for ShapeArray
+
+        :param shape_arr: shape array
         :param start_prio: Start with this priority value (and skip the ones before)
         :param step: Slicing value, higher = more skips = faster but less accurate
         :return: namedtuple with properties x, y, penalty_points, priority.
         """
+
         start_idx_list = numpy.where(self._priority_unique_values == start_prio)
         if start_idx_list:
             try:
@@ -211,15 +217,16 @@ class Arrange:
         return LocationSuggestion(x = None, y = None, penalty_points = None, priority = priority)  # No suitable location found :-(
 
     def place(self, x, y, shape_arr, update_empty = True):
-        """
-        Place the object.
+        """Place the object.
+
         Marks the locations in self._occupied and self._priority
+
         :param x:
         :param y:
         :param shape_arr:
         :param update_empty: updates the _is_empty, used when adding disallowed areas
-        :return:
         """
+
         x = int(self._scale * x)
         y = int(self._scale * y)
         offset_x = x + self._offset_x + shape_arr.offset_x

Some files were not shown because too many files changed in this diff