mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-09-26 07:13:15 +08:00
commit
a1a7d48e46
1
.github/workflows/cicd.yml
vendored
1
.github/workflows/cicd.yml
vendored
@ -6,6 +6,7 @@ on:
|
||||
- master
|
||||
- 'WIP**'
|
||||
- '4.*'
|
||||
- 'CURA-*'
|
||||
pull_request:
|
||||
jobs:
|
||||
build:
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -53,6 +53,7 @@ plugins/GodMode
|
||||
plugins/OctoPrintPlugin
|
||||
plugins/ProfileFlattener
|
||||
plugins/SettingsGuide
|
||||
plugins/SVGToolpathReader
|
||||
plugins/X3GWriter
|
||||
|
||||
#Build stuff
|
||||
|
116
.pylintrc
Normal file
116
.pylintrc
Normal file
@ -0,0 +1,116 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# This file contains the Pylint rules used in the stardust projects.
|
||||
|
||||
# To configure PyLint as an external tool in PyCharm, create a new External Tool with the settings:
|
||||
#
|
||||
# Name: PyLint
|
||||
# Program: Check with 'which pylint'. For example: ~/.local/bin/pylint
|
||||
# Arguments: $FileDirName$ --rcfile=.pylintrc --msg-template='{abspath}:{line}:{column}:({symbol}):{msg_id}:{msg}'
|
||||
# Working directory: $ContentRoot$
|
||||
# Output filters: $FILE_PATH$:$LINE$:$COLUMN$:.*
|
||||
#
|
||||
# You can add a keyboard shortcut in the keymap settings. To run Pylint to a project, select the module
|
||||
# you want to check (e.g. cura folder) before running the external tool.
|
||||
#
|
||||
# If you find a better way to configure the external tool please edit this file.
|
||||
|
||||
[MASTER]
|
||||
# List of plugins (as comma separated values of python modules names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=pylint_quotes
|
||||
|
||||
# We expect double string quotes
|
||||
string-quote=double-avoid-escape
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Add files or directories to the blacklist. They should be base names, not paths.
|
||||
ignore=tests
|
||||
|
||||
[REFACTORING]
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
# C0326: No space allowed around keyword argument assignment
|
||||
# C0411: Ignore import order because the rules are different than in PyCharm, so automatic imports break lots of builds
|
||||
# C0412: Ignore import order because the rules are different than in PyCharm, so automatic imports break lots of builds
|
||||
# C0413: Ignore import order because the rules are different than in PyCharm, so automatic imports break lots of builds
|
||||
# R0201: Method could be a function (no-self-use)
|
||||
# R0401: Cyclic imports (cyclic-import) are used for typing
|
||||
# R0801: Unfortunately the error is triggered for a lot of similar models (duplicate-code)
|
||||
# R1710: Either all return statements in a function should return an expression, or none of them should.
|
||||
# W0221: Parameters differ from overridden method (tornado http methods have a flexible number of parameters)
|
||||
# W0511: Ignore warnings generated for TODOs in the code
|
||||
# C0111: We don't use docstring
|
||||
# C0303: Trailing whitespace isn't something we care about
|
||||
# C4001: You can put " in a string if you escape it first...
|
||||
disable=C0326,C0411,C0412,C0413,R0201,R0401,R0801,R1710,W0221,W0511, C0111, C0303,C4001
|
||||
|
||||
[FORMAT]
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=120
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=500
|
||||
|
||||
good-names=os
|
||||
|
||||
[BASIC]
|
||||
# allow modules and functions to use PascalCase
|
||||
module-rgx=[a-zA-Z0-9_]+$
|
||||
function-rgx=
|
||||
## Allowed methods:
|
||||
# getSomething
|
||||
# _getSomething
|
||||
# __getSomething
|
||||
# __new__
|
||||
## Disallowed:
|
||||
# _GET
|
||||
# GetSomething
|
||||
method-rgx=(_{,2}[a-z][A-Za-z0-9]*_{,2})$
|
||||
|
||||
[DESIGN]
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=7
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=8
|
||||
|
||||
# Maximum number of boolean expressions in an if statement.
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (R0903).
|
||||
# We set this to 0 because our models and fields do not have methods.
|
||||
min-public-methods=0
|
||||
|
||||
ignored-argument-names=arg|args|kwargs|_
|
||||
|
||||
[CLASSES]
|
||||
defining-attr-methods=__init__,__new__,setUp,initialize
|
||||
|
||||
[TYPECHECK]
|
||||
ignored-classes=NotImplemented
|
||||
|
||||
[VARIABLES]
|
||||
dummy-variables-rgx=_+[a-z0-9_]{2,30}
|
@ -5,8 +5,8 @@ include(GNUInstallDirs)
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
|
||||
|
||||
set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE DIRECTORY "The location of the Uranium repository")
|
||||
set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
|
||||
set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE PATH "The location of the Uranium repository")
|
||||
set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE PATH "The location of the scripts directory of the Uranium repository")
|
||||
|
||||
# Tests
|
||||
include(CuraTests)
|
||||
@ -23,6 +23,7 @@ set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
|
||||
set(CURA_CLOUD_API_ROOT "" CACHE STRING "Alternative Cura cloud API root")
|
||||
set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version")
|
||||
set(CURA_CLOUD_ACCOUNT_API_ROOT "" CACHE STRING "Alternative Cura cloud account API version")
|
||||
set(CURA_MARKETPLACE_ROOT "" CACHE STRING "Alternative Marketplace location")
|
||||
|
||||
configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY)
|
||||
|
||||
|
@ -56,6 +56,13 @@ function(cura_add_test)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
#Add test for import statements which are not compatible with all builds
|
||||
add_test(
|
||||
NAME "invalid-imports"
|
||||
COMMAND ${Python3_EXECUTABLE} scripts/check_invalid_imports.py
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
)
|
||||
|
||||
cura_add_test(NAME pytest-main DIRECTORY ${CMAKE_SOURCE_DIR}/tests PYTHONPATH "${CMAKE_SOURCE_DIR}|${URANIUM_DIR}")
|
||||
|
||||
file(GLOB_RECURSE _plugins plugins/*/__init__.py)
|
||||
|
@ -4,12 +4,11 @@ from typing import Optional, Dict, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Message import Message
|
||||
from cura import UltimakerCloudAuthentication
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from cura.OAuth2.AuthorizationService import AuthorizationService
|
||||
from cura.OAuth2.Models import OAuth2Settings
|
||||
from cura.UltimakerCloud import UltimakerCloudAuthentication
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
@ -28,11 +28,12 @@ class CuraAPI(QObject):
|
||||
# The main reason for this is that we want to prevent consumers of API to have a dependency on CuraApplication.
|
||||
# Since the API is intended to be used by plugins, the cura application should have already created this.
|
||||
def __new__(cls, application: Optional["CuraApplication"] = None):
|
||||
if cls.__instance is None:
|
||||
if application is None:
|
||||
raise Exception("Upon first time creation, the application must be set.")
|
||||
cls.__instance = super(CuraAPI, cls).__new__(cls)
|
||||
cls._application = application
|
||||
if cls.__instance is not None:
|
||||
raise RuntimeError("Tried to create singleton '{class_name}' more than once.".format(class_name = CuraAPI.__name__))
|
||||
if application is None:
|
||||
raise RuntimeError("Upon first time creation, the application must be set.")
|
||||
cls.__instance = super(CuraAPI, cls).__new__(cls)
|
||||
cls._application = application
|
||||
return cls.__instance
|
||||
|
||||
def __init__(self, application: Optional["CuraApplication"] = None) -> None:
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
# ---------
|
||||
@ -13,7 +13,7 @@ DEFAULT_CURA_DEBUG_MODE = False
|
||||
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
|
||||
# example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the
|
||||
# CuraVersion.py.in template.
|
||||
CuraSDKVersion = "7.0.0"
|
||||
CuraSDKVersion = "7.1.0"
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraAppName # type: ignore
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Logger import Logger
|
||||
@ -8,6 +8,7 @@ from UM.Math.Polygon import Polygon
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from cura.Arranging.ShapeArray import ShapeArray
|
||||
from cura.BuildVolume import BuildVolume
|
||||
from cura.Scene import ZOffsetDecorator
|
||||
|
||||
from collections import namedtuple
|
||||
@ -27,9 +28,9 @@ LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points
|
||||
#
|
||||
# Note: Make sure the scale is the same between ShapeArray objects and the Arrange instance.
|
||||
class Arrange:
|
||||
build_volume = None
|
||||
build_volume = None # type: Optional[BuildVolume]
|
||||
|
||||
def __init__(self, x, y, offset_x, offset_y, scale= 0.5):
|
||||
def __init__(self, x, y, offset_x, offset_y, scale = 0.5):
|
||||
self._scale = scale # convert input coordinates to arrange coordinates
|
||||
world_x, world_y = int(x * self._scale), int(y * self._scale)
|
||||
self._shape = (world_y, world_x)
|
||||
@ -68,7 +69,7 @@ class Arrange:
|
||||
points = copy.deepcopy(vertices._points)
|
||||
|
||||
# After scaling (like up to 0.1 mm) the node might not have points
|
||||
if len(points) == 0:
|
||||
if not points.size:
|
||||
continue
|
||||
|
||||
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
|
||||
@ -113,7 +114,7 @@ class Arrange:
|
||||
found_spot = True
|
||||
self.place(x, y, offset_shape_arr) # place the object in arranger
|
||||
else:
|
||||
Logger.log("d", "Could not find spot!"),
|
||||
Logger.log("d", "Could not find spot!")
|
||||
found_spot = False
|
||||
node.setPosition(Vector(200, center_y, 100))
|
||||
return found_spot
|
||||
@ -172,7 +173,10 @@ class Arrange:
|
||||
def bestSpot(self, shape_arr, start_prio = 0, step = 1):
|
||||
start_idx_list = numpy.where(self._priority_unique_values == start_prio)
|
||||
if start_idx_list:
|
||||
start_idx = start_idx_list[0][0]
|
||||
try:
|
||||
start_idx = start_idx_list[0][0]
|
||||
except IndexError:
|
||||
start_idx = 0
|
||||
else:
|
||||
start_idx = 0
|
||||
for priority in self._priority_unique_values[start_idx::step]:
|
||||
|
@ -29,7 +29,7 @@ class ArrangeArray:
|
||||
self._has_empty = False
|
||||
self._arrange = [] # type: List[Arrange]
|
||||
|
||||
def _update_first_empty(self):
|
||||
def _updateFirstEmpty(self):
|
||||
for i, a in enumerate(self._arrange):
|
||||
if a.isEmpty:
|
||||
self._first_empty = i
|
||||
@ -42,7 +42,7 @@ class ArrangeArray:
|
||||
new_arrange = Arrange.create(x = self._x, y = self._y, fixed_nodes = self._fixed_nodes)
|
||||
self._arrange.append(new_arrange)
|
||||
self._count += 1
|
||||
self._update_first_empty()
|
||||
self._updateFirstEmpty()
|
||||
|
||||
def count(self):
|
||||
return self._count
|
||||
|
@ -2,12 +2,16 @@
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QTimer
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
class AutoSave:
|
||||
def __init__(self, application):
|
||||
def __init__(self, application: "CuraApplication") -> None:
|
||||
self._application = application
|
||||
self._application.getPreferences().preferenceChanged.connect(self._triggerTimer)
|
||||
|
||||
@ -22,14 +26,14 @@ class AutoSave:
|
||||
self._enabled = True
|
||||
self._saving = False
|
||||
|
||||
def initialize(self):
|
||||
def initialize(self) -> None:
|
||||
# only initialise if the application is created and has started
|
||||
self._change_timer.timeout.connect(self._onTimeout)
|
||||
self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||
self._onGlobalStackChanged()
|
||||
self._triggerTimer()
|
||||
|
||||
def _triggerTimer(self, *args):
|
||||
def _triggerTimer(self, *args: Any) -> None:
|
||||
if not self._saving:
|
||||
self._change_timer.start()
|
||||
|
||||
@ -40,7 +44,7 @@ class AutoSave:
|
||||
else:
|
||||
self._change_timer.stop()
|
||||
|
||||
def _onGlobalStackChanged(self):
|
||||
def _onGlobalStackChanged(self) -> None:
|
||||
if self._global_stack:
|
||||
self._global_stack.propertyChanged.disconnect(self._triggerTimer)
|
||||
self._global_stack.containersChanged.disconnect(self._triggerTimer)
|
||||
@ -51,7 +55,7 @@ class AutoSave:
|
||||
self._global_stack.propertyChanged.connect(self._triggerTimer)
|
||||
self._global_stack.containersChanged.connect(self._triggerTimer)
|
||||
|
||||
def _onTimeout(self):
|
||||
def _onTimeout(self) -> None:
|
||||
self._saving = True # To prevent the save process from triggering another autosave.
|
||||
Logger.log("d", "Autosaving preferences, instances and profiles")
|
||||
|
||||
|
@ -145,6 +145,14 @@ class Backup:
|
||||
# \return Whether we had success or not.
|
||||
@staticmethod
|
||||
def _extractArchive(archive: "ZipFile", target_path: str) -> bool:
|
||||
|
||||
# Implement security recommendations: Sanity check on zip files will make it harder to spoof.
|
||||
from cura.CuraApplication import CuraApplication
|
||||
config_filename = CuraApplication.getInstance().getApplicationName() + ".cfg" # Should be there if valid.
|
||||
if config_filename not in [file.filename for file in archive.filelist]:
|
||||
Logger.logException("e", "Unable to extract the backup due to corruption of compressed file(s).")
|
||||
return False
|
||||
|
||||
Logger.log("d", "Removing current data in location: %s", target_path)
|
||||
Resources.factoryReset()
|
||||
Logger.log("d", "Extracting backup to location: %s", target_path)
|
||||
|
@ -10,18 +10,23 @@ if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
## The BackupsManager is responsible for managing the creating and restoring of
|
||||
# back-ups.
|
||||
#
|
||||
# Back-ups themselves are represented in a different class.
|
||||
class BackupsManager:
|
||||
"""
|
||||
The BackupsManager is responsible for managing the creating and restoring of
|
||||
back-ups.
|
||||
|
||||
Back-ups themselves are represented in a different class.
|
||||
"""
|
||||
|
||||
def __init__(self, application: "CuraApplication") -> None:
|
||||
self._application = application
|
||||
|
||||
## Get a back-up of the current configuration.
|
||||
# \return A tuple containing a ZipFile (the actual back-up) and a dict
|
||||
# containing some metadata (like version).
|
||||
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]:
|
||||
"""
|
||||
Get a back-up of the current configuration.
|
||||
:return: A tuple containing a ZipFile (the actual back-up) and a dict containing some metadata (like version).
|
||||
"""
|
||||
|
||||
self._disableAutoSave()
|
||||
backup = Backup(self._application)
|
||||
backup.makeFromCurrent()
|
||||
@ -29,11 +34,13 @@ class BackupsManager:
|
||||
# We don't return a Backup here because we want plugins only to interact with our API and not full objects.
|
||||
return backup.zip_file, backup.meta_data
|
||||
|
||||
## Restore a back-up from a given ZipFile.
|
||||
# \param zip_file A bytes object containing the actual back-up.
|
||||
# \param meta_data A dict containing some metadata that is needed to
|
||||
# restore the back-up correctly.
|
||||
def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, str]) -> None:
|
||||
"""
|
||||
Restore a back-up from a given ZipFile.
|
||||
:param zip_file: A bytes object containing the actual back-up.
|
||||
:param meta_data: A dict containing some metadata that is needed to restore the back-up correctly.
|
||||
"""
|
||||
|
||||
if not meta_data.get("cura_release", None):
|
||||
# If there is no "cura_release" specified in the meta data, we don't execute a backup restore.
|
||||
Logger.log("w", "Tried to restore a backup without specifying a Cura version number.")
|
||||
@ -48,9 +55,10 @@ class BackupsManager:
|
||||
# We don't want to store the data at this point as that would override the just-restored backup.
|
||||
self._application.windowClosed(save_data = False)
|
||||
|
||||
## Here we try to disable the auto-save plug-in as it might interfere with
|
||||
# restoring a back-up.
|
||||
def _disableAutoSave(self) -> None:
|
||||
"""Here we (try to) disable the saving as it might interfere with restoring a back-up."""
|
||||
|
||||
self._application.enableSave(False)
|
||||
auto_save = self._application.getAutoSave()
|
||||
# The auto save is only not created if the application has not yet started.
|
||||
if auto_save:
|
||||
@ -58,8 +66,10 @@ class BackupsManager:
|
||||
else:
|
||||
Logger.log("e", "Unable to disable the autosave as application init has not been completed")
|
||||
|
||||
## Re-enable auto-save after we're done.
|
||||
def _enableAutoSave(self) -> None:
|
||||
"""Re-enable auto-save and other saving after we're done."""
|
||||
|
||||
self._application.enableSave(True)
|
||||
auto_save = self._application.getAutoSave()
|
||||
# The auto save is only not created if the application has not yet started.
|
||||
if auto_save:
|
||||
|
@ -1,15 +1,21 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import numpy
|
||||
import math
|
||||
|
||||
from typing import List, Optional, TYPE_CHECKING, Any, Set, cast, Iterable, Dict
|
||||
|
||||
from UM.Mesh.MeshData import MeshData
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
|
||||
from UM.Application import Application #To modify the maximum zoom level.
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Scene.Platform import Platform
|
||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Resources import Resources
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Math.Matrix import Matrix
|
||||
from UM.Math.Color import Color
|
||||
@ -17,23 +23,23 @@ from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||
from UM.Math.Polygon import Polygon
|
||||
from UM.Message import Message
|
||||
from UM.Signal import Signal
|
||||
from PyQt5.QtCore import QTimer
|
||||
from UM.View.RenderBatch import RenderBatch
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
from PyQt5.QtCore import QTimer
|
||||
|
||||
import numpy
|
||||
import math
|
||||
|
||||
from typing import List, Optional, TYPE_CHECKING, Any, Set, cast, Iterable, Dict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
# Radius of disallowed area in mm around prime. I.e. how much distance to keep from prime position.
|
||||
PRIME_CLEARANCE = 6.5
|
||||
|
||||
@ -1012,13 +1018,13 @@ class BuildVolume(SceneNode):
|
||||
all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "value")
|
||||
all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type")
|
||||
for i, (setting_value, setting_type) in enumerate(zip(all_values, all_types)):
|
||||
if not setting_value and (setting_type == "int" or setting_type == "float"):
|
||||
if not setting_value and setting_type in ["int", "float"]:
|
||||
all_values[i] = 0
|
||||
return all_values
|
||||
|
||||
def _calculateBedAdhesionSize(self, used_extruders):
|
||||
if self._global_container_stack is None:
|
||||
return
|
||||
return None
|
||||
|
||||
container_stack = self._global_container_stack
|
||||
adhesion_type = container_stack.getProperty("adhesion_type", "value")
|
||||
|
@ -10,7 +10,7 @@ import os.path
|
||||
import uuid
|
||||
import json
|
||||
import locale
|
||||
from typing import cast
|
||||
from typing import cast, Any
|
||||
|
||||
try:
|
||||
from sentry_sdk.hub import Hub
|
||||
@ -32,6 +32,8 @@ from UM.Resources import Resources
|
||||
from cura import ApplicationMetadata
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
home_dir = os.path.expanduser("~")
|
||||
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
@ -58,6 +60,8 @@ class CrashHandler:
|
||||
self.traceback = tb
|
||||
self.has_started = has_started
|
||||
self.dialog = None # Don't create a QDialog before there is a QApplication
|
||||
self.cura_version = None
|
||||
self.cura_locale = None
|
||||
|
||||
Logger.log("c", "An uncaught error has occurred!")
|
||||
for line in traceback.format_exception(exception_type, value, tb):
|
||||
@ -81,6 +85,21 @@ class CrashHandler:
|
||||
self.dialog = QDialog()
|
||||
self._createDialog()
|
||||
|
||||
@staticmethod
|
||||
def pruneSensitiveData(obj: Any) -> Any:
|
||||
if isinstance(obj, str):
|
||||
return obj.replace(home_dir, "<user_home>")
|
||||
if isinstance(obj, list):
|
||||
return [CrashHandler.pruneSensitiveData(item) for item in obj]
|
||||
if isinstance(obj, dict):
|
||||
return {k: CrashHandler.pruneSensitiveData(v) for k, v in obj.items()}
|
||||
|
||||
return obj
|
||||
|
||||
@staticmethod
|
||||
def sentryBeforeSend(event, hint):
|
||||
return CrashHandler.pruneSensitiveData(event)
|
||||
|
||||
def _createEarlyCrashDialog(self):
|
||||
dialog = QDialog()
|
||||
dialog.setMinimumWidth(500)
|
||||
@ -159,7 +178,6 @@ class CrashHandler:
|
||||
layout.addWidget(self._informationWidget())
|
||||
layout.addWidget(self._exceptionInfoWidget())
|
||||
layout.addWidget(self._logInfoWidget())
|
||||
layout.addWidget(self._userDescriptionWidget())
|
||||
layout.addWidget(self._buttonsWidget())
|
||||
|
||||
def _close(self):
|
||||
@ -372,21 +390,6 @@ class CrashHandler:
|
||||
|
||||
return group
|
||||
|
||||
def _userDescriptionWidget(self):
|
||||
group = QGroupBox()
|
||||
group.setTitle(catalog.i18nc("@title:groupbox", "User description" +
|
||||
" (Note: Developers may not speak your language, please use English if possible)"))
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# When sending the report, the user comments will be collected
|
||||
self.user_description_text_area = QTextEdit()
|
||||
self.user_description_text_area.setFocus(True)
|
||||
|
||||
layout.addWidget(self.user_description_text_area)
|
||||
group.setLayout(layout)
|
||||
|
||||
return group
|
||||
|
||||
def _buttonsWidget(self):
|
||||
buttons = QDialogButtonBox()
|
||||
buttons.addButton(QDialogButtonBox.Close)
|
||||
@ -401,9 +404,6 @@ class CrashHandler:
|
||||
return buttons
|
||||
|
||||
def _sendCrashReport(self):
|
||||
# Before sending data, the user comments are stored
|
||||
self.data["user_info"] = self.user_description_text_area.toPlainText()
|
||||
|
||||
if with_sentry_sdk:
|
||||
try:
|
||||
hub = Hub.current
|
||||
|
@ -3,17 +3,15 @@
|
||||
|
||||
from PyQt5.QtCore import QObject, QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from typing import List, Optional, cast
|
||||
from typing import List, cast
|
||||
|
||||
from UM.Event import CallFunctionEvent
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.Math.Quaternion import Quaternion
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||
from UM.Operations.RotateOperation import RotateOperation
|
||||
from UM.Operations.TranslateOperation import TranslateOperation
|
||||
|
||||
import cura.CuraApplication
|
||||
|
@ -1,77 +1,59 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from typing import cast, TYPE_CHECKING, Optional, Callable, List
|
||||
from typing import cast, TYPE_CHECKING, Optional, Callable, List, Any, Dict
|
||||
|
||||
import numpy
|
||||
|
||||
from PyQt5.QtCore import QObject, QTimer, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
|
||||
from PyQt5.QtGui import QColor, QIcon
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
import UM.Util
|
||||
import cura.Settings.cura_empty_instance_containers
|
||||
from UM.Application import Application
|
||||
from UM.Decorators import override, deprecated
|
||||
from UM.Decorators import override
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Platform import Platform
|
||||
from UM.PluginError import PluginNotFoundError
|
||||
from UM.Resources import Resources
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Qt.QtApplication import QtApplication # The class we're inheriting from.
|
||||
import UM.Util
|
||||
from UM.View.SelectionPass import SelectionPass # For typing.
|
||||
|
||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||
from UM.Math.Matrix import Matrix
|
||||
from UM.Math.Quaternion import Quaternion
|
||||
from UM.Math.Vector import Vector
|
||||
|
||||
from UM.Mesh.ReadMeshJob import ReadMeshJob
|
||||
|
||||
from UM.Message import Message
|
||||
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
from UM.Operations.SetTransformOperation import SetTransformOperation
|
||||
|
||||
from UM.Platform import Platform
|
||||
from UM.PluginError import PluginNotFoundError
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Qt.QtApplication import QtApplication # The class we're inheriting from.
|
||||
from UM.Resources import Resources
|
||||
from UM.Scene.Camera import Camera
|
||||
from UM.Scene.GroupDecorator import GroupDecorator
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Scene.ToolHandle import ToolHandle
|
||||
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
from UM.Settings.Validator import Validator
|
||||
|
||||
from UM.View.SelectionPass import SelectionPass # For typing.
|
||||
from UM.Workspace.WorkspaceReader import WorkspaceReader
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from cura import ApplicationMetadata
|
||||
from cura.API import CuraAPI
|
||||
|
||||
from cura.Arranging.Arrange import Arrange
|
||||
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
||||
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
||||
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
||||
from cura.Arranging.ShapeArray import ShapeArray
|
||||
|
||||
from cura.Operations.SetParentOperation import SetParentOperation
|
||||
|
||||
from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
|
||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
||||
from cura.Scene.CuraSceneController import CuraSceneController
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
|
||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
from cura.Scene import ZOffsetDecorator
|
||||
from cura.Machines.MachineErrorChecker import MachineErrorChecker
|
||||
|
||||
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
|
||||
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
|
||||
from cura.Machines.Models.DiscoveredPrintersModel import DiscoveredPrintersModel
|
||||
@ -80,6 +62,8 @@ from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel
|
||||
from cura.Machines.Models.FirstStartMachineActionsModel import FirstStartMachineActionsModel
|
||||
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
|
||||
from cura.Machines.Models.GlobalStacksModel import GlobalStacksModel
|
||||
from cura.Machines.Models.IntentCategoryModel import IntentCategoryModel
|
||||
from cura.Machines.Models.IntentModel import IntentModel
|
||||
from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
|
||||
from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel
|
||||
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
|
||||
@ -89,51 +73,47 @@ from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfile
|
||||
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
|
||||
from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
|
||||
from cura.Machines.Models.UserChangesModel import UserChangesModel
|
||||
from cura.Machines.Models.IntentModel import IntentModel
|
||||
from cura.Machines.Models.IntentCategoryModel import IntentCategoryModel
|
||||
|
||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.Operations.SetParentOperation import SetParentOperation
|
||||
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
|
||||
|
||||
import cura.Settings.cura_empty_instance_containers
|
||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.Scene import ZOffsetDecorator
|
||||
from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
|
||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
||||
from cura.Scene.CuraSceneController import CuraSceneController
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
from cura.Settings.ContainerManager import ContainerManager
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from cura.Settings.IntentManager import IntentManager
|
||||
from cura.Settings.MachineManager import MachineManager
|
||||
from cura.Settings.MachineNameValidator import MachineNameValidator
|
||||
from cura.Settings.IntentManager import IntentManager
|
||||
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
|
||||
from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
|
||||
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
|
||||
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
|
||||
|
||||
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
|
||||
|
||||
from cura.UI import CuraSplashScreen, MachineActionManager, PrintInformation
|
||||
from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel
|
||||
from cura.UI.MachineSettingsManager import MachineSettingsManager
|
||||
from cura.UI.ObjectsModel import ObjectsModel
|
||||
from cura.UI.TextManager import TextManager
|
||||
from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel
|
||||
from cura.UI.RecommendedMode import RecommendedMode
|
||||
from cura.UI.TextManager import TextManager
|
||||
from cura.UI.WelcomePagesModel import WelcomePagesModel
|
||||
from cura.UI.WhatsNewPagesModel import WhatsNewPagesModel
|
||||
|
||||
from cura.UltimakerCloud import UltimakerCloudAuthentication
|
||||
from cura.Utils.NetworkingUtil import NetworkingUtil
|
||||
|
||||
from .SingleInstance import SingleInstance
|
||||
from .AutoSave import AutoSave
|
||||
from . import PlatformPhysics
|
||||
from . import BuildVolume
|
||||
from . import CameraAnimation
|
||||
from . import CuraActions
|
||||
from . import PlatformPhysics
|
||||
from . import PrintJobPreviewImageProvider
|
||||
|
||||
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
|
||||
|
||||
from cura import ApplicationMetadata, UltimakerCloudAuthentication
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from .AutoSave import AutoSave
|
||||
from .SingleInstance import SingleInstance
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
|
||||
@ -145,7 +125,7 @@ class CuraApplication(QtApplication):
|
||||
# SettingVersion represents the set of settings available in the machine/extruder definitions.
|
||||
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
|
||||
# changes of the settings.
|
||||
SettingVersion = 11
|
||||
SettingVersion = 13
|
||||
|
||||
Created = False
|
||||
|
||||
@ -191,9 +171,7 @@ class CuraApplication(QtApplication):
|
||||
|
||||
self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions]
|
||||
|
||||
self._cura_package_manager = None
|
||||
|
||||
self._machine_action_manager = None
|
||||
self._machine_action_manager = None # type: Optional[MachineActionManager.MachineActionManager]
|
||||
|
||||
self.empty_container = None # type: EmptyInstanceContainer
|
||||
self.empty_definition_changes_container = None # type: EmptyInstanceContainer
|
||||
@ -265,8 +243,8 @@ class CuraApplication(QtApplication):
|
||||
|
||||
# Backups
|
||||
self._auto_save = None # type: Optional[AutoSave]
|
||||
self._enable_save = True
|
||||
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
self._container_registry_class = CuraContainerRegistry
|
||||
# Redefined here in order to please the typing.
|
||||
self._container_registry = None # type: CuraContainerRegistry
|
||||
@ -351,6 +329,9 @@ class CuraApplication(QtApplication):
|
||||
for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "quality_changes", "user", "variants", "intent"]:
|
||||
Resources.addExpectedDirNameInData(dir_name)
|
||||
|
||||
app_root = os.path.abspath(os.path.join(os.path.dirname(sys.executable)))
|
||||
Resources.addSearchPath(os.path.join(app_root, "share", "cura", "resources"))
|
||||
|
||||
Resources.addSearchPath(os.path.join(self._app_install_dir, "share", "cura", "resources"))
|
||||
if not hasattr(sys, "frozen"):
|
||||
resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")
|
||||
@ -394,6 +375,8 @@ class CuraApplication(QtApplication):
|
||||
SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders)
|
||||
SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue)
|
||||
SettingFunction.registerOperator("defaultExtruderPosition", self._cura_formula_functions.getDefaultExtruderPosition)
|
||||
SettingFunction.registerOperator("valueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndex)
|
||||
SettingFunction.registerOperator("extruderValueFromContainer", self._cura_formula_functions.getValueFromContainerAtIndexInExtruder)
|
||||
|
||||
# Adds all resources and container related resources.
|
||||
def __addAllResourcesAndContainerResources(self) -> None:
|
||||
@ -633,6 +616,12 @@ class CuraApplication(QtApplication):
|
||||
def showPreferences(self) -> None:
|
||||
self.showPreferencesWindow.emit()
|
||||
|
||||
# This is called by drag-and-dropping curapackage files.
|
||||
@pyqtSlot(QUrl)
|
||||
def installPackageViaDragAndDrop(self, file_url: str) -> Optional[str]:
|
||||
filename = QUrl(file_url).toLocalFile()
|
||||
return self._package_manager.installPackage(filename)
|
||||
|
||||
@override(Application)
|
||||
def getGlobalContainerStack(self) -> Optional["GlobalStack"]:
|
||||
return self._global_container_stack
|
||||
@ -698,15 +687,20 @@ class CuraApplication(QtApplication):
|
||||
self._message_box_callback = None
|
||||
self._message_box_callback_arguments = []
|
||||
|
||||
def enableSave(self, enable: bool):
|
||||
self._enable_save = enable
|
||||
|
||||
# Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
|
||||
def saveSettings(self):
|
||||
if not self.started:
|
||||
def saveSettings(self) -> None:
|
||||
if not self.started or not self._enable_save:
|
||||
# Do not do saving during application start or when data should not be saved on quit.
|
||||
return
|
||||
ContainerRegistry.getInstance().saveDirtyContainers()
|
||||
self.savePreferences()
|
||||
|
||||
def saveStack(self, stack):
|
||||
if not self._enable_save:
|
||||
return
|
||||
ContainerRegistry.getInstance().saveContainer(stack)
|
||||
|
||||
@pyqtSlot(str, result = QUrl)
|
||||
@ -989,8 +983,8 @@ class CuraApplication(QtApplication):
|
||||
## Get the machine action manager
|
||||
# We ignore any *args given to this, as we also register the machine manager as qml singleton.
|
||||
# It wants to give this function an engine and script engine, but we don't care about that.
|
||||
def getMachineActionManager(self, *args):
|
||||
return self._machine_action_manager
|
||||
def getMachineActionManager(self, *args: Any) -> MachineActionManager.MachineActionManager:
|
||||
return cast(MachineActionManager.MachineActionManager, self._machine_action_manager)
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getMaterialManagementModel(self) -> MaterialManagementModel:
|
||||
@ -1389,22 +1383,29 @@ class CuraApplication(QtApplication):
|
||||
if not nodes:
|
||||
return
|
||||
|
||||
objects_in_filename = {} # type: Dict[str, List[CuraSceneNode]]
|
||||
for node in nodes:
|
||||
mesh_data = node.getMeshData()
|
||||
|
||||
if mesh_data:
|
||||
file_name = mesh_data.getFileName()
|
||||
if file_name:
|
||||
job = ReadMeshJob(file_name)
|
||||
job._node = node # type: ignore
|
||||
job.finished.connect(self._reloadMeshFinished)
|
||||
if has_merged_nodes:
|
||||
job.finished.connect(self.updateOriginOfMergedMeshes)
|
||||
|
||||
job.start()
|
||||
if file_name not in objects_in_filename:
|
||||
objects_in_filename[file_name] = []
|
||||
if file_name in objects_in_filename:
|
||||
objects_in_filename[file_name].append(node)
|
||||
else:
|
||||
Logger.log("w", "Unable to reload data because we don't have a filename.")
|
||||
|
||||
for file_name, nodes in objects_in_filename.items():
|
||||
for node in nodes:
|
||||
job = ReadMeshJob(file_name)
|
||||
job._node = node # type: ignore
|
||||
job.finished.connect(self._reloadMeshFinished)
|
||||
if has_merged_nodes:
|
||||
job.finished.connect(self.updateOriginOfMergedMeshes)
|
||||
|
||||
job.start()
|
||||
|
||||
@pyqtSlot("QStringList")
|
||||
def setExpandedCategories(self, categories: List[str]) -> None:
|
||||
categories = list(set(categories))
|
||||
@ -1443,7 +1444,7 @@ class CuraApplication(QtApplication):
|
||||
if center is not None:
|
||||
object_centers.append(center)
|
||||
|
||||
if object_centers and len(object_centers) > 0:
|
||||
if object_centers:
|
||||
middle_x = sum([v.x for v in object_centers]) / len(object_centers)
|
||||
middle_y = sum([v.y for v in object_centers]) / len(object_centers)
|
||||
middle_z = sum([v.z for v in object_centers]) / len(object_centers)
|
||||
@ -1493,7 +1494,7 @@ class CuraApplication(QtApplication):
|
||||
if center is not None:
|
||||
object_centers.append(center)
|
||||
|
||||
if object_centers and len(object_centers) > 0:
|
||||
if object_centers:
|
||||
middle_x = sum([v.x for v in object_centers]) / len(object_centers)
|
||||
middle_y = sum([v.y for v in object_centers]) / len(object_centers)
|
||||
middle_z = sum([v.z for v in object_centers]) / len(object_centers)
|
||||
@ -1579,13 +1580,30 @@ class CuraApplication(QtApplication):
|
||||
fileLoaded = pyqtSignal(str)
|
||||
fileCompleted = pyqtSignal(str)
|
||||
|
||||
def _reloadMeshFinished(self, job):
|
||||
# TODO; This needs to be fixed properly. We now make the assumption that we only load a single mesh!
|
||||
job_result = job.getResult()
|
||||
def _reloadMeshFinished(self, job) -> None:
|
||||
"""
|
||||
Function called whenever a ReadMeshJob finishes in the background. It reloads a specific node object in the
|
||||
scene from its source file. The function gets all the nodes that exist in the file through the job result, and
|
||||
then finds the scene node that it wants to refresh by its object id. Each job refreshes only one node.
|
||||
|
||||
:param job: The ReadMeshJob running in the background that reads all the meshes in a file
|
||||
:return: None
|
||||
"""
|
||||
job_result = job.getResult() # nodes that exist inside the file read by this job
|
||||
if len(job_result) == 0:
|
||||
Logger.log("e", "Reloading the mesh failed.")
|
||||
return
|
||||
mesh_data = job_result[0].getMeshData()
|
||||
object_found = False
|
||||
mesh_data = None
|
||||
# Find the node to be refreshed based on its id
|
||||
for job_result_node in job_result:
|
||||
if job_result_node.getId() == job._node.getId():
|
||||
mesh_data = job_result_node.getMeshData()
|
||||
object_found = True
|
||||
break
|
||||
if not object_found:
|
||||
Logger.warning("The object with id {} no longer exists! Keeping the old version in the scene.".format(job_result_node.getId()))
|
||||
return
|
||||
if not mesh_data:
|
||||
Logger.log("w", "Could not find a mesh in reloaded node.")
|
||||
return
|
||||
@ -1675,7 +1693,7 @@ class CuraApplication(QtApplication):
|
||||
extension = os.path.splitext(f)[1]
|
||||
extension = extension.lower()
|
||||
filename = os.path.basename(f)
|
||||
if len(self._currently_loading_files) > 0:
|
||||
if self._currently_loading_files:
|
||||
# If a non-slicable file is already being loaded, we prevent loading of any further non-slicable files
|
||||
if extension in self._non_sliceable_extensions:
|
||||
message = Message(
|
||||
@ -1796,8 +1814,8 @@ class CuraApplication(QtApplication):
|
||||
node.addDecorator(build_plate_decorator)
|
||||
build_plate_decorator.setBuildPlateNumber(target_build_plate)
|
||||
|
||||
op = AddSceneNodeOperation(node, scene.getRoot())
|
||||
op.push()
|
||||
operation = AddSceneNodeOperation(node, scene.getRoot())
|
||||
operation.push()
|
||||
|
||||
node.callDecoration("setActiveExtruder", default_extruder_id)
|
||||
scene.sceneChanged.emit(node)
|
||||
@ -1828,15 +1846,21 @@ class CuraApplication(QtApplication):
|
||||
|
||||
def _onContextMenuRequested(self, x: float, y: float) -> None:
|
||||
# Ensure we select the object if we request a context menu over an object without having a selection.
|
||||
if not Selection.hasSelection():
|
||||
node = self.getController().getScene().findObject(cast(SelectionPass, self.getRenderer().getRenderPass("selection")).getIdAtPosition(x, y))
|
||||
if node:
|
||||
parent = node.getParent()
|
||||
while(parent and parent.callDecoration("isGroup")):
|
||||
node = parent
|
||||
parent = node.getParent()
|
||||
if Selection.hasSelection():
|
||||
return
|
||||
selection_pass = cast(SelectionPass, self.getRenderer().getRenderPass("selection"))
|
||||
if not selection_pass: # If you right-click before the rendering has been initialised there might not be a selection pass yet.
|
||||
print("--------------ding! Got the crash.")
|
||||
return
|
||||
node = self.getController().getScene().findObject(selection_pass.getIdAtPosition(x, y))
|
||||
if not node:
|
||||
return
|
||||
parent = node.getParent()
|
||||
while parent and parent.callDecoration("isGroup"):
|
||||
node = parent
|
||||
parent = node.getParent()
|
||||
|
||||
Selection.add(node)
|
||||
Selection.add(node)
|
||||
|
||||
@pyqtSlot()
|
||||
def showMoreInformationDialogForAnonymousDataCollection(self):
|
||||
@ -1871,16 +1895,14 @@ class CuraApplication(QtApplication):
|
||||
main_window = QtApplication.getInstance().getMainWindow()
|
||||
if main_window:
|
||||
return main_window.width()
|
||||
else:
|
||||
return 0
|
||||
return 0
|
||||
|
||||
@pyqtSlot(result = int)
|
||||
def appHeight(self) -> int:
|
||||
main_window = QtApplication.getInstance().getMainWindow()
|
||||
if main_window:
|
||||
return main_window.height()
|
||||
else:
|
||||
return 0
|
||||
return 0
|
||||
|
||||
@pyqtSlot()
|
||||
def deleteAll(self, only_selectable: bool = True) -> None:
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import List, Tuple
|
||||
from typing import List, Tuple, TYPE_CHECKING, Optional
|
||||
|
||||
from cura.CuraApplication import CuraApplication #To find some resource types.
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
@ -9,12 +9,16 @@ from cura.Settings.GlobalStack import GlobalStack
|
||||
from UM.PackageManager import PackageManager #The class we're extending.
|
||||
from UM.Resources import Resources #To find storage paths for some resource types.
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
from PyQt5.QtCore import QObject
|
||||
|
||||
|
||||
class CuraPackageManager(PackageManager):
|
||||
def __init__(self, application, parent = None):
|
||||
def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(application, parent)
|
||||
|
||||
def initialize(self):
|
||||
def initialize(self) -> None:
|
||||
self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
|
||||
self._installation_dirs_dict["qualities"] = Resources.getStoragePath(CuraApplication.ResourceTypes.QualityInstanceContainer)
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
CuraAppName = "@CURA_APP_NAME@"
|
||||
@ -9,3 +9,4 @@ CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
|
||||
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
|
||||
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
|
||||
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"
|
||||
CuraMarketplaceRoot = "@CURA_MARKETPLACE_ROOT@"
|
@ -26,6 +26,7 @@ class CuraView(View):
|
||||
def mainComponent(self) -> QUrl:
|
||||
return self.getDisplayComponent("main")
|
||||
|
||||
|
||||
@pyqtProperty(QUrl, constant = True)
|
||||
def stageMenuComponent(self) -> QUrl:
|
||||
url = self.getDisplayComponent("menu")
|
||||
|
@ -33,10 +33,10 @@ class Layer:
|
||||
def elementCount(self):
|
||||
return self._element_count
|
||||
|
||||
def setHeight(self, height):
|
||||
def setHeight(self, height: float) -> None:
|
||||
self._height = height
|
||||
|
||||
def setThickness(self, thickness):
|
||||
def setThickness(self, thickness: float) -> None:
|
||||
self._thickness = thickness
|
||||
|
||||
def lineMeshVertexCount(self) -> int:
|
||||
|
@ -16,8 +16,7 @@ class LayerData(MeshData):
|
||||
def getLayer(self, layer):
|
||||
if layer in self._layers:
|
||||
return self._layers[layer]
|
||||
else:
|
||||
return None
|
||||
return None
|
||||
|
||||
def getLayers(self):
|
||||
return self._layers
|
||||
|
@ -9,7 +9,7 @@ from cura.LayerData import LayerData
|
||||
|
||||
## Simple decorator to indicate a scene node holds layer data.
|
||||
class LayerDataDecorator(SceneNodeDecorator):
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._layer_data = None # type: Optional[LayerData]
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
from typing import Any, Optional
|
||||
import numpy
|
||||
|
||||
from typing import Optional, cast
|
||||
|
||||
from UM.Qt.Bindings.Theme import Theme
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
from UM.Logger import Logger
|
||||
|
||||
|
||||
@ -61,7 +62,7 @@ class LayerPolygon:
|
||||
|
||||
# When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType
|
||||
# Should be generated in better way, not hardcoded.
|
||||
self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype = numpy.bool)
|
||||
self._is_infill_or_skin_type_map = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype = numpy.bool)
|
||||
|
||||
self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray]
|
||||
self._build_cache_needed_points = None # type: Optional[numpy.ndarray]
|
||||
@ -149,17 +150,17 @@ class LayerPolygon:
|
||||
def getColors(self):
|
||||
return self._colors
|
||||
|
||||
def mapLineTypeToColor(self, line_types):
|
||||
def mapLineTypeToColor(self, line_types: numpy.ndarray) -> numpy.ndarray:
|
||||
return self._color_map[line_types]
|
||||
|
||||
def isInfillOrSkinType(self, line_types):
|
||||
return self._isInfillOrSkinTypeMap[line_types]
|
||||
def isInfillOrSkinType(self, line_types: numpy.ndarray) -> numpy.ndarray:
|
||||
return self._is_infill_or_skin_type_map[line_types]
|
||||
|
||||
def lineMeshVertexCount(self):
|
||||
return (self._vertex_end - self._vertex_begin)
|
||||
def lineMeshVertexCount(self) -> int:
|
||||
return self._vertex_end - self._vertex_begin
|
||||
|
||||
def lineMeshElementCount(self):
|
||||
return (self._index_end - self._index_begin)
|
||||
def lineMeshElementCount(self) -> int:
|
||||
return self._index_end - self._index_begin
|
||||
|
||||
@property
|
||||
def extruder(self):
|
||||
@ -202,7 +203,7 @@ class LayerPolygon:
|
||||
return self._jump_count
|
||||
|
||||
# Calculate normals for the entire polygon using numpy.
|
||||
def getNormals(self):
|
||||
def getNormals(self) -> numpy.ndarray:
|
||||
normals = numpy.copy(self._data)
|
||||
normals[:, 1] = 0.0 # We are only interested in 2D normals
|
||||
|
||||
@ -226,13 +227,13 @@ class LayerPolygon:
|
||||
|
||||
return normals
|
||||
|
||||
__color_map = None # type: numpy.ndarray[Any]
|
||||
__color_map = None # type: numpy.ndarray
|
||||
|
||||
## Gets the instance of the VersionUpgradeManager, or creates one.
|
||||
@classmethod
|
||||
def getColorMap(cls):
|
||||
def getColorMap(cls) -> numpy.ndarray:
|
||||
if cls.__color_map is None:
|
||||
theme = QtApplication.getInstance().getTheme()
|
||||
theme = cast(Theme, QtApplication.getInstance().getTheme())
|
||||
cls.__color_map = numpy.array([
|
||||
theme.getColor("layerview_none").getRgbF(), # NoneType
|
||||
theme.getColor("layerview_inset_0").getRgbF(), # Inset0Type
|
||||
|
@ -26,7 +26,7 @@ class ContainerNode:
|
||||
## Gets the metadata of the container that this node represents.
|
||||
# Getting the metadata from the container directly is about 10x as fast.
|
||||
# \return The metadata of the container in this node.
|
||||
def getMetadata(self):
|
||||
def getMetadata(self) -> Dict[str, Any]:
|
||||
return ContainerRegistry.getInstance().findContainersMetadata(id = self.container_id)[0]
|
||||
|
||||
## Get an entry from the metadata of the container that this node contains.
|
||||
|
@ -30,7 +30,7 @@ if TYPE_CHECKING:
|
||||
# nodes that have children) but that child node may be a node representing the
|
||||
# empty instance container.
|
||||
class ContainerTree:
|
||||
__instance = None
|
||||
__instance = None # type: Optional["ContainerTree"]
|
||||
|
||||
@classmethod
|
||||
def getInstance(cls):
|
||||
@ -75,7 +75,7 @@ class ContainerTree:
|
||||
return self.machines[global_stack.definition.getId()].getQualityChangesGroups(variant_names, material_bases, extruder_enabled)
|
||||
|
||||
## Ran after completely starting up the application.
|
||||
def _onStartupFinished(self):
|
||||
def _onStartupFinished(self) -> None:
|
||||
currently_added = ContainerRegistry.getInstance().findContainerStacks() # Find all currently added global stacks.
|
||||
JobQueue.getInstance().add(self._MachineNodeLoadJob(self, currently_added))
|
||||
|
||||
@ -137,7 +137,7 @@ class ContainerTree:
|
||||
# \param container_stacks All of the stacks to pre-load the container
|
||||
# trees for. This needs to be provided from here because the stacks
|
||||
# need to be constructed on the main thread because they are QObject.
|
||||
def __init__(self, tree_root: "ContainerTree", container_stacks: List["ContainerStack"]):
|
||||
def __init__(self, tree_root: "ContainerTree", container_stacks: List["ContainerStack"]) -> None:
|
||||
self.tree_root = tree_root
|
||||
self.container_stacks = container_stacks
|
||||
super().__init__()
|
||||
|
@ -6,13 +6,13 @@ import time
|
||||
from collections import deque
|
||||
|
||||
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtProperty
|
||||
from typing import Optional, Any, Set
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.Settings.SettingDefinition import SettingDefinition
|
||||
from UM.Settings.Validator import ValidatorState
|
||||
|
||||
|
||||
import cura.CuraApplication
|
||||
#
|
||||
# This class performs setting error checks for the currently active machine.
|
||||
#
|
||||
@ -24,25 +24,25 @@ from UM.Settings.Validator import ValidatorState
|
||||
#
|
||||
class MachineErrorChecker(QObject):
|
||||
|
||||
def __init__(self, parent = None):
|
||||
def __init__(self, parent: Optional[QObject] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self._global_stack = None
|
||||
|
||||
self._has_errors = True # Result of the error check, indicating whether there are errors in the stack
|
||||
self._error_keys = set() # A set of settings keys that have errors
|
||||
self._error_keys_in_progress = set() # The variable that stores the results of the currently in progress check
|
||||
self._error_keys = set() # type: Set[str] # A set of settings keys that have errors
|
||||
self._error_keys_in_progress = set() # type: Set[str] # The variable that stores the results of the currently in progress check
|
||||
|
||||
self._stacks_and_keys_to_check = None # a FIFO queue of tuples (stack, key) to check for errors
|
||||
self._stacks_and_keys_to_check = None # type: Optional[deque] # a FIFO queue of tuples (stack, key) to check for errors
|
||||
|
||||
self._need_to_check = False # Whether we need to schedule a new check or not. This flag is set when a new
|
||||
# error check needs to take place while there is already one running at the moment.
|
||||
self._check_in_progress = False # Whether there is an error check running in progress at the moment.
|
||||
|
||||
self._application = Application.getInstance()
|
||||
self._application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
self._machine_manager = self._application.getMachineManager()
|
||||
|
||||
self._start_time = 0 # measure checking time
|
||||
self._start_time = 0. # measure checking time
|
||||
|
||||
# This timer delays the starting of error check so we can react less frequently if the user is frequently
|
||||
# changing settings.
|
||||
@ -94,13 +94,13 @@ class MachineErrorChecker(QObject):
|
||||
|
||||
# Start the error check for property changed
|
||||
# this is seperate from the startErrorCheck because it ignores a number property types
|
||||
def startErrorCheckPropertyChanged(self, key, property_name):
|
||||
def startErrorCheckPropertyChanged(self, key: str, property_name: str) -> None:
|
||||
if property_name != "value":
|
||||
return
|
||||
self.startErrorCheck()
|
||||
|
||||
# Starts the error check timer to schedule a new error check.
|
||||
def startErrorCheck(self, *args) -> None:
|
||||
def startErrorCheck(self, *args: Any) -> None:
|
||||
if not self._check_in_progress:
|
||||
self._need_to_check = True
|
||||
self.needToWaitForResultChanged.emit()
|
||||
|
@ -176,9 +176,9 @@ class MachineNode(ContainerNode):
|
||||
|
||||
# Find the global qualities for this printer.
|
||||
global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = self.quality_definition, global_quality = "True") # First try specific to this printer.
|
||||
if len(global_qualities) == 0: # This printer doesn't override the global qualities.
|
||||
if not global_qualities: # This printer doesn't override the global qualities.
|
||||
global_qualities = container_registry.findInstanceContainersMetadata(type = "quality", definition = "fdmprinter", global_quality = "True") # Otherwise pick the global global qualities.
|
||||
if len(global_qualities) == 0: # There are no global qualities either?! Something went very wrong, but we'll not crash and properly fill the tree.
|
||||
if not global_qualities: # There are no global qualities either?! Something went very wrong, but we'll not crash and properly fill the tree.
|
||||
global_qualities = [cura.CuraApplication.CuraApplication.getInstance().empty_quality_container.getMetaData()]
|
||||
for global_quality in global_qualities:
|
||||
self.global_qualities[global_quality["quality_type"]] = QualityNode(global_quality["id"], parent = self)
|
||||
|
@ -14,6 +14,7 @@ if TYPE_CHECKING:
|
||||
from typing import Dict
|
||||
from cura.Machines.VariantNode import VariantNode
|
||||
|
||||
|
||||
## Represents a material in the container tree.
|
||||
#
|
||||
# Its subcontainers are quality profiles.
|
||||
|
@ -114,7 +114,10 @@ class IntentModel(ListModel):
|
||||
Logger.log("w", "Could not find the variant %s", active_variant_name)
|
||||
continue
|
||||
active_variant_node = machine_node.variants[active_variant_name]
|
||||
active_material_node = active_variant_node.materials[extruder.material.getMetaDataEntry("base_file")]
|
||||
active_material_node = active_variant_node.materials.get(extruder.material.getMetaDataEntry("base_file"))
|
||||
if active_material_node is None:
|
||||
Logger.log("w", "Could not find the material %s", extruder.material.getMetaDataEntry("base_file"))
|
||||
continue
|
||||
nodes.add(active_material_node)
|
||||
|
||||
return nodes
|
||||
|
@ -34,7 +34,7 @@ class MaterialBrandsModel(BaseMaterialsModel):
|
||||
brand_item_list = []
|
||||
brand_group_dict = {}
|
||||
|
||||
# Part 1: Generate the entire tree of brands -> material types -> spcific materials
|
||||
# Part 1: Generate the entire tree of brands -> material types -> specific materials
|
||||
for root_material_id, container_node in self._available_materials.items():
|
||||
# Do not include the materials from a to-be-removed package
|
||||
if bool(container_node.getMetaDataEntry("removed", False)):
|
||||
|
@ -2,6 +2,7 @@
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, Qt
|
||||
from typing import Set
|
||||
|
||||
import cura.CuraApplication
|
||||
from UM.Logger import Logger
|
||||
@ -23,7 +24,7 @@ class QualitySettingsModel(ListModel):
|
||||
|
||||
GLOBAL_STACK_POSITION = -1
|
||||
|
||||
def __init__(self, parent = None):
|
||||
def __init__(self, parent = None) -> None:
|
||||
super().__init__(parent = parent)
|
||||
|
||||
self.addRoleName(self.KeyRole, "key")
|
||||
@ -38,7 +39,9 @@ class QualitySettingsModel(ListModel):
|
||||
self._application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
self._application.getMachineManager().activeStackChanged.connect(self._update)
|
||||
|
||||
self._selected_position = self.GLOBAL_STACK_POSITION #Must be either GLOBAL_STACK_POSITION or an extruder position (0, 1, etc.)
|
||||
# Must be either GLOBAL_STACK_POSITION or an extruder position (0, 1, etc.)
|
||||
self._selected_position = self.GLOBAL_STACK_POSITION
|
||||
|
||||
self._selected_quality_item = None # The selected quality in the quality management page
|
||||
self._i18n_catalog = None
|
||||
|
||||
@ -47,14 +50,14 @@ class QualitySettingsModel(ListModel):
|
||||
selectedPositionChanged = pyqtSignal()
|
||||
selectedQualityItemChanged = pyqtSignal()
|
||||
|
||||
def setSelectedPosition(self, selected_position):
|
||||
def setSelectedPosition(self, selected_position: int) -> None:
|
||||
if selected_position != self._selected_position:
|
||||
self._selected_position = selected_position
|
||||
self.selectedPositionChanged.emit()
|
||||
self._update()
|
||||
|
||||
@pyqtProperty(int, fset = setSelectedPosition, notify = selectedPositionChanged)
|
||||
def selectedPosition(self):
|
||||
def selectedPosition(self) -> int:
|
||||
return self._selected_position
|
||||
|
||||
def setSelectedQualityItem(self, selected_quality_item):
|
||||
@ -67,7 +70,7 @@ class QualitySettingsModel(ListModel):
|
||||
def selectedQualityItem(self):
|
||||
return self._selected_quality_item
|
||||
|
||||
def _update(self):
|
||||
def _update(self) -> None:
|
||||
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||
|
||||
if not self._selected_quality_item:
|
||||
@ -83,7 +86,7 @@ class QualitySettingsModel(ListModel):
|
||||
quality_changes_group = self._selected_quality_item["quality_changes_group"]
|
||||
|
||||
quality_node = None
|
||||
settings_keys = set()
|
||||
settings_keys = set() # type: Set[str]
|
||||
if quality_group:
|
||||
if self._selected_position == self.GLOBAL_STACK_POSITION:
|
||||
quality_node = quality_group.node_for_global
|
||||
|
@ -41,4 +41,4 @@ class QualityNode(ContainerNode):
|
||||
self.intents[intent["id"]] = IntentNode(intent["id"], quality = self)
|
||||
|
||||
self.intents["empty_intent"] = IntentNode("empty_intent", quality = self)
|
||||
# Otherwise, there are no intents for global profiles.
|
||||
# Otherwise, there are no intents for global profiles.
|
||||
|
@ -51,7 +51,7 @@ class VariantNode(ContainerNode):
|
||||
# Find all the materials for this variant's name.
|
||||
else: # Printer has its own material profiles. Look for material profiles with this printer's definition.
|
||||
base_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = "fdmprinter")
|
||||
printer_specific_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = self.machine.container_id, variant_name = None)
|
||||
printer_specific_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = self.machine.container_id)
|
||||
variant_specific_materials = container_registry.findInstanceContainersMetadata(type = "material", definition = self.machine.container_id, variant_name = self.variant_name) # If empty_variant, this won't return anything.
|
||||
materials_per_base_file = {material["base_file"]: material for material in base_materials}
|
||||
materials_per_base_file.update({material["base_file"]: material for material in printer_specific_materials}) # Printer-specific profiles override global ones.
|
||||
|
@ -47,7 +47,7 @@ class MultiplyObjectsJob(Job):
|
||||
nodes = []
|
||||
|
||||
not_fit_count = 0
|
||||
|
||||
found_solution_for_all = False
|
||||
for node in self._objects:
|
||||
# If object is part of a group, multiply group
|
||||
current_node = node
|
||||
@ -66,7 +66,7 @@ class MultiplyObjectsJob(Job):
|
||||
|
||||
found_solution_for_all = True
|
||||
arranger.resetLastPriority()
|
||||
for i in range(self._count):
|
||||
for _ in range(self._count):
|
||||
# We do place the nodes one by one, as we want to yield in between.
|
||||
new_node = copy.deepcopy(node)
|
||||
solution_found = False
|
||||
@ -98,10 +98,10 @@ class MultiplyObjectsJob(Job):
|
||||
Job.yieldThread()
|
||||
|
||||
if nodes:
|
||||
op = GroupedOperation()
|
||||
operation = GroupedOperation()
|
||||
for new_node in nodes:
|
||||
op.addOperation(AddSceneNodeOperation(new_node, current_node.getParent()))
|
||||
op.push()
|
||||
operation.addOperation(AddSceneNodeOperation(new_node, current_node.getParent()))
|
||||
operation.push()
|
||||
status_message.hide()
|
||||
|
||||
if not found_solution_for_all:
|
||||
|
@ -115,9 +115,10 @@ class AuthorizationHelpers:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
## Generate a 16-character verification code.
|
||||
# \param code_length: How long should the code be?
|
||||
def generateVerificationCode(code_length: int = 16) -> str:
|
||||
## Generate a verification code of arbitrary length.
|
||||
# \param code_length: How long should the code be? This should never be lower than 16, but it's probably better to
|
||||
# leave it at 32
|
||||
def generateVerificationCode(code_length: int = 32) -> str:
|
||||
return "".join(random.choice("0123456789ABCDEF") for i in range(code_length))
|
||||
|
||||
@staticmethod
|
||||
|
@ -25,6 +25,8 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
||||
self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]]
|
||||
self.verification_code = None # type: Optional[str]
|
||||
|
||||
self.state = None # type: Optional[str]
|
||||
|
||||
# CURA-6609: Some browser seems to issue a HEAD instead of GET request as the callback.
|
||||
def do_HEAD(self) -> None:
|
||||
self.do_GET()
|
||||
@ -58,7 +60,14 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
||||
# \return HTTP ResponseData containing a success page to show to the user.
|
||||
def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]:
|
||||
code = self._queryGet(query, "code")
|
||||
if code and self.authorization_helpers is not None and self.verification_code is not None:
|
||||
state = self._queryGet(query, "state")
|
||||
if state != self.state:
|
||||
token_response = AuthenticationResponse(
|
||||
success = False,
|
||||
err_message=catalog.i18nc("@message",
|
||||
"The provided state is not correct.")
|
||||
)
|
||||
elif code and self.authorization_helpers is not None and self.verification_code is not None:
|
||||
# If the code was returned we get the access token.
|
||||
token_response = self.authorization_helpers.getAccessTokenUsingAuthorizationCode(
|
||||
code, self.verification_code)
|
||||
|
@ -25,3 +25,6 @@ class AuthorizationRequestServer(HTTPServer):
|
||||
## Set the verification code on the request handler.
|
||||
def setVerificationCode(self, verification_code: str) -> None:
|
||||
self.RequestHandlerClass.verification_code = verification_code # type: ignore
|
||||
|
||||
def setState(self, state: str) -> None:
|
||||
self.RequestHandlerClass.state = state # type: ignore
|
||||
|
@ -153,13 +153,15 @@ class AuthorizationService:
|
||||
verification_code = self._auth_helpers.generateVerificationCode()
|
||||
challenge_code = self._auth_helpers.generateVerificationCodeChallenge(verification_code)
|
||||
|
||||
state = AuthorizationHelpers.generateVerificationCode()
|
||||
|
||||
# Create the query string needed for the OAuth2 flow.
|
||||
query_string = urlencode({
|
||||
"client_id": self._settings.CLIENT_ID,
|
||||
"redirect_uri": self._settings.CALLBACK_URL,
|
||||
"scope": self._settings.CLIENT_SCOPES,
|
||||
"response_type": "code",
|
||||
"state": "(.Y.)",
|
||||
"state": state, # Forever in our Hearts, RIP "(.Y.)" (2018-2020)
|
||||
"code_challenge": challenge_code,
|
||||
"code_challenge_method": "S512"
|
||||
})
|
||||
@ -168,7 +170,7 @@ class AuthorizationService:
|
||||
QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string)))
|
||||
|
||||
# Start a local web server to receive the callback URL on.
|
||||
self._server.start(verification_code)
|
||||
self._server.start(verification_code, state)
|
||||
|
||||
## Callback method for the authentication flow.
|
||||
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:
|
||||
|
@ -1,13 +1,18 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import threading
|
||||
from typing import Optional, Callable, Any, TYPE_CHECKING
|
||||
from typing import Any, Callable, Optional, TYPE_CHECKING
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
||||
from cura.OAuth2.AuthorizationRequestServer import AuthorizationRequestServer
|
||||
from cura.OAuth2.AuthorizationRequestHandler import AuthorizationRequestHandler
|
||||
got_server_type = False
|
||||
try:
|
||||
from cura.OAuth2.AuthorizationRequestServer import AuthorizationRequestServer
|
||||
from cura.OAuth2.AuthorizationRequestHandler import AuthorizationRequestHandler
|
||||
got_server_type = True
|
||||
except PermissionError: # Bug in http.server: Can't access MIME types. This will prevent the user from logging in. See Sentry bug Cura-3Q.
|
||||
Logger.error("Can't start a server due to a PermissionError when starting the http.server.")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.OAuth2.Models import AuthenticationResponse
|
||||
@ -36,7 +41,8 @@ class LocalAuthorizationServer:
|
||||
|
||||
## Starts the local web server to handle the authorization callback.
|
||||
# \param verification_code The verification code part of the OAuth2 client identification.
|
||||
def start(self, verification_code: str) -> None:
|
||||
# \param state The unique state code (to ensure that the request we get back is really from the server.
|
||||
def start(self, verification_code: str, state: str) -> None:
|
||||
if self._web_server:
|
||||
# If the server is already running (because of a previously aborted auth flow), we don't have to start it.
|
||||
# We still inject the new verification code though.
|
||||
@ -49,14 +55,16 @@ class LocalAuthorizationServer:
|
||||
Logger.log("d", "Starting local web server to handle authorization callback on port %s", self._web_server_port)
|
||||
|
||||
# Create the server and inject the callback and code.
|
||||
self._web_server = AuthorizationRequestServer(("0.0.0.0", self._web_server_port), AuthorizationRequestHandler)
|
||||
self._web_server.setAuthorizationHelpers(self._auth_helpers)
|
||||
self._web_server.setAuthorizationCallback(self._auth_state_changed_callback)
|
||||
self._web_server.setVerificationCode(verification_code)
|
||||
if got_server_type:
|
||||
self._web_server = AuthorizationRequestServer(("0.0.0.0", self._web_server_port), AuthorizationRequestHandler)
|
||||
self._web_server.setAuthorizationHelpers(self._auth_helpers)
|
||||
self._web_server.setAuthorizationCallback(self._auth_state_changed_callback)
|
||||
self._web_server.setVerificationCode(verification_code)
|
||||
self._web_server.setState(state)
|
||||
|
||||
# Start the server on a new thread.
|
||||
self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon)
|
||||
self._web_server_thread.start()
|
||||
# Start the server on a new thread.
|
||||
self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon)
|
||||
self._web_server_thread.start()
|
||||
|
||||
## Stops the web server if it was running. It also does some cleanup.
|
||||
def stop(self) -> None:
|
||||
|
@ -1,10 +1,10 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
|
||||
class BaseModel:
|
||||
def __init__(self, **kwargs):
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
|
||||
@ -53,9 +53,10 @@ class ResponseData(BaseModel):
|
||||
redirect_uri = None # type: Optional[str]
|
||||
content_type = "text/html" # type: str
|
||||
|
||||
|
||||
## Possible HTTP responses.
|
||||
HTTP_STATUS = {
|
||||
"OK": ResponseStatus(code = 200, message = "OK"),
|
||||
"NOT_FOUND": ResponseStatus(code = 404, message = "NOT FOUND"),
|
||||
"REDIRECT": ResponseStatus(code = 302, message = "REDIRECT")
|
||||
}
|
||||
} # type: Dict[str, ResponseStatus]
|
||||
|
@ -122,6 +122,6 @@ class _ObjectOrder:
|
||||
# \param order List of indices in which to print objects, ordered by printing
|
||||
# order.
|
||||
# \param todo: List of indices which are not yet inserted into the order list.
|
||||
def __init__(self, order: List[SceneNode], todo: List[SceneNode]):
|
||||
def __init__(self, order: List[SceneNode], todo: List[SceneNode]) -> None:
|
||||
self.order = order
|
||||
self.todo = todo
|
||||
|
@ -1,26 +1,27 @@
|
||||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Operations.Operation import Operation
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
|
||||
|
||||
## A specialised operation designed specifically to modify the previous operation.
|
||||
class PlatformPhysicsOperation(Operation):
|
||||
def __init__(self, node, translation):
|
||||
def __init__(self, node: SceneNode, translation: Vector) -> None:
|
||||
super().__init__()
|
||||
self._node = node
|
||||
self._old_transformation = node.getLocalTransformation()
|
||||
self._translation = translation
|
||||
self._always_merge = True
|
||||
|
||||
def undo(self):
|
||||
def undo(self) -> None:
|
||||
self._node.setTransformation(self._old_transformation)
|
||||
|
||||
def redo(self):
|
||||
def redo(self) -> None:
|
||||
self._node.translate(self._translation, SceneNode.TransformSpace.World)
|
||||
|
||||
def mergeWith(self, other):
|
||||
def mergeWith(self, other: Operation) -> GroupedOperation:
|
||||
group = GroupedOperation()
|
||||
|
||||
group.addOperation(other)
|
||||
@ -28,5 +29,5 @@ class PlatformPhysicsOperation(Operation):
|
||||
|
||||
return group
|
||||
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return "PlatformPhysicsOp.(trans.={0})".format(self._translation)
|
||||
|
@ -6,9 +6,9 @@ from UM.Operations.Operation import Operation
|
||||
|
||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator
|
||||
|
||||
|
||||
## Simple operation to set the buildplate number of a scenenode.
|
||||
class SetBuildPlateNumberOperation(Operation):
|
||||
|
||||
def __init__(self, node: SceneNode, build_plate_nr: int) -> None:
|
||||
super().__init__()
|
||||
self._node = node
|
||||
@ -16,11 +16,11 @@ class SetBuildPlateNumberOperation(Operation):
|
||||
self._previous_build_plate_nr = None
|
||||
self._decorator_added = False
|
||||
|
||||
def undo(self):
|
||||
def undo(self) -> None:
|
||||
if self._previous_build_plate_nr:
|
||||
self._node.callDecoration("setBuildPlateNumber", self._previous_build_plate_nr)
|
||||
|
||||
def redo(self):
|
||||
def redo(self) -> None:
|
||||
stack = self._node.callDecoration("getStack") #Don't try to get the active extruder since it may be None anyway.
|
||||
if not stack:
|
||||
self._node.addDecorator(SettingOverrideDecorator())
|
||||
|
@ -1,36 +1,37 @@
|
||||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional
|
||||
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Operations import Operation
|
||||
|
||||
from UM.Math.Vector import Vector
|
||||
|
||||
## An operation that parents a scene node to another scene node.
|
||||
|
||||
## An operation that parents a scene node to another scene node.
|
||||
class SetParentOperation(Operation.Operation):
|
||||
## Initialises this SetParentOperation.
|
||||
#
|
||||
# \param node The node which will be reparented.
|
||||
# \param parent_node The node which will be the parent.
|
||||
def __init__(self, node, parent_node):
|
||||
def __init__(self, node: SceneNode, parent_node: Optional[SceneNode]) -> None:
|
||||
super().__init__()
|
||||
self._node = node
|
||||
self._parent = parent_node
|
||||
self._old_parent = node.getParent() # To restore the previous parent in case of an undo.
|
||||
|
||||
## Undoes the set-parent operation, restoring the old parent.
|
||||
def undo(self):
|
||||
def undo(self) -> None:
|
||||
self._set_parent(self._old_parent)
|
||||
|
||||
## Re-applies the set-parent operation.
|
||||
def redo(self):
|
||||
def redo(self) -> None:
|
||||
self._set_parent(self._parent)
|
||||
|
||||
## Sets the parent of the node while applying transformations to the world-transform of the node stays the same.
|
||||
#
|
||||
# \param new_parent The new parent. Note: this argument can be None, which would hide the node from the scene.
|
||||
def _set_parent(self, new_parent):
|
||||
def _set_parent(self, new_parent: Optional[SceneNode]) -> None:
|
||||
if new_parent:
|
||||
current_parent = self._node.getParent()
|
||||
if current_parent:
|
||||
@ -59,5 +60,5 @@ class SetParentOperation(Operation.Operation):
|
||||
## Returns a programmer-readable representation of this operation.
|
||||
#
|
||||
# \return A programmer-readable representation of this operation.
|
||||
def __repr__(self):
|
||||
def __repr__(self) -> str:
|
||||
return "SetParentOperation(node = {0}, parent_node={1})".format(self._node, self._parent)
|
||||
|
@ -1,9 +1,11 @@
|
||||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QTimer
|
||||
from shapely.errors import TopologicalError # To capture errors if Shapely messes up.
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||
from UM.Math.Vector import Vector
|
||||
@ -136,7 +138,11 @@ class PlatformPhysics:
|
||||
own_convex_hull = node.callDecoration("getConvexHull")
|
||||
other_convex_hull = other_node.callDecoration("getConvexHull")
|
||||
if own_convex_hull and other_convex_hull:
|
||||
overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull)
|
||||
try:
|
||||
overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull)
|
||||
except TopologicalError as e: # Can happen if the convex hull is degenerate?
|
||||
Logger.warning("Got a topological error when calculating convex hull intersection: {err}".format(err = str(e)))
|
||||
overlap = False
|
||||
if overlap: # Moving ensured that overlap was still there. Try anew!
|
||||
temp_move_vector = move_vector.set(x = move_vector.x + overlap[0] * self._move_factor,
|
||||
z = move_vector.z + overlap[1] * self._move_factor)
|
||||
|
@ -17,9 +17,6 @@ from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.View.GL.ShaderProgram import ShaderProgram
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from UM.Scene.Camera import Camera
|
||||
|
||||
|
||||
|
@ -3,6 +3,7 @@ from PyQt5.QtQuick import QQuickImageProvider
|
||||
from PyQt5.QtCore import QSize
|
||||
|
||||
from UM.Application import Application
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
class PrintJobPreviewImageProvider(QQuickImageProvider):
|
||||
@ -10,7 +11,7 @@ class PrintJobPreviewImageProvider(QQuickImageProvider):
|
||||
super().__init__(QQuickImageProvider.Image)
|
||||
|
||||
## Request a new image.
|
||||
def requestImage(self, id: str, size: QSize) -> QImage:
|
||||
def requestImage(self, id: str, size: QSize) -> Tuple[QImage, QSize]:
|
||||
# The id will have an uuid and an increment separated by a slash. As we don't care about the value of the
|
||||
# increment, we need to strip that first.
|
||||
uuid = id[id.find("/") + 1:]
|
||||
@ -22,6 +23,6 @@ class PrintJobPreviewImageProvider(QQuickImageProvider):
|
||||
if print_job.key == uuid:
|
||||
if print_job.getPreviewImage():
|
||||
return print_job.getPreviewImage(), QSize(15, 15)
|
||||
else:
|
||||
return QImage(), QSize(15, 15)
|
||||
return QImage(), QSize(15,15)
|
||||
|
||||
return QImage(), QSize(15, 15)
|
||||
return QImage(), QSize(15, 15)
|
@ -33,6 +33,10 @@ class FirmwareUpdater(QObject):
|
||||
else:
|
||||
self._firmware_file = firmware_file
|
||||
|
||||
if self._firmware_file == "":
|
||||
self._setFirmwareUpdateState(FirmwareUpdateState.firmware_not_found_error)
|
||||
return
|
||||
|
||||
self._setFirmwareUpdateState(FirmwareUpdateState.updating)
|
||||
|
||||
self._update_firmware_thread.start()
|
||||
|
@ -161,7 +161,7 @@ class PrintJobOutputModel(QObject):
|
||||
self._time_elapsed = new_time_elapsed
|
||||
self.timeElapsedChanged.emit()
|
||||
|
||||
def updateState(self, new_state):
|
||||
def updateState(self, new_state: str) -> None:
|
||||
if self._state != new_state:
|
||||
self._state = new_state
|
||||
self.stateChanged.emit()
|
||||
|
@ -148,7 +148,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
|
||||
@pyqtProperty(QObject, notify = printersChanged)
|
||||
def activePrinter(self) -> Optional["PrinterOutputModel"]:
|
||||
if len(self._printers):
|
||||
if self._printers:
|
||||
return self._printers[0]
|
||||
return None
|
||||
|
||||
|
@ -10,3 +10,6 @@ class BlockSlicingDecorator(SceneNodeDecorator):
|
||||
|
||||
def isBlockSlicing(self) -> bool:
|
||||
return True
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
return BlockSlicingDecorator()
|
@ -36,8 +36,10 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
|
||||
# Make sure the timer is created on the main thread
|
||||
self._recompute_convex_hull_timer = None # type: Optional[QTimer]
|
||||
self._timer_scheduled_to_be_created = False
|
||||
from cura.CuraApplication import CuraApplication
|
||||
if CuraApplication.getInstance() is not None:
|
||||
self._timer_scheduled_to_be_created = True
|
||||
CuraApplication.getInstance().callLater(self.createRecomputeConvexHullTimer)
|
||||
|
||||
self._raft_thickness = 0.0
|
||||
@ -171,7 +173,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
if self._recompute_convex_hull_timer is not None:
|
||||
self._recompute_convex_hull_timer.start()
|
||||
else:
|
||||
self.recomputeConvexHull()
|
||||
from cura.CuraApplication import CuraApplication
|
||||
if not self._timer_scheduled_to_be_created:
|
||||
# The timer is not created and we never scheduled it. Time to create it now!
|
||||
CuraApplication.getInstance().callLater(self.createRecomputeConvexHullTimer)
|
||||
# Now we know for sure that the timer has been scheduled for creation, so we can try this again.
|
||||
CuraApplication.getInstance().callLater(self.recomputeConvexHullDelayed)
|
||||
|
||||
def recomputeConvexHull(self) -> None:
|
||||
controller = Application.getInstance().getController()
|
||||
|
@ -17,8 +17,8 @@ class GCodeListDecorator(SceneNodeDecorator):
|
||||
def getGCodeList(self) -> List[str]:
|
||||
return self._gcode_list
|
||||
|
||||
def setGCodeList(self, list: List[str]) -> None:
|
||||
self._gcode_list = list
|
||||
def setGCodeList(self, gcode_list: List[str]) -> None:
|
||||
self._gcode_list = gcode_list
|
||||
|
||||
def __deepcopy__(self, memo) -> "GCodeListDecorator":
|
||||
copied_decorator = GCodeListDecorator()
|
||||
|
@ -239,6 +239,8 @@ class ContainerManager(QObject):
|
||||
container_type = container_registry.getContainerForMimeType(mime_type)
|
||||
if not container_type:
|
||||
return {"status": "error", "message": "Could not find a container to handle the specified file."}
|
||||
if not issubclass(container_type, InstanceContainer):
|
||||
return {"status": "error", "message": "This is not a material container, but another type of file."}
|
||||
|
||||
container_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_url)))
|
||||
container_id = container_registry.uniqueName(container_id)
|
||||
|
@ -15,7 +15,6 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.SettingInstance import SettingInstance
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Platform import Platform
|
||||
@ -176,7 +175,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
if not file_name:
|
||||
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Failed to import profile from <filename>{0}</filename>: {1}", file_name, "Invalid path")}
|
||||
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Can't import profile from <filename>{0}</filename> before a printer is added.", file_name)}
|
||||
container_tree = ContainerTree.getInstance()
|
||||
@ -384,7 +383,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
if not quality_type:
|
||||
return catalog.i18nc("@info:status", "Profile is missing a quality type.")
|
||||
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
return None
|
||||
definition_id = ContainerTree.getInstance().machines[global_stack.definition.getId()].quality_definition
|
||||
|
@ -133,6 +133,38 @@ class CuraFormulaFunctions:
|
||||
context = self.createContextForDefaultValueEvaluation(global_stack)
|
||||
return self.getResolveOrValue(property_key, context = context)
|
||||
|
||||
# Gets the value for the given setting key starting from the given container index.
|
||||
def getValueFromContainerAtIndex(self, property_key: str, container_index: int,
|
||||
context: Optional["PropertyEvaluationContext"] = None) -> Any:
|
||||
machine_manager = self._application.getMachineManager()
|
||||
global_stack = machine_manager.activeMachine
|
||||
|
||||
context = self.createContextForDefaultValueEvaluation(global_stack)
|
||||
context.context["evaluate_from_container_index"] = container_index
|
||||
|
||||
return global_stack.getProperty(property_key, "value", context = context)
|
||||
|
||||
# Gets the extruder value for the given setting key starting from the given container index.
|
||||
def getValueFromContainerAtIndexInExtruder(self, extruder_position: int, property_key: str, container_index: int,
|
||||
context: Optional["PropertyEvaluationContext"] = None) -> Any:
|
||||
machine_manager = self._application.getMachineManager()
|
||||
global_stack = machine_manager.activeMachine
|
||||
|
||||
if extruder_position == -1:
|
||||
extruder_position = int(machine_manager.defaultExtruderPosition)
|
||||
|
||||
global_stack = machine_manager.activeMachine
|
||||
try:
|
||||
extruder_stack = global_stack.extruderList[int(extruder_position)]
|
||||
except IndexError:
|
||||
Logger.log("w", "Value for %s of extruder %s was requested, but that extruder is not available. " % (property_key, extruder_position))
|
||||
return None
|
||||
|
||||
context = self.createContextForDefaultValueEvaluation(extruder_stack)
|
||||
context.context["evaluate_from_container_index"] = container_index
|
||||
|
||||
return self.getValueInExtruder(extruder_position, property_key, context)
|
||||
|
||||
# Creates a context for evaluating default values (skip the user_changes container).
|
||||
def createContextForDefaultValueEvaluation(self, source_stack: "CuraContainerStack") -> "PropertyEvaluationContext":
|
||||
context = PropertyEvaluationContext(source_stack)
|
||||
|
@ -58,7 +58,10 @@ class CuraStackBuilder:
|
||||
# Create ExtruderStacks
|
||||
extruder_dict = machine_definition.getMetaDataEntry("machine_extruder_trains")
|
||||
for position in extruder_dict:
|
||||
cls.createExtruderStackWithDefaultSetup(new_global_stack, position)
|
||||
try:
|
||||
cls.createExtruderStackWithDefaultSetup(new_global_stack, position)
|
||||
except IndexError:
|
||||
return None
|
||||
|
||||
for new_extruder in new_global_stack.extruders.values(): # Only register the extruders if we're sure that all of them are correct.
|
||||
registry.addContainer(new_extruder)
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt.
|
||||
@ -275,6 +275,25 @@ class ExtruderManager(QObject):
|
||||
Logger.log("e", "Unable to find one or more of the extruders in %s", used_extruder_stack_ids)
|
||||
return []
|
||||
|
||||
## Get the extruder that the print will start with.
|
||||
#
|
||||
# This should mirror the implementation in CuraEngine of
|
||||
# ``FffGcodeWriter::getStartExtruder()``.
|
||||
def getInitialExtruderNr(self) -> int:
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
global_stack = application.getGlobalContainerStack()
|
||||
|
||||
# Starts with the adhesion extruder.
|
||||
if global_stack.getProperty("adhesion_type", "value") != "none":
|
||||
return global_stack.getProperty("adhesion_extruder_nr", "value")
|
||||
|
||||
# No adhesion? Well maybe there is still support brim.
|
||||
if (global_stack.getProperty("support_enable", "value") or global_stack.getProperty("support_tree_enable", "value")) and global_stack.getProperty("support_brim_enable", "value"):
|
||||
return global_stack.getProperty("support_infill_extruder_nr", "value")
|
||||
|
||||
# REALLY no adhesion? Use the first used extruder.
|
||||
return self.getUsedExtruderStacks()[0].getProperty("extruder_nr", "value")
|
||||
|
||||
## Removes the container stack and user profile for the extruders for a specific machine.
|
||||
#
|
||||
# \param machine_id The machine to remove the extruders for.
|
||||
|
@ -320,8 +320,19 @@ class MachineManager(QObject):
|
||||
# This signal might not have been emitted yet (if it didn't change) but we still want the models to update that depend on it because we changed the contents of the containers too.
|
||||
extruder_manager.activeExtruderChanged.emit()
|
||||
|
||||
# Validate if the machine has the correct variants
|
||||
# It can happen that a variant is empty, even though the machine has variants. This will ensure that that
|
||||
# that situation will be fixed (and not occur again, since it switches it out to the preferred variant instead!)
|
||||
machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
|
||||
for extruder in self._global_container_stack.extruderList:
|
||||
variant_name = extruder.variant.getName()
|
||||
variant_node = machine_node.variants.get(variant_name)
|
||||
if variant_node is None:
|
||||
Logger.log("w", "An extruder has an unknown variant, switching it to the preferred variant")
|
||||
self.setVariantByName(extruder.getMetaDataEntry("position"), machine_node.preferred_variant_name)
|
||||
self.__emitChangedSignals()
|
||||
|
||||
|
||||
## Given a definition id, return the machine with this id.
|
||||
# Optional: add a list of keys and values to filter the list of machines with the given definition id
|
||||
# \param definition_id \type{str} definition id that needs to look for
|
||||
@ -336,9 +347,9 @@ class MachineManager(QObject):
|
||||
return cast(GlobalStack, machine)
|
||||
return None
|
||||
|
||||
@pyqtSlot(str)
|
||||
@pyqtSlot(str, str)
|
||||
def addMachine(self, definition_id: str, name: Optional[str] = None) -> None:
|
||||
@pyqtSlot(str, result=bool)
|
||||
@pyqtSlot(str, str, result = bool)
|
||||
def addMachine(self, definition_id: str, name: Optional[str] = None) -> bool:
|
||||
Logger.log("i", "Trying to add a machine with the definition id [%s]", definition_id)
|
||||
if name is None:
|
||||
definitions = CuraContainerRegistry.getInstance().findDefinitionContainers(id = definition_id)
|
||||
@ -353,6 +364,8 @@ class MachineManager(QObject):
|
||||
self.setActiveMachine(new_stack.getId())
|
||||
else:
|
||||
Logger.log("w", "Failed creating a new machine!")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _checkStacksHaveErrors(self) -> bool:
|
||||
time_start = time.time()
|
||||
@ -671,7 +684,10 @@ class MachineManager(QObject):
|
||||
if other_machine_stacks:
|
||||
self.setActiveMachine(other_machine_stacks[0]["id"])
|
||||
|
||||
metadata = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0]
|
||||
metadatas = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)
|
||||
if not metadatas:
|
||||
return # machine_id doesn't exist. Nothing to remove.
|
||||
metadata = metadatas[0]
|
||||
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
|
||||
containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
|
||||
for container in containers:
|
||||
@ -747,6 +763,11 @@ class MachineManager(QObject):
|
||||
result = [] # type: List[str]
|
||||
for setting_instance in container.findInstances():
|
||||
setting_key = setting_instance.definition.key
|
||||
if setting_key == "print_sequence":
|
||||
old_value = container.getProperty(setting_key, "value")
|
||||
Logger.log("d", "Reset setting [%s] in [%s] because its old value [%s] is no longer valid", setting_key, container, old_value)
|
||||
result.append(setting_key)
|
||||
continue
|
||||
if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"):
|
||||
continue
|
||||
|
||||
@ -795,7 +816,7 @@ class MachineManager(QObject):
|
||||
definition_changes_container.setProperty("machine_extruder_count", "value", extruder_count)
|
||||
|
||||
self.updateDefaultExtruder()
|
||||
self.updateNumberExtrudersEnabled()
|
||||
self.numberExtrudersEnabledChanged.emit()
|
||||
self.correctExtruderSettings()
|
||||
|
||||
# Check to see if any objects are set to print with an extruder that will no longer exist
|
||||
@ -1502,7 +1523,17 @@ class MachineManager(QObject):
|
||||
if quality_id == empty_quality_container.getId():
|
||||
extruder.intent = empty_intent_container
|
||||
continue
|
||||
quality_node = container_tree.machines[definition_id].variants[variant_name].materials[material_base_file].qualities[quality_id]
|
||||
|
||||
# Yes, we can find this in a single line of code. This makes it easier to read and it has the benefit
|
||||
# that it doesn't lump key errors together for the crashlogs
|
||||
try:
|
||||
machine_node = container_tree.machines[definition_id]
|
||||
variant_node = machine_node.variants[variant_name]
|
||||
material_node = variant_node.materials[material_base_file]
|
||||
quality_node = material_node.qualities[quality_id]
|
||||
except KeyError as e:
|
||||
Logger.error("Can't set the intent category '{category}' since the profile '{profile}' in the stack is not supported according to the container tree.".format(category = intent_category, profile = e))
|
||||
continue
|
||||
|
||||
for intent_node in quality_node.intents.values():
|
||||
if intent_node.intent_category == intent_category: # Found an intent with the correct category.
|
||||
|
@ -43,7 +43,7 @@ class MachineActionManager(QObject):
|
||||
# Dict of all actions that need to be done when first added by definition ID
|
||||
self._first_start_actions = {} # type: Dict[str, List[MachineAction]]
|
||||
|
||||
def initialize(self):
|
||||
def initialize(self) -> None:
|
||||
# Add machine_action as plugin type
|
||||
PluginRegistry.addType("machine_action", self.addMachineAction)
|
||||
|
||||
|
@ -28,7 +28,11 @@ class TextManager(QObject):
|
||||
|
||||
def _loadChangeLogText(self) -> str:
|
||||
# Load change log texts and organize them with a dict
|
||||
file_path = Resources.getPath(Resources.Texts, "change_log.txt")
|
||||
try:
|
||||
file_path = Resources.getPath(Resources.Texts, "change_log.txt")
|
||||
except FileNotFoundError:
|
||||
# I have no idea how / when this happens, but we're getting crash reports about it.
|
||||
return ""
|
||||
change_logs_dict = {} # type: Dict[Version, Dict[str, List[str]]]
|
||||
with open(file_path, "r", encoding = "utf-8") as f:
|
||||
open_version = None # type: Optional[Version]
|
||||
|
31
cura/UltimakerCloud/UltimakerCloudScope.py
Normal file
31
cura/UltimakerCloud/UltimakerCloudScope.py
Normal file
@ -0,0 +1,31 @@
|
||||
from PyQt5.QtNetwork import QNetworkRequest
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.TaskManagement.HttpRequestScope import DefaultUserAgentScope
|
||||
from cura.API import Account
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
class UltimakerCloudScope(DefaultUserAgentScope):
|
||||
"""Add an Authorization header to the request for Ultimaker Cloud Api requests.
|
||||
|
||||
When the user is not logged in or a token is not available, a warning will be logged
|
||||
Also add the user agent headers (see DefaultUserAgentScope)
|
||||
"""
|
||||
|
||||
def __init__(self, application: CuraApplication):
|
||||
super().__init__(application)
|
||||
api = application.getCuraAPI()
|
||||
self._account = api.account # type: Account
|
||||
|
||||
def requestHook(self, request: QNetworkRequest):
|
||||
super().requestHook(request)
|
||||
token = self._account.accessToken
|
||||
if not self._account.isLoggedIn or token is None:
|
||||
Logger.warning("Cannot add authorization to Cloud Api request")
|
||||
return
|
||||
|
||||
header_dict = {
|
||||
"Authorization": "Bearer {}".format(token)
|
||||
}
|
||||
self.addHeaders(request, header_dict)
|
0
cura/UltimakerCloud/__init__.py
Normal file
0
cura/UltimakerCloud/__init__.py
Normal file
54
cura_app.py
54
cura_app.py
@ -1,16 +1,34 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
# Remove the working directory from sys.path.
|
||||
# This fixes a security issue where Cura could import Python packages from the
|
||||
# current working directory, and therefore be made to execute locally installed
|
||||
# code (e.g. in the user's home directory where AppImages by default run from).
|
||||
# See issue CURA-7081.
|
||||
import sys
|
||||
if "" in sys.path:
|
||||
sys.path.remove("")
|
||||
|
||||
import argparse
|
||||
import faulthandler
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Workaround for a race condition on certain systems where there
|
||||
# is a race condition between Arcus and PyQt. Importing Arcus
|
||||
# first seems to prevent Sip from going into a state where it
|
||||
# tries to create PyQt objects on a non-main thread.
|
||||
import Arcus # @UnusedImport
|
||||
import Savitar # @UnusedImport
|
||||
|
||||
from PyQt5.QtNetwork import QSslConfiguration, QSslSocket
|
||||
|
||||
from UM.Platform import Platform
|
||||
from cura import ApplicationMetadata
|
||||
from cura.ApplicationMetadata import CuraAppName
|
||||
from cura.CrashHandler import CrashHandler
|
||||
|
||||
try:
|
||||
import sentry_sdk
|
||||
@ -29,21 +47,30 @@ parser.add_argument("--debug",
|
||||
known_args = vars(parser.parse_known_args()[0])
|
||||
|
||||
if with_sentry_sdk:
|
||||
sentry_env = "production"
|
||||
sentry_env = "unknown" # Start off with a "IDK"
|
||||
if hasattr(sys, "frozen"):
|
||||
sentry_env = "production" # A frozen build has the posibility to be a "real" distribution.
|
||||
|
||||
if ApplicationMetadata.CuraVersion == "master":
|
||||
sentry_env = "development"
|
||||
sentry_env = "development" # Master is always a development version.
|
||||
elif ApplicationMetadata.CuraVersion in ["beta", "BETA"]:
|
||||
sentry_env = "beta"
|
||||
try:
|
||||
if ApplicationMetadata.CuraVersion.split(".")[2] == "99":
|
||||
sentry_env = "nightly"
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
|
||||
# Errors to be ignored by Sentry
|
||||
ignore_errors = [KeyboardInterrupt, MemoryError]
|
||||
sentry_sdk.init("https://5034bf0054fb4b889f82896326e79b13@sentry.io/1821564",
|
||||
before_send = CrashHandler.sentryBeforeSend,
|
||||
environment = sentry_env,
|
||||
release = "cura%s" % ApplicationMetadata.CuraVersion,
|
||||
default_integrations = False,
|
||||
max_breadcrumbs = 300,
|
||||
server_name = "cura")
|
||||
server_name = "cura",
|
||||
ignore_errors = ignore_errors)
|
||||
|
||||
if not known_args["debug"]:
|
||||
def get_cura_dir_path():
|
||||
@ -156,17 +183,11 @@ def exceptHook(hook_type, value, traceback):
|
||||
# Set exception hook to use the crash dialog handler
|
||||
sys.excepthook = exceptHook
|
||||
# Enable dumping traceback for all threads
|
||||
if sys.stderr:
|
||||
if sys.stderr and not sys.stderr.closed:
|
||||
faulthandler.enable(file = sys.stderr, all_threads = True)
|
||||
else:
|
||||
elif sys.stdout and not sys.stdout.closed:
|
||||
faulthandler.enable(file = sys.stdout, all_threads = True)
|
||||
|
||||
# Workaround for a race condition on certain systems where there
|
||||
# is a race condition between Arcus and PyQt. Importing Arcus
|
||||
# first seems to prevent Sip from going into a state where it
|
||||
# tries to create PyQt objects on a non-main thread.
|
||||
import Arcus #@UnusedImport
|
||||
import Savitar #@UnusedImport
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
@ -201,5 +222,10 @@ if Platform.isLinux() and getattr(sys, "frozen", False):
|
||||
import trimesh.exchange.load
|
||||
os.environ["LD_LIBRARY_PATH"] = old_env
|
||||
|
||||
if ApplicationMetadata.CuraDebugMode:
|
||||
ssl_conf = QSslConfiguration.defaultConfiguration()
|
||||
ssl_conf.setPeerVerifyMode(QSslSocket.VerifyNone)
|
||||
QSslConfiguration.setDefaultConfiguration(ssl_conf)
|
||||
|
||||
app = CuraApplication()
|
||||
app.run()
|
||||
|
@ -13,28 +13,46 @@ export PKG_CONFIG_PATH="${CURA_BUILD_ENV_PATH}/lib/pkgconfig:${PKG_CONFIG_PATH}"
|
||||
|
||||
cd "${PROJECT_DIR}"
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Clone Uranium and set PYTHONPATH first
|
||||
#
|
||||
|
||||
# Check the branch to use:
|
||||
# 1. Use the Uranium branch with the branch same if it exists.
|
||||
# 2. Otherwise, use the default branch name "master"
|
||||
# Check the branch to use for Uranium.
|
||||
# It tries the following branch names and uses the first one that's available.
|
||||
# - GITHUB_HEAD_REF: the branch name of a PR. If it's not a PR, it will be empty.
|
||||
# - GITHUB_BASE_REF: the branch a PR is based on. If it's not a PR, it will be empty.
|
||||
# - GITHUB_REF: the branch name if it's a branch on the repository;
|
||||
# refs/pull/123/merge if it's a pull_request.
|
||||
# - master: the master branch. It should always exist.
|
||||
|
||||
# For debugging.
|
||||
echo "GITHUB_REF: ${GITHUB_REF}"
|
||||
echo "GITHUB_HEAD_REF: ${GITHUB_HEAD_REF}"
|
||||
echo "GITHUB_BASE_REF: ${GITHUB_BASE_REF}"
|
||||
|
||||
GIT_REF_NAME="${GITHUB_REF}"
|
||||
if [ -n "${GITHUB_BASE_REF}" ]; then
|
||||
GIT_REF_NAME="${GITHUB_BASE_REF}"
|
||||
fi
|
||||
GIT_REF_NAME="$(basename "${GIT_REF_NAME}")"
|
||||
|
||||
URANIUM_BRANCH="${GIT_REF_NAME:-master}"
|
||||
output="$(git ls-remote --heads https://github.com/Ultimaker/Uranium.git "${URANIUM_BRANCH}")"
|
||||
if [ -z "${output}" ]; then
|
||||
echo "Could not find Uranium banch ${URANIUM_BRANCH}, fallback to use master."
|
||||
URANIUM_BRANCH="master"
|
||||
fi
|
||||
GIT_REF_NAME_LIST=( "${GITHUB_HEAD_REF}" "${GITHUB_BASE_REF}" "${GITHUB_REF}" "master" )
|
||||
for git_ref_name in "${GIT_REF_NAME_LIST[@]}"
|
||||
do
|
||||
if [ -z "${git_ref_name}" ]; then
|
||||
continue
|
||||
fi
|
||||
git_ref_name="$(basename "${git_ref_name}")"
|
||||
# Skip refs/pull/1234/merge as pull requests use it as GITHUB_REF
|
||||
if [[ "${git_ref_name}" == "merge" ]]; then
|
||||
echo "Skip [${git_ref_name}]"
|
||||
continue
|
||||
fi
|
||||
URANIUM_BRANCH="${git_ref_name}"
|
||||
output="$(git ls-remote --heads https://github.com/Ultimaker/Uranium.git "${URANIUM_BRANCH}")"
|
||||
if [ -n "${output}" ]; then
|
||||
echo "Found Uranium branch [${URANIUM_BRANCH}]."
|
||||
break
|
||||
else
|
||||
echo "Could not find Uranium banch [${URANIUM_BRANCH}], try next."
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Using Uranium branch ${URANIUM_BRANCH} ..."
|
||||
git clone --depth=1 -b "${URANIUM_BRANCH}" https://github.com/Ultimaker/Uranium.git "${PROJECT_DIR}"/Uranium
|
||||
|
@ -52,7 +52,6 @@ class ThreeMFReader(MeshReader):
|
||||
self._root = None
|
||||
self._base_name = ""
|
||||
self._unit = None
|
||||
self._object_count = 0 # Used to name objects as there is no node name yet.
|
||||
|
||||
def _createMatrixFromTransformationString(self, transformation: str) -> Matrix:
|
||||
if transformation == "":
|
||||
@ -86,15 +85,21 @@ class ThreeMFReader(MeshReader):
|
||||
|
||||
## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
|
||||
# \returns Scene node.
|
||||
def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode) -> Optional[SceneNode]:
|
||||
self._object_count += 1
|
||||
node_name = "Object %s" % self._object_count
|
||||
def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]:
|
||||
node_name = savitar_node.getName()
|
||||
node_id = savitar_node.getId()
|
||||
if node_name == "":
|
||||
if file_name != "":
|
||||
node_name = os.path.basename(file_name)
|
||||
else:
|
||||
node_name = "Object {}".format(node_id)
|
||||
|
||||
active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||
|
||||
um_node = CuraSceneNode() # This adds a SettingOverrideDecorator
|
||||
um_node.addDecorator(BuildPlateDecorator(active_build_plate))
|
||||
um_node.setName(node_name)
|
||||
um_node.setId(node_id)
|
||||
transformation = self._createMatrixFromTransformationString(savitar_node.getTransformation())
|
||||
um_node.setTransformation(transformation)
|
||||
mesh_builder = MeshBuilder()
|
||||
@ -104,6 +109,10 @@ class ThreeMFReader(MeshReader):
|
||||
vertices = numpy.resize(data, (int(data.size / 3), 3))
|
||||
mesh_builder.setVertices(vertices)
|
||||
mesh_builder.calculateNormals(fast=True)
|
||||
if file_name:
|
||||
# The filename is used to give the user the option to reload the file if it is changed on disk
|
||||
# It is only set for the root node of the 3mf file
|
||||
mesh_builder.setFileName(file_name)
|
||||
mesh_data = mesh_builder.build()
|
||||
|
||||
if len(mesh_data.getVertices()):
|
||||
@ -162,7 +171,6 @@ class ThreeMFReader(MeshReader):
|
||||
|
||||
def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]:
|
||||
result = []
|
||||
self._object_count = 0 # Used to name objects as there is no node name yet.
|
||||
# The base object of 3mf is a zipped archive.
|
||||
try:
|
||||
archive = zipfile.ZipFile(file_name, "r")
|
||||
@ -171,7 +179,7 @@ class ThreeMFReader(MeshReader):
|
||||
scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read())
|
||||
self._unit = scene_3mf.getUnit()
|
||||
for node in scene_3mf.getSceneNodes():
|
||||
um_node = self._convertSavitarNodeToUMNode(node)
|
||||
um_node = self._convertSavitarNodeToUMNode(node, file_name)
|
||||
if um_node is None:
|
||||
continue
|
||||
# compensate for original center position, if object(s) is/are not around its zero position
|
||||
|
@ -4,7 +4,8 @@
|
||||
from configparser import ConfigParser
|
||||
import zipfile
|
||||
import os
|
||||
from typing import cast, Dict, List, Optional, Tuple
|
||||
import json
|
||||
from typing import cast, Dict, List, Optional, Tuple, Any
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
@ -284,13 +285,13 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
serialized = archive.open(instance_container_file_name).read().decode("utf-8")
|
||||
|
||||
# Qualities and variants don't have upgrades, so don't upgrade them
|
||||
parser = ConfigParser(interpolation = None)
|
||||
parser = ConfigParser(interpolation = None, comment_prefixes = ())
|
||||
parser.read_string(serialized)
|
||||
container_type = parser["metadata"]["type"]
|
||||
if container_type not in ("quality", "variant"):
|
||||
serialized = InstanceContainer._updateSerialized(serialized, instance_container_file_name)
|
||||
|
||||
parser = ConfigParser(interpolation = None)
|
||||
parser = ConfigParser(interpolation = None, comment_prefixes = ())
|
||||
parser.read_string(serialized)
|
||||
container_info = ContainerInfo(instance_container_file_name, serialized, parser)
|
||||
instance_container_info_dict[container_id] = container_info
|
||||
@ -732,7 +733,25 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
|
||||
base_file_name = os.path.basename(file_name)
|
||||
self.setWorkspaceName(base_file_name)
|
||||
return nodes
|
||||
|
||||
return nodes, self._loadMetadata(file_name)
|
||||
|
||||
@staticmethod
|
||||
def _loadMetadata(file_name: str) -> Dict[str, Dict[str, Any]]:
|
||||
archive = zipfile.ZipFile(file_name, "r")
|
||||
|
||||
metadata_files = [name for name in archive.namelist() if name.endswith("plugin_metadata.json")]
|
||||
|
||||
result = dict()
|
||||
|
||||
for metadata_file in metadata_files:
|
||||
try:
|
||||
plugin_id = metadata_file.split("/")[0]
|
||||
result[plugin_id] = json.loads(archive.open("%s/plugin_metadata.json" % plugin_id).read().decode("utf-8"))
|
||||
except Exception:
|
||||
Logger.logException("w", "Unable to retrieve metadata for %s", metadata_file)
|
||||
|
||||
return result
|
||||
|
||||
def _processQualityChanges(self, global_stack):
|
||||
if self._machine_info.quality_changes_info is None:
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for reading 3MF files.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import configparser
|
||||
@ -6,9 +6,12 @@ from io import StringIO
|
||||
import zipfile
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Workspace.WorkspaceWriter import WorkspaceWriter
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
from cura.Utils.Threading import call_on_qt_thread
|
||||
|
||||
@ -25,6 +28,8 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||
mesh_writer = application.getMeshFileHandler().getWriter("3MFWriter")
|
||||
|
||||
if not mesh_writer: # We need to have the 3mf mesh writer, otherwise we can't save the entire workspace
|
||||
self.setInformation(catalog.i18nc("@error:zip", "3MF Writer plug-in is corrupt."))
|
||||
Logger.error("3MF Writer class is unavailable. Can't write workspace.")
|
||||
return False
|
||||
|
||||
# Indicate that the 3mf mesh writer should not close the archive just yet (we still need to add stuff to it).
|
||||
@ -37,19 +42,24 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||
|
||||
global_stack = machine_manager.activeMachine
|
||||
|
||||
# Add global container stack data to the archive.
|
||||
self._writeContainerToArchive(global_stack, archive)
|
||||
try:
|
||||
# Add global container stack data to the archive.
|
||||
self._writeContainerToArchive(global_stack, archive)
|
||||
|
||||
# Also write all containers in the stack to the file
|
||||
for container in global_stack.getContainers():
|
||||
self._writeContainerToArchive(container, archive)
|
||||
|
||||
# Check if the machine has extruders and save all that data as well.
|
||||
for extruder_stack in global_stack.extruders.values():
|
||||
self._writeContainerToArchive(extruder_stack, archive)
|
||||
for container in extruder_stack.getContainers():
|
||||
# Also write all containers in the stack to the file
|
||||
for container in global_stack.getContainers():
|
||||
self._writeContainerToArchive(container, archive)
|
||||
|
||||
# Check if the machine has extruders and save all that data as well.
|
||||
for extruder_stack in global_stack.extruders.values():
|
||||
self._writeContainerToArchive(extruder_stack, archive)
|
||||
for container in extruder_stack.getContainers():
|
||||
self._writeContainerToArchive(container, archive)
|
||||
except PermissionError:
|
||||
self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
|
||||
Logger.error("No permission to write workspace to this stream.")
|
||||
return False
|
||||
|
||||
# Write preferences to archive
|
||||
original_preferences = Application.getInstance().getPreferences() #Copy only the preferences that we use to the workspace.
|
||||
temp_preferences = Preferences()
|
||||
@ -59,25 +69,44 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
||||
preferences_string = StringIO()
|
||||
temp_preferences.writeToFile(preferences_string)
|
||||
preferences_file = zipfile.ZipInfo("Cura/preferences.cfg")
|
||||
archive.writestr(preferences_file, preferences_string.getvalue())
|
||||
try:
|
||||
archive.writestr(preferences_file, preferences_string.getvalue())
|
||||
|
||||
# Save Cura version
|
||||
version_file = zipfile.ZipInfo("Cura/version.ini")
|
||||
version_config_parser = configparser.ConfigParser(interpolation = None)
|
||||
version_config_parser.add_section("versions")
|
||||
version_config_parser.set("versions", "cura_version", application.getVersion())
|
||||
version_config_parser.set("versions", "build_type", application.getBuildType())
|
||||
version_config_parser.set("versions", "is_debug_mode", str(application.getIsDebugMode()))
|
||||
# Save Cura version
|
||||
version_file = zipfile.ZipInfo("Cura/version.ini")
|
||||
version_config_parser = configparser.ConfigParser(interpolation = None)
|
||||
version_config_parser.add_section("versions")
|
||||
version_config_parser.set("versions", "cura_version", application.getVersion())
|
||||
version_config_parser.set("versions", "build_type", application.getBuildType())
|
||||
version_config_parser.set("versions", "is_debug_mode", str(application.getIsDebugMode()))
|
||||
|
||||
version_file_string = StringIO()
|
||||
version_config_parser.write(version_file_string)
|
||||
archive.writestr(version_file, version_file_string.getvalue())
|
||||
version_file_string = StringIO()
|
||||
version_config_parser.write(version_file_string)
|
||||
archive.writestr(version_file, version_file_string.getvalue())
|
||||
|
||||
# Close the archive & reset states.
|
||||
archive.close()
|
||||
self._writePluginMetadataToArchive(archive)
|
||||
|
||||
# Close the archive & reset states.
|
||||
archive.close()
|
||||
except PermissionError:
|
||||
self.setInformation(catalog.i18nc("@error:zip", "No permission to write the workspace here."))
|
||||
Logger.error("No permission to write workspace to this stream.")
|
||||
return False
|
||||
mesh_writer.setStoreArchive(False)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _writePluginMetadataToArchive(archive: zipfile.ZipFile) -> None:
|
||||
file_name_template = "%s/plugin_metadata.json"
|
||||
|
||||
for plugin_id, metadata in Application.getInstance().getWorkspaceMetadataStorage().getAllData().items():
|
||||
file_name = file_name_template % plugin_id
|
||||
file_in_archive = zipfile.ZipInfo(file_name)
|
||||
# We have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
|
||||
file_in_archive.compress_type = zipfile.ZIP_DEFLATED
|
||||
import json
|
||||
archive.writestr(file_in_archive, json.dumps(metadata, separators = (", ", ": "), indent = 4, skipkeys = True))
|
||||
|
||||
## Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive.
|
||||
# \param container That follows the \type{ContainerInterface} to archive.
|
||||
# \param archive The archive to write to.
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional
|
||||
|
||||
from UM.Mesh.MeshWriter import MeshWriter
|
||||
from UM.Math.Vector import Vector
|
||||
@ -40,7 +41,7 @@ class ThreeMFWriter(MeshWriter):
|
||||
}
|
||||
|
||||
self._unit_matrix_string = self._convertMatrixToString(Matrix())
|
||||
self._archive = None
|
||||
self._archive = None # type: Optional[zipfile.ZipFile]
|
||||
self._store_archive = False
|
||||
|
||||
def _convertMatrixToString(self, matrix):
|
||||
@ -76,6 +77,7 @@ class ThreeMFWriter(MeshWriter):
|
||||
return
|
||||
|
||||
savitar_node = Savitar.SceneNode()
|
||||
savitar_node.setName(um_node.getName())
|
||||
|
||||
node_matrix = um_node.getLocalTransformation()
|
||||
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for writing 3MF files.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -118,7 +118,7 @@ class AMFReader(MeshReader):
|
||||
mesh.merge_vertices()
|
||||
mesh.remove_unreferenced_vertices()
|
||||
mesh.fix_normals()
|
||||
mesh_data = self._toMeshData(mesh)
|
||||
mesh_data = self._toMeshData(mesh, file_name)
|
||||
|
||||
new_node = CuraSceneNode()
|
||||
new_node.setSelectable(True)
|
||||
@ -147,7 +147,13 @@ class AMFReader(MeshReader):
|
||||
|
||||
return group_node
|
||||
|
||||
def _toMeshData(self, tri_node: trimesh.base.Trimesh) -> MeshData:
|
||||
## Converts a Trimesh to Uranium's MeshData.
|
||||
# \param tri_node A Trimesh containing the contents of a file that was
|
||||
# just read.
|
||||
# \param file_name The full original filename used to watch for changes
|
||||
# \return Mesh data from the Trimesh in a way that Uranium can understand
|
||||
# it.
|
||||
def _toMeshData(self, tri_node: trimesh.base.Trimesh, file_name: str = "") -> MeshData:
|
||||
tri_faces = tri_node.faces
|
||||
tri_vertices = tri_node.vertices
|
||||
|
||||
@ -169,5 +175,5 @@ class AMFReader(MeshReader):
|
||||
indices = numpy.asarray(indices, dtype = numpy.int32)
|
||||
normals = calculateNormalsFromIndexedVertices(vertices, indices, face_count)
|
||||
|
||||
mesh_data = MeshData(vertices = vertices, indices = indices, normals = normals)
|
||||
mesh_data = MeshData(vertices = vertices, indices = indices, normals = normals,file_name = file_name)
|
||||
return mesh_data
|
||||
|
@ -3,5 +3,5 @@
|
||||
"author": "fieldOfView",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for reading AMF files.",
|
||||
"api": "7.0.0"
|
||||
"api": "7.1.0"
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"description": "Backup and restore your configuration.",
|
||||
"version": "1.2.0",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
134
plugins/CuraDrive/src/CreateBackupJob.py
Normal file
134
plugins/CuraDrive/src/CreateBackupJob.py
Normal file
@ -0,0 +1,134 @@
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import json
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import sentry_sdk
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
|
||||
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
|
||||
from UM.i18n import i18nCatalog
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class CreateBackupJob(Job):
|
||||
"""Creates backup zip, requests upload url and uploads the backup file to cloud storage."""
|
||||
|
||||
MESSAGE_TITLE = catalog.i18nc("@info:title", "Backups")
|
||||
DEFAULT_UPLOAD_ERROR_MESSAGE = catalog.i18nc("@info:backup_status", "There was an error while uploading your backup.")
|
||||
|
||||
def __init__(self, api_backup_url: str) -> None:
|
||||
""" Create a new backup Job. start the job by calling start()
|
||||
|
||||
:param api_backup_url: The url of the 'backups' endpoint of the Cura Drive Api
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
|
||||
self._api_backup_url = api_backup_url
|
||||
self._json_cloud_scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance()))
|
||||
|
||||
self._backup_zip = None # type: Optional[bytes]
|
||||
self._job_done = threading.Event()
|
||||
"""Set when the job completes. Does not indicate success."""
|
||||
self.backup_upload_error_message = ""
|
||||
"""After the job completes, an empty string indicates success. Othrerwise, the value is a translated message."""
|
||||
|
||||
def run(self) -> None:
|
||||
upload_message = Message(catalog.i18nc("@info:backup_status", "Creating your backup..."), title = self.MESSAGE_TITLE, progress = -1)
|
||||
upload_message.show()
|
||||
CuraApplication.getInstance().processEvents()
|
||||
cura_api = CuraApplication.getInstance().getCuraAPI()
|
||||
self._backup_zip, backup_meta_data = cura_api.backups.createBackup()
|
||||
|
||||
if not self._backup_zip or not backup_meta_data:
|
||||
self.backup_upload_error_message = catalog.i18nc("@info:backup_status", "There was an error while creating your backup.")
|
||||
upload_message.hide()
|
||||
return
|
||||
|
||||
upload_message.setText(catalog.i18nc("@info:backup_status", "Uploading your backup..."))
|
||||
CuraApplication.getInstance().processEvents()
|
||||
|
||||
# Create an upload entry for the backup.
|
||||
timestamp = datetime.now().isoformat()
|
||||
backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"])
|
||||
self._requestUploadSlot(backup_meta_data, len(self._backup_zip))
|
||||
|
||||
self._job_done.wait()
|
||||
if self.backup_upload_error_message == "":
|
||||
upload_message.setText(catalog.i18nc("@info:backup_status", "Your backup has finished uploading."))
|
||||
upload_message.setProgress(None) # Hide progress bar
|
||||
else:
|
||||
# some error occurred. This error is presented to the user by DrivePluginExtension
|
||||
upload_message.hide()
|
||||
|
||||
def _requestUploadSlot(self, backup_metadata: Dict[str, Any], backup_size: int) -> None:
|
||||
"""Request a backup upload slot from the API.
|
||||
|
||||
:param backup_metadata: A dict containing some meta data about the backup.
|
||||
:param backup_size: The size of the backup file in bytes.
|
||||
"""
|
||||
|
||||
payload = json.dumps({"data": {"backup_size": backup_size,
|
||||
"metadata": backup_metadata
|
||||
}
|
||||
}).encode()
|
||||
|
||||
HttpRequestManager.getInstance().put(
|
||||
self._api_backup_url,
|
||||
data = payload,
|
||||
callback = self._onUploadSlotCompleted,
|
||||
error_callback = self._onUploadSlotCompleted,
|
||||
scope = self._json_cloud_scope)
|
||||
|
||||
def _onUploadSlotCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
|
||||
if HttpRequestManager.safeHttpStatus(reply) >= 300:
|
||||
replyText = HttpRequestManager.readText(reply)
|
||||
Logger.warning("Could not request backup upload: %s", replyText)
|
||||
self.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE
|
||||
|
||||
if HttpRequestManager.safeHttpStatus(reply) == 400:
|
||||
errors = json.loads(replyText)["errors"]
|
||||
if "moreThanMaximum" in [error["code"] for error in errors if error["meta"] and error["meta"]["field_name"] == "backup_size"]:
|
||||
if self._backup_zip is None: # will never happen; keep mypy happy
|
||||
zip_error = "backup is None."
|
||||
else:
|
||||
zip_error = "{} exceeds max size.".format(str(len(self._backup_zip)))
|
||||
sentry_sdk.capture_message("backup failed: {}".format(zip_error), level ="warning")
|
||||
self.backup_upload_error_message = catalog.i18nc("@error:file_size", "The backup exceeds the maximum file size.")
|
||||
from sentry_sdk import capture_message
|
||||
|
||||
self._job_done.set()
|
||||
return
|
||||
|
||||
if error is not None:
|
||||
Logger.warning("Could not request backup upload: %s", HttpRequestManager.qt_network_error_name(error))
|
||||
self.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE
|
||||
self._job_done.set()
|
||||
return
|
||||
|
||||
backup_upload_url = HttpRequestManager.readJSON(reply)["data"]["upload_url"]
|
||||
|
||||
# Upload the backup to storage.
|
||||
HttpRequestManager.getInstance().put(
|
||||
backup_upload_url,
|
||||
data=self._backup_zip,
|
||||
callback=self._uploadFinishedCallback,
|
||||
error_callback=self._uploadFinishedCallback
|
||||
)
|
||||
|
||||
def _uploadFinishedCallback(self, reply: QNetworkReply, error: QNetworkReply.NetworkError = None):
|
||||
if not HttpRequestManager.replyIndicatesSuccess(reply, error):
|
||||
Logger.log("w", "Could not upload backup file: %s", HttpRequestManager.readText(reply))
|
||||
self.backup_upload_error_message = self.DEFAULT_UPLOAD_ERROR_MESSAGE
|
||||
|
||||
self._job_done.set()
|
@ -1,90 +1,70 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import Any, Optional, List, Dict
|
||||
from typing import Any, Optional, List, Dict, Callable
|
||||
|
||||
import requests
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Signal import Signal, signalemitter
|
||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
|
||||
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
|
||||
from UM.i18n import i18nCatalog
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from .UploadBackupJob import UploadBackupJob
|
||||
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
|
||||
from .CreateBackupJob import CreateBackupJob
|
||||
from .RestoreBackupJob import RestoreBackupJob
|
||||
from .Settings import Settings
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling.
|
||||
@signalemitter
|
||||
class DriveApiService:
|
||||
"""The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling."""
|
||||
|
||||
BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL)
|
||||
|
||||
# Emit signal when restoring backup started or finished.
|
||||
restoringStateChanged = Signal()
|
||||
"""Emits signal when restoring backup started or finished."""
|
||||
|
||||
# Emit signal when creating backup started or finished.
|
||||
creatingStateChanged = Signal()
|
||||
"""Emits signal when creating backup started or finished."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._cura_api = CuraApplication.getInstance().getCuraAPI()
|
||||
self._json_cloud_scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance()))
|
||||
|
||||
def getBackups(self) -> List[Dict[str, Any]]:
|
||||
access_token = self._cura_api.account.accessToken
|
||||
if not access_token:
|
||||
Logger.log("w", "Could not get access token.")
|
||||
return []
|
||||
try:
|
||||
backup_list_request = requests.get(self.BACKUP_URL, headers = {
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
except requests.exceptions.ConnectionError:
|
||||
Logger.logException("w", "Unable to connect with the server.")
|
||||
return []
|
||||
def getBackups(self, changed: Callable[[List[Dict[str, Any]]], None]) -> None:
|
||||
def callback(reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
|
||||
if error is not None:
|
||||
Logger.log("w", "Could not get backups: " + str(error))
|
||||
changed([])
|
||||
return
|
||||
|
||||
# HTTP status 300s mean redirection. 400s and 500s are errors.
|
||||
# Technically 300s are not errors, but the use case here relies on "requests" to handle redirects automatically.
|
||||
if backup_list_request.status_code >= 300:
|
||||
Logger.log("w", "Could not get backups list from remote: %s", backup_list_request.text)
|
||||
Message(catalog.i18nc("@info:backup_status", "There was an error listing your backups."), title = catalog.i18nc("@info:title", "Backup")).show()
|
||||
return []
|
||||
backup_list_response = HttpRequestManager.readJSON(reply)
|
||||
if "data" not in backup_list_response:
|
||||
Logger.log("w", "Could not get backups from remote, actual response body was: %s",
|
||||
str(backup_list_response))
|
||||
changed([]) # empty list of backups
|
||||
return
|
||||
|
||||
backup_list_response = backup_list_request.json()
|
||||
if "data" not in backup_list_response:
|
||||
Logger.log("w", "Could not get backups from remote, actual response body was: %s", str(backup_list_response))
|
||||
return []
|
||||
changed(backup_list_response["data"])
|
||||
|
||||
return backup_list_response["data"]
|
||||
HttpRequestManager.getInstance().get(
|
||||
self.BACKUP_URL,
|
||||
callback= callback,
|
||||
error_callback = callback,
|
||||
scope=self._json_cloud_scope
|
||||
)
|
||||
|
||||
def createBackup(self) -> None:
|
||||
self.creatingStateChanged.emit(is_creating = True)
|
||||
|
||||
# Create the backup.
|
||||
backup_zip_file, backup_meta_data = self._cura_api.backups.createBackup()
|
||||
if not backup_zip_file or not backup_meta_data:
|
||||
self.creatingStateChanged.emit(is_creating = False, error_message ="Could not create backup.")
|
||||
return
|
||||
|
||||
# Create an upload entry for the backup.
|
||||
timestamp = datetime.now().isoformat()
|
||||
backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"])
|
||||
backup_upload_url = self._requestBackupUpload(backup_meta_data, len(backup_zip_file))
|
||||
if not backup_upload_url:
|
||||
self.creatingStateChanged.emit(is_creating = False, error_message ="Could not upload backup.")
|
||||
return
|
||||
|
||||
# Upload the backup to storage.
|
||||
upload_backup_job = UploadBackupJob(backup_upload_url, backup_zip_file)
|
||||
upload_backup_job = CreateBackupJob(self.BACKUP_URL)
|
||||
upload_backup_job.finished.connect(self._onUploadFinished)
|
||||
upload_backup_job.start()
|
||||
|
||||
def _onUploadFinished(self, job: "UploadBackupJob") -> None:
|
||||
def _onUploadFinished(self, job: "CreateBackupJob") -> None:
|
||||
if job.backup_upload_error_message != "":
|
||||
# If the job contains an error message we pass it along so the UI can display it.
|
||||
self.creatingStateChanged.emit(is_creating = False, error_message = job.backup_upload_error_message)
|
||||
@ -96,96 +76,38 @@ class DriveApiService:
|
||||
download_url = backup.get("download_url")
|
||||
if not download_url:
|
||||
# If there is no download URL, we can't restore the backup.
|
||||
return self._emitRestoreError()
|
||||
Logger.warning("backup download_url is missing. Aborting backup.")
|
||||
self.restoringStateChanged.emit(is_restoring = False,
|
||||
error_message = catalog.i18nc("@info:backup_status",
|
||||
"There was an error trying to restore your backup."))
|
||||
return
|
||||
|
||||
try:
|
||||
download_package = requests.get(download_url, stream = True)
|
||||
except requests.exceptions.ConnectionError:
|
||||
Logger.logException("e", "Unable to connect with the server")
|
||||
return self._emitRestoreError()
|
||||
restore_backup_job = RestoreBackupJob(backup)
|
||||
restore_backup_job.finished.connect(self._onRestoreFinished)
|
||||
restore_backup_job.start()
|
||||
|
||||
if download_package.status_code >= 300:
|
||||
# Something went wrong when attempting to download the backup.
|
||||
Logger.log("w", "Could not download backup from url %s: %s", download_url, download_package.text)
|
||||
return self._emitRestoreError()
|
||||
def _onRestoreFinished(self, job: "RestoreBackupJob") -> None:
|
||||
if job.restore_backup_error_message != "":
|
||||
# If the job contains an error message we pass it along so the UI can display it.
|
||||
self.restoringStateChanged.emit(is_restoring=False)
|
||||
else:
|
||||
self.restoringStateChanged.emit(is_restoring = False, error_message = job.restore_backup_error_message)
|
||||
|
||||
# We store the file in a temporary path fist to ensure integrity.
|
||||
temporary_backup_file = NamedTemporaryFile(delete = False)
|
||||
with open(temporary_backup_file.name, "wb") as write_backup:
|
||||
for chunk in download_package:
|
||||
write_backup.write(chunk)
|
||||
def deleteBackup(self, backup_id: str, finished_callable: Callable[[bool], None]):
|
||||
|
||||
if not self._verifyMd5Hash(temporary_backup_file.name, backup.get("md5_hash", "")):
|
||||
# Don't restore the backup if the MD5 hashes do not match.
|
||||
# This can happen if the download was interrupted.
|
||||
Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.")
|
||||
return self._emitRestoreError()
|
||||
def finishedCallback(reply: QNetworkReply, ca: Callable[[bool], None] = finished_callable) -> None:
|
||||
self._onDeleteRequestCompleted(reply, ca)
|
||||
|
||||
# Tell Cura to place the backup back in the user data folder.
|
||||
with open(temporary_backup_file.name, "rb") as read_backup:
|
||||
self._cura_api.backups.restoreBackup(read_backup.read(), backup.get("metadata", {}))
|
||||
self.restoringStateChanged.emit(is_restoring = False)
|
||||
def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, ca: Callable[[bool], None] = finished_callable) -> None:
|
||||
self._onDeleteRequestCompleted(reply, ca, error)
|
||||
|
||||
def _emitRestoreError(self) -> None:
|
||||
self.restoringStateChanged.emit(is_restoring = False,
|
||||
error_message = catalog.i18nc("@info:backup_status",
|
||||
"There was an error trying to restore your backup."))
|
||||
HttpRequestManager.getInstance().delete(
|
||||
url = "{}/{}".format(self.BACKUP_URL, backup_id),
|
||||
callback = finishedCallback,
|
||||
error_callback = errorCallback,
|
||||
scope= self._json_cloud_scope
|
||||
)
|
||||
|
||||
# Verify the MD5 hash of a file.
|
||||
# \param file_path Full path to the file.
|
||||
# \param known_hash The known MD5 hash of the file.
|
||||
# \return: Success or not.
|
||||
@staticmethod
|
||||
def _verifyMd5Hash(file_path: str, known_hash: str) -> bool:
|
||||
with open(file_path, "rb") as read_backup:
|
||||
local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8")
|
||||
return known_hash == local_md5_hash
|
||||
|
||||
def deleteBackup(self, backup_id: str) -> bool:
|
||||
access_token = self._cura_api.account.accessToken
|
||||
if not access_token:
|
||||
Logger.log("w", "Could not get access token.")
|
||||
return False
|
||||
|
||||
try:
|
||||
delete_backup = requests.delete("{}/{}".format(self.BACKUP_URL, backup_id), headers = {
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
except requests.exceptions.ConnectionError:
|
||||
Logger.logException("e", "Unable to connect with the server")
|
||||
return False
|
||||
|
||||
if delete_backup.status_code >= 300:
|
||||
Logger.log("w", "Could not delete backup: %s", delete_backup.text)
|
||||
return False
|
||||
return True
|
||||
|
||||
# Request a backup upload slot from the API.
|
||||
# \param backup_metadata: A dict containing some meta data about the backup.
|
||||
# \param backup_size The size of the backup file in bytes.
|
||||
# \return: The upload URL for the actual backup file if successful, otherwise None.
|
||||
def _requestBackupUpload(self, backup_metadata: Dict[str, Any], backup_size: int) -> Optional[str]:
|
||||
access_token = self._cura_api.account.accessToken
|
||||
if not access_token:
|
||||
Logger.log("w", "Could not get access token.")
|
||||
return None
|
||||
try:
|
||||
backup_upload_request = requests.put(
|
||||
self.BACKUP_URL,
|
||||
json = {"data": {"backup_size": backup_size,
|
||||
"metadata": backup_metadata
|
||||
}
|
||||
},
|
||||
headers = {
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
except requests.exceptions.ConnectionError:
|
||||
Logger.logException("e", "Unable to connect with the server")
|
||||
return None
|
||||
|
||||
# Any status code of 300 or above indicates an error.
|
||||
if backup_upload_request.status_code >= 300:
|
||||
Logger.log("w", "Could not request backup upload: %s", backup_upload_request.text)
|
||||
return None
|
||||
|
||||
return backup_upload_request.json()["data"]["upload_url"]
|
||||
def _onDeleteRequestCompleted(reply: QNetworkReply, callable: Callable[[bool], None], error: Optional["QNetworkReply.NetworkError"] = None) -> None:
|
||||
callable(HttpRequestManager.replyIndicatesSuccess(reply, error))
|
||||
|
@ -133,7 +133,10 @@ class DrivePluginExtension(QObject, Extension):
|
||||
|
||||
@pyqtSlot(name = "refreshBackups")
|
||||
def refreshBackups(self) -> None:
|
||||
self._backups = self._drive_api_service.getBackups()
|
||||
self._drive_api_service.getBackups(self._backupsChangedCallback)
|
||||
|
||||
def _backupsChangedCallback(self, backups: List[Dict[str, Any]]) -> None:
|
||||
self._backups = backups
|
||||
self.backupsChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, notify = restoringStateChanged)
|
||||
@ -158,5 +161,8 @@ class DrivePluginExtension(QObject, Extension):
|
||||
|
||||
@pyqtSlot(str, name = "deleteBackup")
|
||||
def deleteBackup(self, backup_id: str) -> None:
|
||||
self._drive_api_service.deleteBackup(backup_id)
|
||||
self.refreshBackups()
|
||||
self._drive_api_service.deleteBackup(backup_id, self._backupDeletedCallback)
|
||||
|
||||
def _backupDeletedCallback(self, success: bool):
|
||||
if success:
|
||||
self.refreshBackups()
|
||||
|
92
plugins/CuraDrive/src/RestoreBackupJob.py
Normal file
92
plugins/CuraDrive/src/RestoreBackupJob.py
Normal file
@ -0,0 +1,92 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import threading
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import Optional, Any, Dict
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from UM.PackageManager import catalog
|
||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
class RestoreBackupJob(Job):
|
||||
"""Downloads a backup and overwrites local configuration with the backup.
|
||||
|
||||
When `Job.finished` emits, `restore_backup_error_message` will either be `""` (no error) or an error message
|
||||
"""
|
||||
|
||||
DISK_WRITE_BUFFER_SIZE = 512 * 1024
|
||||
DEFAULT_ERROR_MESSAGE = catalog.i18nc("@info:backup_status", "There was an error trying to restore your backup.")
|
||||
|
||||
def __init__(self, backup: Dict[str, Any]) -> None:
|
||||
""" Create a new restore Job. start the job by calling start()
|
||||
|
||||
:param backup: A dict containing a backup spec
|
||||
"""
|
||||
|
||||
super().__init__()
|
||||
self._job_done = threading.Event()
|
||||
|
||||
self._backup = backup
|
||||
self.restore_backup_error_message = ""
|
||||
|
||||
def run(self) -> None:
|
||||
|
||||
url = self._backup.get("download_url")
|
||||
assert url is not None
|
||||
|
||||
HttpRequestManager.getInstance().get(
|
||||
url = url,
|
||||
callback = self._onRestoreRequestCompleted,
|
||||
error_callback = self._onRestoreRequestCompleted
|
||||
)
|
||||
|
||||
self._job_done.wait() # A job is considered finished when the run function completes
|
||||
|
||||
def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
|
||||
if not HttpRequestManager.replyIndicatesSuccess(reply, error):
|
||||
Logger.warning("Requesting backup failed, response code %s while trying to connect to %s",
|
||||
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url())
|
||||
self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE
|
||||
self._job_done.set()
|
||||
return
|
||||
|
||||
# We store the file in a temporary path fist to ensure integrity.
|
||||
temporary_backup_file = NamedTemporaryFile(delete = False)
|
||||
with open(temporary_backup_file.name, "wb") as write_backup:
|
||||
app = CuraApplication.getInstance()
|
||||
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
|
||||
while bytes_read:
|
||||
write_backup.write(bytes_read)
|
||||
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
|
||||
app.processEvents()
|
||||
|
||||
if not self._verifyMd5Hash(temporary_backup_file.name, self._backup.get("md5_hash", "")):
|
||||
# Don't restore the backup if the MD5 hashes do not match.
|
||||
# This can happen if the download was interrupted.
|
||||
Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.")
|
||||
self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE
|
||||
|
||||
# Tell Cura to place the backup back in the user data folder.
|
||||
with open(temporary_backup_file.name, "rb") as read_backup:
|
||||
cura_api = CuraApplication.getInstance().getCuraAPI()
|
||||
cura_api.backups.restoreBackup(read_backup.read(), self._backup.get("metadata", {}))
|
||||
|
||||
self._job_done.set()
|
||||
|
||||
@staticmethod
|
||||
def _verifyMd5Hash(file_path: str, known_hash: str) -> bool:
|
||||
"""Verify the MD5 hash of a file.
|
||||
|
||||
:param file_path: Full path to the file.
|
||||
:param known_hash: The known MD5 hash of the file.
|
||||
:return: Success or not.
|
||||
"""
|
||||
|
||||
with open(file_path, "rb") as read_backup:
|
||||
local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8")
|
||||
return known_hash == local_md5_hash
|
@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from cura import UltimakerCloudAuthentication
|
||||
from cura.UltimakerCloud import UltimakerCloudAuthentication
|
||||
|
||||
|
||||
class Settings:
|
||||
|
@ -1,41 +0,0 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import requests
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class UploadBackupJob(Job):
|
||||
MESSAGE_TITLE = catalog.i18nc("@info:title", "Backups")
|
||||
|
||||
# This job is responsible for uploading the backup file to cloud storage.
|
||||
# As it can take longer than some other tasks, we schedule this using a Cura Job.
|
||||
def __init__(self, signed_upload_url: str, backup_zip: bytes) -> None:
|
||||
super().__init__()
|
||||
self._signed_upload_url = signed_upload_url
|
||||
self._backup_zip = backup_zip
|
||||
self._upload_success = False
|
||||
self.backup_upload_error_message = ""
|
||||
|
||||
def run(self) -> None:
|
||||
upload_message = Message(catalog.i18nc("@info:backup_status", "Uploading your backup..."), title = self.MESSAGE_TITLE, progress = -1)
|
||||
upload_message.show()
|
||||
|
||||
backup_upload = requests.put(self._signed_upload_url, data = self._backup_zip)
|
||||
upload_message.hide()
|
||||
|
||||
if backup_upload.status_code >= 300:
|
||||
self.backup_upload_error_message = backup_upload.text
|
||||
Logger.log("w", "Could not upload backup file: %s", backup_upload.text)
|
||||
Message(catalog.i18nc("@info:backup_status", "There was an error while uploading your backup."), title = self.MESSAGE_TITLE).show()
|
||||
else:
|
||||
self._upload_success = True
|
||||
Message(catalog.i18nc("@info:backup_status", "Your backup has finished uploading."), title = self.MESSAGE_TITLE).show()
|
||||
|
||||
self.finished.emit(self)
|
@ -55,10 +55,22 @@ class CuraEngineBackend(QObject, Backend):
|
||||
if Platform.isWindows():
|
||||
executable_name += ".exe"
|
||||
default_engine_location = executable_name
|
||||
if os.path.exists(os.path.join(CuraApplication.getInstallPrefix(), "bin", executable_name)):
|
||||
default_engine_location = os.path.join(CuraApplication.getInstallPrefix(), "bin", executable_name)
|
||||
if hasattr(sys, "frozen"):
|
||||
default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), executable_name)
|
||||
|
||||
search_path = [
|
||||
os.path.abspath(os.path.dirname(sys.executable)),
|
||||
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "bin")),
|
||||
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "..")),
|
||||
|
||||
os.path.join(CuraApplication.getInstallPrefix(), "bin"),
|
||||
os.path.dirname(os.path.abspath(sys.executable)),
|
||||
]
|
||||
|
||||
for path in search_path:
|
||||
engine_path = os.path.join(path, executable_name)
|
||||
if os.path.isfile(engine_path):
|
||||
default_engine_location = engine_path
|
||||
break
|
||||
|
||||
if Platform.isLinux() and not default_engine_location:
|
||||
if not os.getenv("PATH"):
|
||||
raise OSError("There is something wrong with your Linux installation.")
|
||||
@ -409,7 +421,10 @@ class CuraEngineBackend(QObject, Backend):
|
||||
|
||||
if job.getResult() == StartJobResult.NothingToSlice:
|
||||
if self._application.platformActivity:
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume or are assigned to a disabled extruder. Please scale or rotate models to fit, or enable an extruder."),
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Please review settings and check if your models:"
|
||||
"\n- Fit within the build volume"
|
||||
"\n- Are assigned to an enabled extruder"
|
||||
"\n- Are not all set as modifier meshes"),
|
||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||
self._error_message.show()
|
||||
self.setState(BackendState.Error)
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import numpy
|
||||
@ -171,146 +171,145 @@ class StartSliceJob(Job):
|
||||
self.setResult(StartJobResult.ObjectSettingError)
|
||||
return
|
||||
|
||||
with self._scene.getSceneLock():
|
||||
# Remove old layer data.
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
|
||||
# Singe we walk through all nodes in the scene, they always have a parent.
|
||||
cast(SceneNode, node.getParent()).removeChild(node)
|
||||
break
|
||||
# Remove old layer data.
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
|
||||
# Singe we walk through all nodes in the scene, they always have a parent.
|
||||
cast(SceneNode, node.getParent()).removeChild(node)
|
||||
break
|
||||
|
||||
# Get the objects in their groups to print.
|
||||
object_groups = []
|
||||
if stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
||||
for node in OneAtATimeIterator(self._scene.getRoot()):
|
||||
temp_list = []
|
||||
|
||||
# Node can't be printed, so don't bother sending it.
|
||||
if getattr(node, "_outside_buildarea", False):
|
||||
continue
|
||||
|
||||
# Filter on current build plate
|
||||
build_plate_number = node.callDecoration("getBuildPlateNumber")
|
||||
if build_plate_number is not None and build_plate_number != self._build_plate_number:
|
||||
continue
|
||||
|
||||
children = node.getAllChildren()
|
||||
children.append(node)
|
||||
for child_node in children:
|
||||
mesh_data = child_node.getMeshData()
|
||||
if mesh_data and mesh_data.getVertices() is not None:
|
||||
temp_list.append(child_node)
|
||||
|
||||
if temp_list:
|
||||
object_groups.append(temp_list)
|
||||
Job.yieldThread()
|
||||
if len(object_groups) == 0:
|
||||
Logger.log("w", "No objects suitable for one at a time found, or no correct order found")
|
||||
else:
|
||||
# Get the objects in their groups to print.
|
||||
object_groups = []
|
||||
if stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
||||
for node in OneAtATimeIterator(self._scene.getRoot()):
|
||||
temp_list = []
|
||||
has_printing_mesh = False
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
mesh_data = node.getMeshData()
|
||||
if node.callDecoration("isSliceable") and mesh_data and mesh_data.getVertices() is not None:
|
||||
is_non_printing_mesh = bool(node.callDecoration("isNonPrintingMesh"))
|
||||
|
||||
# Find a reason not to add the node
|
||||
if node.callDecoration("getBuildPlateNumber") != self._build_plate_number:
|
||||
continue
|
||||
if getattr(node, "_outside_buildarea", False) and not is_non_printing_mesh:
|
||||
continue
|
||||
# Node can't be printed, so don't bother sending it.
|
||||
if getattr(node, "_outside_buildarea", False):
|
||||
continue
|
||||
|
||||
temp_list.append(node)
|
||||
if not is_non_printing_mesh:
|
||||
has_printing_mesh = True
|
||||
# Filter on current build plate
|
||||
build_plate_number = node.callDecoration("getBuildPlateNumber")
|
||||
if build_plate_number is not None and build_plate_number != self._build_plate_number:
|
||||
continue
|
||||
|
||||
Job.yieldThread()
|
||||
|
||||
# If the list doesn't have any model with suitable settings then clean the list
|
||||
# otherwise CuraEngine will crash
|
||||
if not has_printing_mesh:
|
||||
temp_list.clear()
|
||||
children = node.getAllChildren()
|
||||
children.append(node)
|
||||
for child_node in children:
|
||||
mesh_data = child_node.getMeshData()
|
||||
if mesh_data and mesh_data.getVertices() is not None:
|
||||
temp_list.append(child_node)
|
||||
|
||||
if temp_list:
|
||||
object_groups.append(temp_list)
|
||||
Job.yieldThread()
|
||||
if len(object_groups) == 0:
|
||||
Logger.log("w", "No objects suitable for one at a time found, or no correct order found")
|
||||
else:
|
||||
temp_list = []
|
||||
has_printing_mesh = False
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
mesh_data = node.getMeshData()
|
||||
if node.callDecoration("isSliceable") and mesh_data and mesh_data.getVertices() is not None:
|
||||
is_non_printing_mesh = bool(node.callDecoration("isNonPrintingMesh"))
|
||||
|
||||
global_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return
|
||||
extruders_enabled = {position: stack.isEnabled for position, stack in global_stack.extruders.items()}
|
||||
filtered_object_groups = []
|
||||
has_model_with_disabled_extruders = False
|
||||
associated_disabled_extruders = set()
|
||||
for group in object_groups:
|
||||
stack = global_stack
|
||||
skip_group = False
|
||||
for node in group:
|
||||
# Only check if the printing extruder is enabled for printing meshes
|
||||
is_non_printing_mesh = node.callDecoration("evaluateIsNonPrintingMesh")
|
||||
extruder_position = node.callDecoration("getActiveExtruderPosition")
|
||||
if not is_non_printing_mesh and not extruders_enabled[extruder_position]:
|
||||
skip_group = True
|
||||
has_model_with_disabled_extruders = True
|
||||
associated_disabled_extruders.add(extruder_position)
|
||||
if not skip_group:
|
||||
filtered_object_groups.append(group)
|
||||
|
||||
if has_model_with_disabled_extruders:
|
||||
self.setResult(StartJobResult.ObjectsWithDisabledExtruder)
|
||||
associated_disabled_extruders = {str(c) for c in sorted([int(p) + 1 for p in associated_disabled_extruders])}
|
||||
self.setMessage(", ".join(associated_disabled_extruders))
|
||||
return
|
||||
|
||||
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
|
||||
# able to find a possible sequence or because there are no objects on the build plate (or they are outside
|
||||
# the build volume)
|
||||
if not filtered_object_groups:
|
||||
self.setResult(StartJobResult.NothingToSlice)
|
||||
return
|
||||
|
||||
self._buildGlobalSettingsMessage(stack)
|
||||
self._buildGlobalInheritsStackMessage(stack)
|
||||
|
||||
# Build messages for extruder stacks
|
||||
for extruder_stack in global_stack.extruderList:
|
||||
self._buildExtruderMessage(extruder_stack)
|
||||
|
||||
for group in filtered_object_groups:
|
||||
group_message = self._slice_message.addRepeatedMessage("object_lists")
|
||||
parent = group[0].getParent()
|
||||
if parent is not None and parent.callDecoration("isGroup"):
|
||||
self._handlePerObjectSettings(cast(CuraSceneNode, parent), group_message)
|
||||
|
||||
for object in group:
|
||||
mesh_data = object.getMeshData()
|
||||
if mesh_data is None:
|
||||
# Find a reason not to add the node
|
||||
if node.callDecoration("getBuildPlateNumber") != self._build_plate_number:
|
||||
continue
|
||||
if getattr(node, "_outside_buildarea", False) and not is_non_printing_mesh:
|
||||
continue
|
||||
rot_scale = object.getWorldTransformation().getTransposed().getData()[0:3, 0:3]
|
||||
translate = object.getWorldTransformation().getData()[:3, 3]
|
||||
|
||||
# This effectively performs a limited form of MeshData.getTransformed that ignores normals.
|
||||
verts = mesh_data.getVertices()
|
||||
verts = verts.dot(rot_scale)
|
||||
verts += translate
|
||||
temp_list.append(node)
|
||||
if not is_non_printing_mesh:
|
||||
has_printing_mesh = True
|
||||
|
||||
# Convert from Y up axes to Z up axes. Equals a 90 degree rotation.
|
||||
verts[:, [1, 2]] = verts[:, [2, 1]]
|
||||
verts[:, 1] *= -1
|
||||
Job.yieldThread()
|
||||
|
||||
obj = group_message.addRepeatedMessage("objects")
|
||||
obj.id = id(object)
|
||||
obj.name = object.getName()
|
||||
indices = mesh_data.getIndices()
|
||||
if indices is not None:
|
||||
flat_verts = numpy.take(verts, indices.flatten(), axis=0)
|
||||
else:
|
||||
flat_verts = numpy.array(verts)
|
||||
# If the list doesn't have any model with suitable settings then clean the list
|
||||
# otherwise CuraEngine will crash
|
||||
if not has_printing_mesh:
|
||||
temp_list.clear()
|
||||
|
||||
obj.vertices = flat_verts
|
||||
if temp_list:
|
||||
object_groups.append(temp_list)
|
||||
|
||||
self._handlePerObjectSettings(cast(CuraSceneNode, object), obj)
|
||||
global_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return
|
||||
extruders_enabled = {position: stack.isEnabled for position, stack in global_stack.extruders.items()}
|
||||
filtered_object_groups = []
|
||||
has_model_with_disabled_extruders = False
|
||||
associated_disabled_extruders = set()
|
||||
for group in object_groups:
|
||||
stack = global_stack
|
||||
skip_group = False
|
||||
for node in group:
|
||||
# Only check if the printing extruder is enabled for printing meshes
|
||||
is_non_printing_mesh = node.callDecoration("evaluateIsNonPrintingMesh")
|
||||
extruder_position = node.callDecoration("getActiveExtruderPosition")
|
||||
if not is_non_printing_mesh and not extruders_enabled[extruder_position]:
|
||||
skip_group = True
|
||||
has_model_with_disabled_extruders = True
|
||||
associated_disabled_extruders.add(extruder_position)
|
||||
if not skip_group:
|
||||
filtered_object_groups.append(group)
|
||||
|
||||
Job.yieldThread()
|
||||
if has_model_with_disabled_extruders:
|
||||
self.setResult(StartJobResult.ObjectsWithDisabledExtruder)
|
||||
associated_disabled_extruders = {str(c) for c in sorted([int(p) + 1 for p in associated_disabled_extruders])}
|
||||
self.setMessage(", ".join(associated_disabled_extruders))
|
||||
return
|
||||
|
||||
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
|
||||
# able to find a possible sequence or because there are no objects on the build plate (or they are outside
|
||||
# the build volume)
|
||||
if not filtered_object_groups:
|
||||
self.setResult(StartJobResult.NothingToSlice)
|
||||
return
|
||||
|
||||
self._buildGlobalSettingsMessage(stack)
|
||||
self._buildGlobalInheritsStackMessage(stack)
|
||||
|
||||
# Build messages for extruder stacks
|
||||
for extruder_stack in global_stack.extruderList:
|
||||
self._buildExtruderMessage(extruder_stack)
|
||||
|
||||
for group in filtered_object_groups:
|
||||
group_message = self._slice_message.addRepeatedMessage("object_lists")
|
||||
parent = group[0].getParent()
|
||||
if parent is not None and parent.callDecoration("isGroup"):
|
||||
self._handlePerObjectSettings(cast(CuraSceneNode, parent), group_message)
|
||||
|
||||
for object in group:
|
||||
mesh_data = object.getMeshData()
|
||||
if mesh_data is None:
|
||||
continue
|
||||
rot_scale = object.getWorldTransformation().getTransposed().getData()[0:3, 0:3]
|
||||
translate = object.getWorldTransformation().getData()[:3, 3]
|
||||
|
||||
# This effectively performs a limited form of MeshData.getTransformed that ignores normals.
|
||||
verts = mesh_data.getVertices()
|
||||
verts = verts.dot(rot_scale)
|
||||
verts += translate
|
||||
|
||||
# Convert from Y up axes to Z up axes. Equals a 90 degree rotation.
|
||||
verts[:, [1, 2]] = verts[:, [2, 1]]
|
||||
verts[:, 1] *= -1
|
||||
|
||||
obj = group_message.addRepeatedMessage("objects")
|
||||
obj.id = id(object)
|
||||
obj.name = object.getName()
|
||||
indices = mesh_data.getIndices()
|
||||
if indices is not None:
|
||||
flat_verts = numpy.take(verts, indices.flatten(), axis=0)
|
||||
else:
|
||||
flat_verts = numpy.array(verts)
|
||||
|
||||
obj.vertices = flat_verts
|
||||
|
||||
self._handlePerObjectSettings(cast(CuraSceneNode, object), obj)
|
||||
|
||||
Job.yieldThread()
|
||||
|
||||
self.setResult(StartJobResult.Finished)
|
||||
|
||||
@ -344,10 +343,7 @@ class StartSliceJob(Job):
|
||||
result["time"] = time.strftime("%H:%M:%S") #Some extra settings.
|
||||
result["date"] = time.strftime("%d-%m-%Y")
|
||||
result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
|
||||
|
||||
initial_extruder_stack = CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
|
||||
initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value")
|
||||
result["initial_extruder_nr"] = initial_extruder_nr
|
||||
result["initial_extruder_nr"] = CuraApplication.getInstance().getExtruderManager().getInitialExtruderNr()
|
||||
|
||||
return result
|
||||
|
||||
@ -426,13 +422,14 @@ class StartSliceJob(Job):
|
||||
|
||||
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
|
||||
start_gcode = settings["machine_start_gcode"]
|
||||
# Remove all the comments from the start g-code
|
||||
start_gcode = re.sub(r";.+?(\n|$)", "\n", start_gcode)
|
||||
bed_temperature_settings = ["material_bed_temperature", "material_bed_temperature_layer_0"]
|
||||
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(bed_temperature_settings) # match {setting} as well as {setting, extruder_nr}
|
||||
settings["material_bed_temp_prepend"] = re.search(pattern, start_gcode) == None
|
||||
print_temperature_settings = ["material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"]
|
||||
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) # match {setting} as well as {setting, extruder_nr}
|
||||
settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) == None
|
||||
|
||||
# Replace the setting tokens in start and end g-code.
|
||||
# Use values from the first used extruder by default so we get the expected temperatures
|
||||
initial_extruder_stack = CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
|
||||
|
@ -2,7 +2,7 @@
|
||||
"name": "CuraEngine Backend",
|
||||
"author": "Ultimaker B.V.",
|
||||
"description": "Provides the link to the CuraEngine slicing backend.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"version": "1.0.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for importing Cura profiles.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for exporting Cura profiles.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog":"cura"
|
||||
}
|
||||
|
@ -44,6 +44,7 @@ class FirmwareUpdateCheckerJob(Job):
|
||||
try:
|
||||
# CURA-6698 Create an SSL context and use certifi CA certificates for verification.
|
||||
context = ssl.SSLContext(protocol = ssl.PROTOCOL_TLSv1_2)
|
||||
context.verify_mode = ssl.CERT_REQUIRED
|
||||
context.load_verify_locations(cafile = certifi.where())
|
||||
|
||||
request = urllib.request.Request(url, headers = self._headers)
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Checks for firmware updates.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a machine actions for updating firmware.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Reads g-code from a compressed archive.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Writes g-code to a compressed archive.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for importing profiles from g-code files.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import math
|
||||
@ -169,6 +169,9 @@ class FlavorParser:
|
||||
# A threshold is set to avoid weird paths in the GCode
|
||||
if line_width > 1.2:
|
||||
return 0.35
|
||||
# Prevent showing infinitely wide lines
|
||||
if line_width < 0.0:
|
||||
return 0.0
|
||||
return line_width
|
||||
|
||||
def _gCode0(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
|
||||
@ -235,7 +238,7 @@ class FlavorParser:
|
||||
def _gCode92(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
|
||||
if params.e is not None:
|
||||
# Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
|
||||
self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e
|
||||
self._extrusion_length_offset[self._extruder_number] = position.e[self._extruder_number] - params.e
|
||||
position.e[self._extruder_number] = params.e
|
||||
self._previous_extrusion_value = params.e
|
||||
else:
|
||||
@ -258,16 +261,19 @@ class FlavorParser:
|
||||
continue
|
||||
if item.startswith(";"):
|
||||
continue
|
||||
if item[0] == "X":
|
||||
x = float(item[1:])
|
||||
if item[0] == "Y":
|
||||
y = float(item[1:])
|
||||
if item[0] == "Z":
|
||||
z = float(item[1:])
|
||||
if item[0] == "F":
|
||||
f = float(item[1:]) / 60
|
||||
if item[0] == "E":
|
||||
e = float(item[1:])
|
||||
try:
|
||||
if item[0] == "X":
|
||||
x = float(item[1:])
|
||||
elif item[0] == "Y":
|
||||
y = float(item[1:])
|
||||
elif item[0] == "Z":
|
||||
z = float(item[1:])
|
||||
elif item[0] == "F":
|
||||
f = float(item[1:]) / 60
|
||||
elif item[0] == "E":
|
||||
e = float(item[1:])
|
||||
except ValueError: # Improperly formatted g-code: Coordinates are not floats.
|
||||
continue # Skip the command then.
|
||||
params = PositionOptional(x, y, z, f, e)
|
||||
return func(position, params, path)
|
||||
return position
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "G-code Reader",
|
||||
"author": "Victor Larchenko, Ultimaker",
|
||||
"author": "Victor Larchenko, Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Allows loading and displaying G-code files.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Writes g-code to a file.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import numpy
|
||||
@ -96,7 +96,7 @@ class ImageReader(MeshReader):
|
||||
texel_width = 1.0 / (width_minus_one) * scale_vector.x
|
||||
texel_height = 1.0 / (height_minus_one) * scale_vector.z
|
||||
|
||||
height_data = numpy.zeros((height, width), dtype=numpy.float32)
|
||||
height_data = numpy.zeros((height, width), dtype = numpy.float32)
|
||||
|
||||
for x in range(0, width):
|
||||
for y in range(0, height):
|
||||
@ -112,7 +112,7 @@ class ImageReader(MeshReader):
|
||||
height_data = 1 - height_data
|
||||
|
||||
for _ in range(0, blur_iterations):
|
||||
copy = numpy.pad(height_data, ((1, 1), (1, 1)), mode= "edge")
|
||||
copy = numpy.pad(height_data, ((1, 1), (1, 1)), mode = "edge")
|
||||
|
||||
height_data += copy[1:-1, 2:]
|
||||
height_data += copy[1:-1, :-2]
|
||||
@ -165,7 +165,7 @@ class ImageReader(MeshReader):
|
||||
offsetsz = numpy.array(offsetsz, numpy.float32).reshape(-1, 1) * texel_height
|
||||
|
||||
# offsets for each texel quad
|
||||
heightmap_vertex_offsets = numpy.concatenate([offsetsx, numpy.zeros((offsetsx.shape[0], offsetsx.shape[1]), dtype=numpy.float32), offsetsz], 1)
|
||||
heightmap_vertex_offsets = numpy.concatenate([offsetsx, numpy.zeros((offsetsx.shape[0], offsetsx.shape[1]), dtype = numpy.float32), offsetsz], 1)
|
||||
heightmap_vertices += heightmap_vertex_offsets.repeat(6, 0).reshape(-1, 6, 3)
|
||||
|
||||
# apply height data to y values
|
||||
@ -174,7 +174,7 @@ class ImageReader(MeshReader):
|
||||
heightmap_vertices[:, 2, 1] = heightmap_vertices[:, 3, 1] = height_data[1:, 1:].reshape(-1)
|
||||
heightmap_vertices[:, 4, 1] = height_data[:-1, 1:].reshape(-1)
|
||||
|
||||
heightmap_indices = numpy.array(numpy.mgrid[0:heightmap_face_count * 3], dtype=numpy.int32).reshape(-1, 3)
|
||||
heightmap_indices = numpy.array(numpy.mgrid[0:heightmap_face_count * 3], dtype = numpy.int32).reshape(-1, 3)
|
||||
|
||||
mesh._vertices[0:(heightmap_vertices.size // 3), :] = heightmap_vertices.reshape(-1, 3)
|
||||
mesh._indices[0:(heightmap_indices.size // 3), :] = heightmap_indices
|
||||
@ -223,7 +223,7 @@ class ImageReader(MeshReader):
|
||||
mesh.addFaceByPoints(geo_width, 0, y, geo_width, 0, ny, geo_width, he1, ny)
|
||||
mesh.addFaceByPoints(geo_width, he1, ny, geo_width, he0, y, geo_width, 0, y)
|
||||
|
||||
mesh.calculateNormals(fast=True)
|
||||
mesh.calculateNormals(fast = True)
|
||||
|
||||
scene_node.setMeshData(mesh.build())
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
@ -33,9 +33,9 @@ class ImageReaderUI(QObject):
|
||||
self.base_height = 0.4
|
||||
self.peak_height = 2.5
|
||||
self.smoothing = 1
|
||||
self.lighter_is_higher = False;
|
||||
self.use_transparency_model = True;
|
||||
self.transmittance_1mm = 50.0; # based on pearl PLA
|
||||
self.lighter_is_higher = False
|
||||
self.use_transparency_model = True
|
||||
self.transmittance_1mm = 50.0 # based on pearl PLA
|
||||
|
||||
self._ui_lock = threading.Lock()
|
||||
self._cancelled = False
|
||||
@ -85,26 +85,37 @@ class ImageReaderUI(QObject):
|
||||
Logger.log("d", "Creating ImageReader config UI")
|
||||
path = os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml")
|
||||
self._ui_view = Application.getInstance().createQmlComponent(path, {"manager": self})
|
||||
self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowCloseButtonHint & ~Qt.WindowMinimizeButtonHint & ~Qt.WindowMaximizeButtonHint);
|
||||
self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowCloseButtonHint & ~Qt.WindowMinimizeButtonHint & ~Qt.WindowMaximizeButtonHint)
|
||||
self._disable_size_callbacks = False
|
||||
|
||||
@pyqtSlot()
|
||||
def onOkButtonClicked(self):
|
||||
self._cancelled = False
|
||||
self._ui_view.close()
|
||||
self._ui_lock.release()
|
||||
try:
|
||||
self._ui_lock.release()
|
||||
except RuntimeError:
|
||||
# We don't really care if it was held or not. Just make sure it's not held now
|
||||
pass
|
||||
|
||||
@pyqtSlot()
|
||||
def onCancelButtonClicked(self):
|
||||
self._cancelled = True
|
||||
self._ui_view.close()
|
||||
self._ui_lock.release()
|
||||
try:
|
||||
self._ui_lock.release()
|
||||
except RuntimeError:
|
||||
# We don't really care if it was held or not. Just make sure it's not held now
|
||||
pass
|
||||
|
||||
@pyqtSlot(str)
|
||||
def onWidthChanged(self, value):
|
||||
if self._ui_view and not self._disable_size_callbacks:
|
||||
if len(value) > 0:
|
||||
self._width = float(value.replace(",", "."))
|
||||
try:
|
||||
self._width = float(value.replace(",", "."))
|
||||
except ValueError: # Can happen with incomplete numbers, such as "-".
|
||||
self._width = 0
|
||||
else:
|
||||
self._width = 0
|
||||
|
||||
@ -117,7 +128,10 @@ class ImageReaderUI(QObject):
|
||||
def onDepthChanged(self, value):
|
||||
if self._ui_view and not self._disable_size_callbacks:
|
||||
if len(value) > 0:
|
||||
self._depth = float(value.replace(",", "."))
|
||||
try:
|
||||
self._depth = float(value.replace(",", "."))
|
||||
except ValueError: # Can happen with incomplete numbers, such as "-".
|
||||
self._depth = 0
|
||||
else:
|
||||
self._depth = 0
|
||||
|
||||
@ -128,15 +142,21 @@ class ImageReaderUI(QObject):
|
||||
|
||||
@pyqtSlot(str)
|
||||
def onBaseHeightChanged(self, value):
|
||||
if (len(value) > 0):
|
||||
self.base_height = float(value.replace(",", "."))
|
||||
if len(value) > 0:
|
||||
try:
|
||||
self.base_height = float(value.replace(",", "."))
|
||||
except ValueError: # Can happen with incomplete numbers, such as "-".
|
||||
self.base_height = 0
|
||||
else:
|
||||
self.base_height = 0
|
||||
|
||||
@pyqtSlot(str)
|
||||
def onPeakHeightChanged(self, value):
|
||||
if (len(value) > 0):
|
||||
self.peak_height = float(value.replace(",", "."))
|
||||
if len(value) > 0:
|
||||
try:
|
||||
self.peak_height = float(value.replace(",", "."))
|
||||
except ValueError: # Can happen with incomplete numbers, such as "-".
|
||||
self._width = 0
|
||||
else:
|
||||
self.peak_height = 0
|
||||
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Enables ability to generate printable geometry from 2D image files.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -3,6 +3,6 @@
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for importing profiles from legacy Cura versions.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user