mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-06 22:47:29 -06:00
Merge branch 'feature_intent' into feature_intent_container_tree
Conflicts: .gitlab-ci.yml cura/Machines/MaterialManager.py cura/Machines/VariantManager.py cura/Settings/ContainerManager.py cura/Settings/MachineManager.py tests/TestMachineManager.py
This commit is contained in:
commit
6a8e1557c3
1383 changed files with 33204 additions and 35215 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -72,3 +72,6 @@ run.sh
|
||||||
CuraEngine
|
CuraEngine
|
||||||
|
|
||||||
/.coverage
|
/.coverage
|
||||||
|
|
||||||
|
#Prevents import failures when plugin running tests
|
||||||
|
plugins/__init__.py
|
||||||
|
|
|
@ -3,7 +3,6 @@ image: registry.gitlab.com/ultimaker/cura/cura-build-environment:centos7
|
||||||
stages:
|
stages:
|
||||||
- build
|
- build
|
||||||
|
|
||||||
|
|
||||||
build and test linux:
|
build and test linux:
|
||||||
stage: build
|
stage: build
|
||||||
tags:
|
tags:
|
||||||
|
|
|
@ -22,10 +22,10 @@ set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
|
||||||
set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
|
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_ROOT "" CACHE STRING "Alternative Cura cloud API root")
|
||||||
set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version")
|
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")
|
||||||
|
|
||||||
configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY)
|
configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY)
|
||||||
|
|
||||||
|
|
||||||
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
|
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
|
||||||
DEFAULT_CURA_VERSION = "master"
|
DEFAULT_CURA_VERSION = "master"
|
||||||
DEFAULT_CURA_BUILD_TYPE = ""
|
DEFAULT_CURA_BUILD_TYPE = ""
|
||||||
DEFAULT_CURA_DEBUG_MODE = False
|
DEFAULT_CURA_DEBUG_MODE = False
|
||||||
DEFAULT_CURA_SDK_VERSION = "6.1.0"
|
DEFAULT_CURA_SDK_VERSION = "6.2.0"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from cura.CuraVersion import CuraAppName # type: ignore
|
from cura.CuraVersion import CuraAppName # type: ignore
|
||||||
|
@ -45,4 +45,4 @@ except ImportError:
|
||||||
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
|
# 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
|
# 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.
|
# CuraVersion.py.in template.
|
||||||
CuraSDKVersion = "6.1.0"
|
CuraSDKVersion = "6.2.0"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
#Copyright (c) 2019 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
#Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
import copy
|
import copy
|
||||||
|
@ -10,6 +10,7 @@ from UM.Math.Polygon import Polygon
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
|
||||||
|
|
||||||
## Polygon representation as an array for use with Arrange
|
## Polygon representation as an array for use with Arrange
|
||||||
class ShapeArray:
|
class ShapeArray:
|
||||||
def __init__(self, arr: numpy.array, offset_x: float, offset_y: float, scale: float = 1) -> None:
|
def __init__(self, arr: numpy.array, offset_x: float, offset_y: float, scale: float = 1) -> None:
|
||||||
|
@ -101,7 +102,9 @@ class ShapeArray:
|
||||||
|
|
||||||
# Create check array for each edge segment, combine into fill array
|
# Create check array for each edge segment, combine into fill array
|
||||||
for k in range(vertices.shape[0]):
|
for k in range(vertices.shape[0]):
|
||||||
fill = numpy.all([fill, cls._check(vertices[k - 1], vertices[k], base_array)], axis=0)
|
check_array = cls._check(vertices[k - 1], vertices[k], base_array)
|
||||||
|
if check_array is not None:
|
||||||
|
fill = numpy.all([fill, check_array], axis=0)
|
||||||
|
|
||||||
# Set all values inside polygon to one
|
# Set all values inside polygon to one
|
||||||
base_array[fill] = 1
|
base_array[fill] = 1
|
||||||
|
@ -117,9 +120,9 @@ class ShapeArray:
|
||||||
# \param p2 2-tuple with x, y for point 2
|
# \param p2 2-tuple with x, y for point 2
|
||||||
# \param base_array boolean array to project the line on
|
# \param base_array boolean array to project the line on
|
||||||
@classmethod
|
@classmethod
|
||||||
def _check(cls, p1: numpy.array, p2: numpy.array, base_array: numpy.array) -> bool:
|
def _check(cls, p1: numpy.array, p2: numpy.array, base_array: numpy.array) -> Optional[numpy.array]:
|
||||||
if p1[0] == p2[0] and p1[1] == p2[1]:
|
if p1[0] == p2[0] and p1[1] == p2[1]:
|
||||||
return False
|
return None
|
||||||
idxs = numpy.indices(base_array.shape) # Create 3D array of indices
|
idxs = numpy.indices(base_array.shape) # Create 3D array of indices
|
||||||
|
|
||||||
p1 = p1.astype(float)
|
p1 = p1.astype(float)
|
||||||
|
|
|
@ -64,7 +64,7 @@ class BuildVolume(SceneNode):
|
||||||
|
|
||||||
self._origin_mesh = None # type: Optional[MeshData]
|
self._origin_mesh = None # type: Optional[MeshData]
|
||||||
self._origin_line_length = 20
|
self._origin_line_length = 20
|
||||||
self._origin_line_width = 0.5
|
self._origin_line_width = 1.5
|
||||||
|
|
||||||
self._grid_mesh = None # type: Optional[MeshData]
|
self._grid_mesh = None # type: Optional[MeshData]
|
||||||
self._grid_shader = None
|
self._grid_shader = None
|
||||||
|
@ -258,7 +258,7 @@ class BuildVolume(SceneNode):
|
||||||
node.setOutsideBuildArea(True)
|
node.setOutsideBuildArea(True)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if node.collidesWithArea(self.getDisallowedAreas()):
|
if node.collidesWithAreas(self.getDisallowedAreas()):
|
||||||
node.setOutsideBuildArea(True)
|
node.setOutsideBuildArea(True)
|
||||||
continue
|
continue
|
||||||
# If the entire node is below the build plate, still mark it as outside.
|
# If the entire node is below the build plate, still mark it as outside.
|
||||||
|
@ -312,7 +312,7 @@ class BuildVolume(SceneNode):
|
||||||
node.setOutsideBuildArea(True)
|
node.setOutsideBuildArea(True)
|
||||||
return
|
return
|
||||||
|
|
||||||
if node.collidesWithArea(self.getDisallowedAreas()):
|
if node.collidesWithAreas(self.getDisallowedAreas()):
|
||||||
node.setOutsideBuildArea(True)
|
node.setOutsideBuildArea(True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -770,7 +770,7 @@ class BuildVolume(SceneNode):
|
||||||
|
|
||||||
self._has_errors = len(self._error_areas) > 0
|
self._has_errors = len(self._error_areas) > 0
|
||||||
|
|
||||||
self._disallowed_areas = [] # type: List[Polygon]
|
self._disallowed_areas = []
|
||||||
for extruder_id in result_areas:
|
for extruder_id in result_areas:
|
||||||
self._disallowed_areas.extend(result_areas[extruder_id])
|
self._disallowed_areas.extend(result_areas[extruder_id])
|
||||||
self._disallowed_areas_no_brim = []
|
self._disallowed_areas_no_brim = []
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, QUrl
|
from PyQt5.QtCore import QObject, QUrl
|
||||||
from PyQt5.QtGui import QDesktopServices
|
from PyQt5.QtGui import QDesktopServices
|
||||||
from typing import List, TYPE_CHECKING, cast
|
from typing import List, cast
|
||||||
|
|
||||||
from UM.Event import CallFunctionEvent
|
from UM.Event import CallFunctionEvent
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
@ -23,9 +23,8 @@ from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation
|
from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from UM.Scene.SceneNode import SceneNode
|
|
||||||
|
|
||||||
class CuraActions(QObject):
|
class CuraActions(QObject):
|
||||||
def __init__(self, parent: QObject = None) -> None:
|
def __init__(self, parent: QObject = None) -> None:
|
||||||
|
|
|
@ -146,7 +146,7 @@ class CuraApplication(QtApplication):
|
||||||
# SettingVersion represents the set of settings available in the machine/extruder definitions.
|
# 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
|
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
|
||||||
# changes of the settings.
|
# changes of the settings.
|
||||||
SettingVersion = 8
|
SettingVersion = 9
|
||||||
|
|
||||||
Created = False
|
Created = False
|
||||||
|
|
||||||
|
@ -425,7 +425,7 @@ class CuraApplication(QtApplication):
|
||||||
# Add empty variant, material and quality containers.
|
# Add empty variant, material and quality containers.
|
||||||
# Since they are empty, they should never be serialized and instead just programmatically created.
|
# Since they are empty, they should never be serialized and instead just programmatically created.
|
||||||
# We need them to simplify the switching between materials.
|
# We need them to simplify the switching between materials.
|
||||||
self.empty_container = cura.Settings.cura_empty_instance_containers.empty_container # type: EmptyInstanceContainer
|
self.empty_container = cura.Settings.cura_empty_instance_containers.empty_container
|
||||||
|
|
||||||
self._container_registry.addContainer(
|
self._container_registry.addContainer(
|
||||||
cura.Settings.cura_empty_instance_containers.empty_definition_changes_container)
|
cura.Settings.cura_empty_instance_containers.empty_definition_changes_container)
|
||||||
|
@ -1266,7 +1266,7 @@ class CuraApplication(QtApplication):
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def arrangeObjectsToAllBuildPlates(self) -> None:
|
def arrangeObjectsToAllBuildPlates(self) -> None:
|
||||||
nodes_to_arrange = []
|
nodes_to_arrange = []
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
if not isinstance(node, SceneNode):
|
if not isinstance(node, SceneNode):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -1293,7 +1293,7 @@ class CuraApplication(QtApplication):
|
||||||
def arrangeAll(self) -> None:
|
def arrangeAll(self) -> None:
|
||||||
nodes_to_arrange = []
|
nodes_to_arrange = []
|
||||||
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
|
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
if not isinstance(node, SceneNode):
|
if not isinstance(node, SceneNode):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -1331,7 +1331,7 @@ class CuraApplication(QtApplication):
|
||||||
Logger.log("i", "Reloading all loaded mesh data.")
|
Logger.log("i", "Reloading all loaded mesh data.")
|
||||||
nodes = []
|
nodes = []
|
||||||
has_merged_nodes = False
|
has_merged_nodes = False
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
if not isinstance(node, CuraSceneNode) or not node.getMeshData():
|
if not isinstance(node, CuraSceneNode) or not node.getMeshData():
|
||||||
if node.getName() == "MergedMesh":
|
if node.getName() == "MergedMesh":
|
||||||
has_merged_nodes = True
|
has_merged_nodes = True
|
||||||
|
@ -1343,9 +1343,9 @@ class CuraApplication(QtApplication):
|
||||||
return
|
return
|
||||||
|
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
file_name = node.getMeshData().getFileName()
|
mesh_data = node.getMeshData()
|
||||||
if file_name:
|
if mesh_data and mesh_data.getFileName():
|
||||||
job = ReadMeshJob(file_name)
|
job = ReadMeshJob(mesh_data.getFileName())
|
||||||
job._node = node # type: ignore
|
job._node = node # type: ignore
|
||||||
job.finished.connect(self._reloadMeshFinished)
|
job.finished.connect(self._reloadMeshFinished)
|
||||||
if has_merged_nodes:
|
if has_merged_nodes:
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import List
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
|
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||||
|
from UM.Mesh.MeshData import MeshData
|
||||||
|
from cura.LayerPolygon import LayerPolygon
|
||||||
|
|
||||||
|
|
||||||
class Layer:
|
class Layer:
|
||||||
def __init__(self, layer_id):
|
def __init__(self, layer_id: int) -> None:
|
||||||
self._id = layer_id
|
self._id = layer_id
|
||||||
self._height = 0.0
|
self._height = 0.0
|
||||||
self._thickness = 0.0
|
self._thickness = 0.0
|
||||||
self._polygons = []
|
self._polygons = [] # type: List[LayerPolygon]
|
||||||
self._element_count = 0
|
self._element_count = 0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -20,7 +26,7 @@ class Layer:
|
||||||
return self._thickness
|
return self._thickness
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def polygons(self):
|
def polygons(self) -> List[LayerPolygon]:
|
||||||
return self._polygons
|
return self._polygons
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -33,14 +39,14 @@ class Layer:
|
||||||
def setThickness(self, thickness):
|
def setThickness(self, thickness):
|
||||||
self._thickness = thickness
|
self._thickness = thickness
|
||||||
|
|
||||||
def lineMeshVertexCount(self):
|
def lineMeshVertexCount(self) -> int:
|
||||||
result = 0
|
result = 0
|
||||||
for polygon in self._polygons:
|
for polygon in self._polygons:
|
||||||
result += polygon.lineMeshVertexCount()
|
result += polygon.lineMeshVertexCount()
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def lineMeshElementCount(self):
|
def lineMeshElementCount(self) -> int:
|
||||||
result = 0
|
result = 0
|
||||||
for polygon in self._polygons:
|
for polygon in self._polygons:
|
||||||
result += polygon.lineMeshElementCount()
|
result += polygon.lineMeshElementCount()
|
||||||
|
@ -57,18 +63,18 @@ class Layer:
|
||||||
result_index_offset += polygon.lineMeshElementCount()
|
result_index_offset += polygon.lineMeshElementCount()
|
||||||
self._element_count += polygon.elementCount
|
self._element_count += polygon.elementCount
|
||||||
|
|
||||||
return (result_vertex_offset, result_index_offset)
|
return result_vertex_offset, result_index_offset
|
||||||
|
|
||||||
def createMesh(self):
|
def createMesh(self) -> MeshData:
|
||||||
return self.createMeshOrJumps(True)
|
return self.createMeshOrJumps(True)
|
||||||
|
|
||||||
def createJumps(self):
|
def createJumps(self) -> MeshData:
|
||||||
return self.createMeshOrJumps(False)
|
return self.createMeshOrJumps(False)
|
||||||
|
|
||||||
# Defines the two triplets of local point indices to use to draw the two faces for each line segment in createMeshOrJump
|
# Defines the two triplets of local point indices to use to draw the two faces for each line segment in createMeshOrJump
|
||||||
__index_pattern = numpy.array([[0, 3, 2, 0, 1, 3]], dtype = numpy.int32 )
|
__index_pattern = numpy.array([[0, 3, 2, 0, 1, 3]], dtype = numpy.int32 )
|
||||||
|
|
||||||
def createMeshOrJumps(self, make_mesh):
|
def createMeshOrJumps(self, make_mesh: bool) -> MeshData:
|
||||||
builder = MeshBuilder()
|
builder = MeshBuilder()
|
||||||
|
|
||||||
line_count = 0
|
line_count = 0
|
||||||
|
@ -79,14 +85,14 @@ class Layer:
|
||||||
for polygon in self._polygons:
|
for polygon in self._polygons:
|
||||||
line_count += polygon.jumpCount
|
line_count += polygon.jumpCount
|
||||||
|
|
||||||
# Reserve the neccesary space for the data upfront
|
# Reserve the necessary space for the data upfront
|
||||||
builder.reserveFaceAndVertexCount(2 * line_count, 4 * line_count)
|
builder.reserveFaceAndVertexCount(2 * line_count, 4 * line_count)
|
||||||
|
|
||||||
for polygon in self._polygons:
|
for polygon in self._polygons:
|
||||||
# Filter out the types of lines we are not interesed in depending on whether we are drawing the mesh or the jumps.
|
# Filter out the types of lines we are not interested in depending on whether we are drawing the mesh or the jumps.
|
||||||
index_mask = numpy.logical_not(polygon.jumpMask) if make_mesh else polygon.jumpMask
|
index_mask = numpy.logical_not(polygon.jumpMask) if make_mesh else polygon.jumpMask
|
||||||
|
|
||||||
# Create an array with rows [p p+1] and only keep those we whant to draw based on make_mesh
|
# Create an array with rows [p p+1] and only keep those we want to draw based on make_mesh
|
||||||
points = numpy.concatenate((polygon.data[:-1], polygon.data[1:]), 1)[index_mask.ravel()]
|
points = numpy.concatenate((polygon.data[:-1], polygon.data[1:]), 1)[index_mask.ravel()]
|
||||||
# Line types of the points we want to draw
|
# Line types of the points we want to draw
|
||||||
line_types = polygon.types[index_mask]
|
line_types = polygon.types[index_mask]
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
@ -61,19 +61,19 @@ class LayerPolygon:
|
||||||
|
|
||||||
# When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType
|
# 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.
|
# Should be generated in better way, not hardcoded.
|
||||||
self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1], dtype=numpy.bool)
|
self._isInfillOrSkinTypeMap = 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_line_mesh_mask = None # type: Optional[numpy.ndarray]
|
||||||
self._build_cache_needed_points = None # type: Optional[numpy.ndarray]
|
self._build_cache_needed_points = None # type: Optional[numpy.ndarray]
|
||||||
|
|
||||||
def buildCache(self) -> None:
|
def buildCache(self) -> None:
|
||||||
# For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out.
|
# For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out.
|
||||||
self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype=bool)
|
self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype = bool)
|
||||||
mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask)
|
mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask)
|
||||||
self._index_begin = 0
|
self._index_begin = 0
|
||||||
self._index_end = mesh_line_count
|
self._index_end = mesh_line_count
|
||||||
|
|
||||||
self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype=numpy.bool)
|
self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype = numpy.bool)
|
||||||
# Only if the type of line segment changes do we need to add an extra vertex to change colors
|
# Only if the type of line segment changes do we need to add an extra vertex to change colors
|
||||||
self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1]
|
self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1]
|
||||||
# Mark points as unneeded if they are of types we don't want in the line mesh according to the calculated mask
|
# Mark points as unneeded if they are of types we don't want in the line mesh according to the calculated mask
|
||||||
|
@ -136,9 +136,9 @@ class LayerPolygon:
|
||||||
self._index_begin += index_offset
|
self._index_begin += index_offset
|
||||||
self._index_end += index_offset
|
self._index_end += index_offset
|
||||||
|
|
||||||
indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype=numpy.int32).reshape((-1, 1))
|
indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype = numpy.int32).reshape((-1, 1))
|
||||||
# When the line type changes the index needs to be increased by 2.
|
# When the line type changes the index needs to be increased by 2.
|
||||||
indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype=numpy.int32).reshape((-1, 1))
|
indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype = numpy.int32).reshape((-1, 1))
|
||||||
# Each line segment goes from it's starting point p to p+1, offset by the vertex index.
|
# Each line segment goes from it's starting point p to p+1, offset by the vertex index.
|
||||||
# The -1 is to compensate for the neccecarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above.
|
# The -1 is to compensate for the neccecarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above.
|
||||||
indices[self._index_begin:self._index_end, :] += numpy.array([self._vertex_begin - 1, self._vertex_begin])
|
indices[self._index_begin:self._index_end, :] += numpy.array([self._vertex_begin - 1, self._vertex_begin])
|
||||||
|
|
|
@ -127,7 +127,7 @@ class MachineErrorChecker(QObject):
|
||||||
|
|
||||||
# Populate the (stack, key) tuples to check
|
# Populate the (stack, key) tuples to check
|
||||||
self._stacks_and_keys_to_check = deque()
|
self._stacks_and_keys_to_check = deque()
|
||||||
for stack in [global_stack] + list(global_stack.extruders.values()):
|
for stack in global_stack.extruders.values():
|
||||||
for key in stack.getAllKeys():
|
for key in stack.getAllKeys():
|
||||||
self._stacks_and_keys_to_check.append((stack, key))
|
self._stacks_and_keys_to_check.append((stack, key))
|
||||||
|
|
||||||
|
|
|
@ -75,7 +75,7 @@ class DiscoveredPrinter(QObject):
|
||||||
def readableMachineType(self) -> str:
|
def readableMachineType(self) -> str:
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
machine_manager = CuraApplication.getInstance().getMachineManager()
|
machine_manager = CuraApplication.getInstance().getMachineManager()
|
||||||
# In ClusterUM3OutputDevice, when it updates a printer information, it updates the machine type using the field
|
# In LocalClusterOutputDevice, when it updates a printer information, it updates the machine type using the field
|
||||||
# "machine_variant", and for some reason, it's not the machine type ID/codename/... but a human-readable string
|
# "machine_variant", and for some reason, it's not the machine type ID/codename/... but a human-readable string
|
||||||
# like "Ultimaker 3". The code below handles this case.
|
# like "Ultimaker 3". The code below handles this case.
|
||||||
if self._hasHumanReadableMachineTypeName(self._machine_type):
|
if self._hasHumanReadableMachineTypeName(self._machine_type):
|
||||||
|
|
|
@ -93,7 +93,14 @@ class VariantManager:
|
||||||
if variant_definition not in self._machine_to_buildplate_dict_map:
|
if variant_definition not in self._machine_to_buildplate_dict_map:
|
||||||
self._machine_to_buildplate_dict_map[variant_definition] = OrderedDict()
|
self._machine_to_buildplate_dict_map[variant_definition] = OrderedDict()
|
||||||
|
|
||||||
|
try:
|
||||||
variant_container = container_registry.findContainers(type = "variant", id = variant_metadata["id"])[0]
|
variant_container = container_registry.findContainers(type = "variant", id = variant_metadata["id"])[0]
|
||||||
|
except IndexError as e:
|
||||||
|
# It still needs to break, but we want to know what variant ID made it break.
|
||||||
|
msg = "Unable to find build plate variant with the id [%s]" % variant_metadata["id"]
|
||||||
|
Logger.logException("e", msg)
|
||||||
|
raise IndexError(msg)
|
||||||
|
|
||||||
buildplate_type = variant_container.getProperty("machine_buildplate_type", "value")
|
buildplate_type = variant_container.getProperty("machine_buildplate_type", "value")
|
||||||
if buildplate_type not in self._machine_to_buildplate_dict_map[variant_definition]:
|
if buildplate_type not in self._machine_to_buildplate_dict_map[variant_definition]:
|
||||||
self._machine_to_variant_dict_map[variant_definition][buildplate_type] = dict()
|
self._machine_to_variant_dict_map[variant_definition][buildplate_type] = dict()
|
||||||
|
|
|
@ -2,10 +2,12 @@
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import copy
|
import copy
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Operations.GroupedOperation import GroupedOperation
|
from UM.Operations.GroupedOperation import GroupedOperation
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
i18n_catalog = i18nCatalog("cura")
|
i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
@ -23,7 +25,7 @@ class MultiplyObjectsJob(Job):
|
||||||
self._count = count
|
self._count = count
|
||||||
self._min_offset = min_offset
|
self._min_offset = min_offset
|
||||||
|
|
||||||
def run(self):
|
def run(self) -> None:
|
||||||
status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0,
|
status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0,
|
||||||
dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Objects"))
|
dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Objects"))
|
||||||
status_message.show()
|
status_message.show()
|
||||||
|
@ -33,13 +35,15 @@ class MultiplyObjectsJob(Job):
|
||||||
current_progress = 0
|
current_progress = 0
|
||||||
|
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
|
if global_container_stack is None:
|
||||||
|
return # We can't do anything in this case.
|
||||||
machine_width = global_container_stack.getProperty("machine_width", "value")
|
machine_width = global_container_stack.getProperty("machine_width", "value")
|
||||||
machine_depth = global_container_stack.getProperty("machine_depth", "value")
|
machine_depth = global_container_stack.getProperty("machine_depth", "value")
|
||||||
|
|
||||||
root = scene.getRoot()
|
root = scene.getRoot()
|
||||||
scale = 0.5
|
scale = 0.5
|
||||||
arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale, min_offset = self._min_offset)
|
arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale, min_offset = self._min_offset)
|
||||||
processed_nodes = []
|
processed_nodes = [] # type: List[SceneNode]
|
||||||
nodes = []
|
nodes = []
|
||||||
|
|
||||||
not_fit_count = 0
|
not_fit_count = 0
|
||||||
|
@ -67,7 +71,11 @@ class MultiplyObjectsJob(Job):
|
||||||
new_node = copy.deepcopy(node)
|
new_node = copy.deepcopy(node)
|
||||||
solution_found = False
|
solution_found = False
|
||||||
if not node_too_big:
|
if not node_too_big:
|
||||||
|
if offset_shape_arr is not None and hull_shape_arr is not None:
|
||||||
solution_found = arranger.findNodePlacement(new_node, offset_shape_arr, hull_shape_arr)
|
solution_found = arranger.findNodePlacement(new_node, offset_shape_arr, hull_shape_arr)
|
||||||
|
else:
|
||||||
|
# The node has no shape, so no need to arrange it. The solution is simple: Do nothing.
|
||||||
|
solution_found = True
|
||||||
|
|
||||||
if node_too_big or not solution_found:
|
if node_too_big or not solution_found:
|
||||||
found_solution_for_all = False
|
found_solution_for_all = False
|
||||||
|
|
|
@ -63,6 +63,10 @@ class LocalAuthorizationServer:
|
||||||
Logger.log("d", "Stopping local oauth2 web server...")
|
Logger.log("d", "Stopping local oauth2 web server...")
|
||||||
|
|
||||||
if self._web_server:
|
if self._web_server:
|
||||||
|
try:
|
||||||
self._web_server.server_close()
|
self._web_server.server_close()
|
||||||
|
except OSError:
|
||||||
|
# OS error can happen if the socket was already closed. We really don't care about that case.
|
||||||
|
pass
|
||||||
self._web_server = None
|
self._web_server = None
|
||||||
self._web_server_thread = None
|
self._web_server_thread = None
|
||||||
|
|
|
@ -49,18 +49,20 @@ class PlatformPhysics:
|
||||||
return
|
return
|
||||||
|
|
||||||
root = self._controller.getScene().getRoot()
|
root = self._controller.getScene().getRoot()
|
||||||
|
build_volume = Application.getInstance().getBuildVolume()
|
||||||
|
build_volume.updateNodeBoundaryCheck()
|
||||||
|
|
||||||
# Keep a list of nodes that are moving. We use this so that we don't move two intersecting objects in the
|
# Keep a list of nodes that are moving. We use this so that we don't move two intersecting objects in the
|
||||||
# same direction.
|
# same direction.
|
||||||
transformed_nodes = []
|
transformed_nodes = []
|
||||||
|
|
||||||
# We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
|
|
||||||
# By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.
|
|
||||||
nodes = list(BreadthFirstIterator(root))
|
nodes = list(BreadthFirstIterator(root))
|
||||||
|
|
||||||
# Only check nodes inside build area.
|
# Only check nodes inside build area.
|
||||||
nodes = [node for node in nodes if (hasattr(node, "_outside_buildarea") and not node._outside_buildarea)]
|
nodes = [node for node in nodes if (hasattr(node, "_outside_buildarea") and not node._outside_buildarea)]
|
||||||
|
|
||||||
|
# We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
|
||||||
|
# By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.
|
||||||
random.shuffle(nodes)
|
random.shuffle(nodes)
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
if node is root or not isinstance(node, SceneNode) or node.getBoundingBox() is None:
|
if node is root or not isinstance(node, SceneNode) or node.getBoundingBox() is None:
|
||||||
|
@ -160,7 +162,6 @@ class PlatformPhysics:
|
||||||
op.push()
|
op.push()
|
||||||
|
|
||||||
# After moving, we have to evaluate the boundary checks for nodes
|
# After moving, we have to evaluate the boundary checks for nodes
|
||||||
build_volume = Application.getInstance().getBuildVolume()
|
|
||||||
build_volume.updateNodeBoundaryCheck()
|
build_volume.updateNodeBoundaryCheck()
|
||||||
|
|
||||||
def _onToolOperationStarted(self, tool):
|
def _onToolOperationStarted(self, tool):
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING, cast
|
||||||
|
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
|
@ -12,6 +13,7 @@ from UM.View.RenderBatch import RenderBatch
|
||||||
|
|
||||||
|
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from UM.View.GL.ShaderProgram import ShaderProgram
|
from UM.View.GL.ShaderProgram import ShaderProgram
|
||||||
|
@ -44,9 +46,9 @@ class PreviewPass(RenderPass):
|
||||||
|
|
||||||
self._renderer = Application.getInstance().getRenderer()
|
self._renderer = Application.getInstance().getRenderer()
|
||||||
|
|
||||||
self._shader = None #type: Optional[ShaderProgram]
|
self._shader = None # type: Optional[ShaderProgram]
|
||||||
self._non_printing_shader = None #type: Optional[ShaderProgram]
|
self._non_printing_shader = None # type: Optional[ShaderProgram]
|
||||||
self._support_mesh_shader = None #type: Optional[ShaderProgram]
|
self._support_mesh_shader = None # type: Optional[ShaderProgram]
|
||||||
self._scene = Application.getInstance().getController().getScene()
|
self._scene = Application.getInstance().getController().getScene()
|
||||||
|
|
||||||
# Set the camera to be used by this render pass
|
# Set the camera to be used by this render pass
|
||||||
|
@ -83,8 +85,8 @@ class PreviewPass(RenderPass):
|
||||||
batch_support_mesh = RenderBatch(self._support_mesh_shader)
|
batch_support_mesh = RenderBatch(self._support_mesh_shader)
|
||||||
|
|
||||||
# Fill up the batch with objects that can be sliced.
|
# Fill up the batch with objects that can be sliced.
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
if hasattr(node, "_outside_buildarea") and not node._outside_buildarea:
|
if hasattr(node, "_outside_buildarea") and not getattr(node, "_outside_buildarea"):
|
||||||
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
|
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
|
||||||
per_mesh_stack = node.callDecoration("getStack")
|
per_mesh_stack = node.callDecoration("getStack")
|
||||||
if node.callDecoration("isNonThumbnailVisibleMesh"):
|
if node.callDecoration("isNonThumbnailVisibleMesh"):
|
||||||
|
@ -94,7 +96,7 @@ class PreviewPass(RenderPass):
|
||||||
# Support mesh
|
# Support mesh
|
||||||
uniforms = {}
|
uniforms = {}
|
||||||
shade_factor = 0.6
|
shade_factor = 0.6
|
||||||
diffuse_color = node.getDiffuseColor()
|
diffuse_color = cast(CuraSceneNode, node).getDiffuseColor()
|
||||||
diffuse_color2 = [
|
diffuse_color2 = [
|
||||||
diffuse_color[0] * shade_factor,
|
diffuse_color[0] * shade_factor,
|
||||||
diffuse_color[1] * shade_factor,
|
diffuse_color[1] * shade_factor,
|
||||||
|
@ -106,7 +108,7 @@ class PreviewPass(RenderPass):
|
||||||
else:
|
else:
|
||||||
# Normal scene node
|
# Normal scene node
|
||||||
uniforms = {}
|
uniforms = {}
|
||||||
uniforms["diffuse_color"] = prettier_color(node.getDiffuseColor())
|
uniforms["diffuse_color"] = prettier_color(cast(CuraSceneNode, node).getDiffuseColor())
|
||||||
batch.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms)
|
batch.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms)
|
||||||
|
|
||||||
self.bind()
|
self.bind()
|
||||||
|
|
|
@ -55,7 +55,7 @@ class GenericOutputController(PrinterOutputController):
|
||||||
self._preheat_hotends_timer.stop()
|
self._preheat_hotends_timer.stop()
|
||||||
for extruder in self._preheat_hotends:
|
for extruder in self._preheat_hotends:
|
||||||
extruder.updateIsPreheating(False)
|
extruder.updateIsPreheating(False)
|
||||||
self._preheat_hotends = set() # type: Set[ExtruderOutputModel]
|
self._preheat_hotends = set()
|
||||||
|
|
||||||
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed) -> None:
|
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed) -> None:
|
||||||
self._output_device.sendCommand("G91")
|
self._output_device.sendCommand("G91")
|
||||||
|
@ -159,7 +159,7 @@ class GenericOutputController(PrinterOutputController):
|
||||||
def _onPreheatHotendsTimerFinished(self) -> None:
|
def _onPreheatHotendsTimerFinished(self) -> None:
|
||||||
for extruder in self._preheat_hotends:
|
for extruder in self._preheat_hotends:
|
||||||
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0)
|
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0)
|
||||||
self._preheat_hotends = set() #type: Set[ExtruderOutputModel]
|
self._preheat_hotends = set()
|
||||||
|
|
||||||
# Cancel any ongoing preheating timers, without setting back the temperature to 0
|
# Cancel any ongoing preheating timers, without setting back the temperature to 0
|
||||||
# This can be used eg at the start of a print
|
# This can be used eg at the start of a print
|
||||||
|
@ -167,7 +167,7 @@ class GenericOutputController(PrinterOutputController):
|
||||||
if self._preheat_hotends_timer.isActive():
|
if self._preheat_hotends_timer.isActive():
|
||||||
for extruder in self._preheat_hotends:
|
for extruder in self._preheat_hotends:
|
||||||
extruder.updateIsPreheating(False)
|
extruder.updateIsPreheating(False)
|
||||||
self._preheat_hotends = set() #type: Set[ExtruderOutputModel]
|
self._preheat_hotends = set()
|
||||||
|
|
||||||
self._preheat_hotends_timer.stop()
|
self._preheat_hotends_timer.stop()
|
||||||
|
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot, QUrl
|
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot, QUrl
|
||||||
from typing import List, Dict, Optional
|
from typing import List, Dict, Optional, TYPE_CHECKING
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
|
from cura.PrinterOutput.Peripheral import Peripheral
|
||||||
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||||
from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel
|
from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel
|
||||||
|
|
||||||
MYPY = False
|
if TYPE_CHECKING:
|
||||||
if MYPY:
|
|
||||||
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
|
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||||
|
|
||||||
|
@ -45,6 +45,7 @@ class PrinterOutputModel(QObject):
|
||||||
self._is_preheating = False
|
self._is_preheating = False
|
||||||
self._printer_type = ""
|
self._printer_type = ""
|
||||||
self._buildplate = ""
|
self._buildplate = ""
|
||||||
|
self._peripherals = [] # type: List[Peripheral]
|
||||||
|
|
||||||
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
|
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
|
||||||
self._extruders]
|
self._extruders]
|
||||||
|
@ -295,3 +296,17 @@ class PrinterOutputModel(QObject):
|
||||||
if self._printer_configuration.isValid():
|
if self._printer_configuration.isValid():
|
||||||
return self._printer_configuration
|
return self._printer_configuration
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
peripheralsChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = peripheralsChanged)
|
||||||
|
def peripherals(self) -> str:
|
||||||
|
return ", ".join(*[peripheral.name for peripheral in self._peripherals])
|
||||||
|
|
||||||
|
def addPeripheral(self, peripheral: Peripheral) -> None:
|
||||||
|
self._peripherals.append(peripheral)
|
||||||
|
self.peripheralsChanged.emit()
|
||||||
|
|
||||||
|
def removePeripheral(self, peripheral: Peripheral) -> None:
|
||||||
|
self._peripherals.remove(peripheral)
|
||||||
|
self.peripheralsChanged.emit()
|
|
@ -60,8 +60,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
self._gcode = [] # type: List[str]
|
self._gcode = [] # type: List[str]
|
||||||
self._connection_state_before_timeout = None # type: Optional[ConnectionState]
|
self._connection_state_before_timeout = None # type: Optional[ConnectionState]
|
||||||
|
|
||||||
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||||
file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
|
file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None:
|
||||||
raise NotImplementedError("requestWrite needs to be implemented")
|
raise NotImplementedError("requestWrite needs to be implemented")
|
||||||
|
|
||||||
def setAuthenticationState(self, authentication_state: AuthState) -> None:
|
def setAuthenticationState(self, authentication_state: AuthState) -> None:
|
||||||
|
|
16
cura/PrinterOutput/Peripheral.py
Normal file
16
cura/PrinterOutput/Peripheral.py
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
|
||||||
|
## Data class that represents a peripheral for a printer.
|
||||||
|
#
|
||||||
|
# Output device plug-ins may specify that the printer has a certain set of
|
||||||
|
# peripherals. This set is then possibly shown in the interface of the monitor
|
||||||
|
# stage.
|
||||||
|
class Peripheral:
|
||||||
|
## Constructs the peripheral.
|
||||||
|
# \param type A unique ID for the type of peripheral.
|
||||||
|
# \param name A human-readable name for the peripheral.
|
||||||
|
def __init__(self, peripheral_type: str, name: str) -> None:
|
||||||
|
self.type = peripheral_type
|
||||||
|
self.name = name
|
|
@ -144,7 +144,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||||
file_handler: Optional["FileHandler"] = None, **kwargs: str) -> None:
|
file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None:
|
||||||
raise NotImplementedError("requestWrite needs to be implemented")
|
raise NotImplementedError("requestWrite needs to be implemented")
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify = printersChanged)
|
@pyqtProperty(QObject, notify = printersChanged)
|
||||||
|
|
|
@ -6,13 +6,13 @@ from typing import cast, Dict, List, Optional
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||||
from UM.Math.Polygon import Polygon #For typing.
|
from UM.Math.Polygon import Polygon # For typing.
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator #To cast the deepcopy of every decorator back to SceneNodeDecorator.
|
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator # To cast the deepcopy of every decorator back to SceneNodeDecorator.
|
||||||
|
|
||||||
import cura.CuraApplication #To get the build plate.
|
import cura.CuraApplication # To get the build plate.
|
||||||
from cura.Settings.ExtruderStack import ExtruderStack #For typing.
|
from cura.Settings.ExtruderStack import ExtruderStack # For typing.
|
||||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator #For per-object settings.
|
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator # For per-object settings.
|
||||||
|
|
||||||
|
|
||||||
## Scene nodes that are models are only seen when selecting the corresponding build plate
|
## Scene nodes that are models are only seen when selecting the corresponding build plate
|
||||||
|
@ -21,7 +21,7 @@ class CuraSceneNode(SceneNode):
|
||||||
def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None:
|
def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None:
|
||||||
super().__init__(parent = parent, visible = visible, name = name)
|
super().__init__(parent = parent, visible = visible, name = name)
|
||||||
if not no_setting_override:
|
if not no_setting_override:
|
||||||
self.addDecorator(SettingOverrideDecorator()) # now we always have a getActiveExtruderPosition, unless explicitly disabled
|
self.addDecorator(SettingOverrideDecorator()) # Now we always have a getActiveExtruderPosition, unless explicitly disabled
|
||||||
self._outside_buildarea = False
|
self._outside_buildarea = False
|
||||||
|
|
||||||
def setOutsideBuildArea(self, new_value: bool) -> None:
|
def setOutsideBuildArea(self, new_value: bool) -> None:
|
||||||
|
@ -87,13 +87,13 @@ class CuraSceneNode(SceneNode):
|
||||||
]
|
]
|
||||||
|
|
||||||
## Return if any area collides with the convex hull of this scene node
|
## Return if any area collides with the convex hull of this scene node
|
||||||
def collidesWithArea(self, areas: List[Polygon]) -> bool:
|
def collidesWithAreas(self, areas: List[Polygon]) -> bool:
|
||||||
convex_hull = self.callDecoration("getConvexHull")
|
convex_hull = self.callDecoration("getConvexHull")
|
||||||
if convex_hull:
|
if convex_hull:
|
||||||
if not convex_hull.isValid():
|
if not convex_hull.isValid():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Check for collisions between disallowed areas and the object
|
# Check for collisions between provided areas and the object
|
||||||
for area in areas:
|
for area in areas:
|
||||||
overlap = convex_hull.intersectsPolygon(area)
|
overlap = convex_hull.intersectsPolygon(area)
|
||||||
if overlap is None:
|
if overlap is None:
|
||||||
|
|
|
@ -5,10 +5,11 @@ import os
|
||||||
import re
|
import re
|
||||||
import configparser
|
import configparser
|
||||||
|
|
||||||
from typing import Any, cast, Dict, Optional
|
from typing import Any, cast, Dict, Optional, List, Union
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
from UM.Decorators import override
|
from UM.Decorators import override
|
||||||
|
from UM.PluginObject import PluginObject
|
||||||
from UM.Settings.ContainerFormatError import ContainerFormatError
|
from UM.Settings.ContainerFormatError import ContainerFormatError
|
||||||
from UM.Settings.Interfaces import ContainerInterface
|
from UM.Settings.Interfaces import ContainerInterface
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
@ -22,6 +23,7 @@ from UM.Platform import Platform
|
||||||
from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with.
|
from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with.
|
||||||
from UM.Util import parseBool
|
from UM.Util import parseBool
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
|
from cura.ReaderWriters.ProfileWriter import ProfileWriter
|
||||||
|
|
||||||
from . import ExtruderStack
|
from . import ExtruderStack
|
||||||
from . import GlobalStack
|
from . import GlobalStack
|
||||||
|
@ -50,10 +52,10 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
# This will also try to convert a ContainerStack to either Extruder or
|
# This will also try to convert a ContainerStack to either Extruder or
|
||||||
# Global stack based on metadata information.
|
# Global stack based on metadata information.
|
||||||
@override(ContainerRegistry)
|
@override(ContainerRegistry)
|
||||||
def addContainer(self, container):
|
def addContainer(self, container: ContainerInterface) -> None:
|
||||||
# Note: Intentional check with type() because we want to ignore subclasses
|
# Note: Intentional check with type() because we want to ignore subclasses
|
||||||
if type(container) == ContainerStack:
|
if type(container) == ContainerStack:
|
||||||
container = self._convertContainerStack(container)
|
container = self._convertContainerStack(cast(ContainerStack, container))
|
||||||
|
|
||||||
if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()):
|
if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()):
|
||||||
# Check against setting version of the definition.
|
# Check against setting version of the definition.
|
||||||
|
@ -61,7 +63,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0))
|
actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0))
|
||||||
if required_setting_version != actual_setting_version:
|
if required_setting_version != actual_setting_version:
|
||||||
Logger.log("w", "Instance container {container_id} is outdated. Its setting version is {actual_setting_version} but it should be {required_setting_version}.".format(container_id = container.getId(), actual_setting_version = actual_setting_version, required_setting_version = required_setting_version))
|
Logger.log("w", "Instance container {container_id} is outdated. Its setting version is {actual_setting_version} but it should be {required_setting_version}.".format(container_id = container.getId(), actual_setting_version = actual_setting_version, required_setting_version = required_setting_version))
|
||||||
return #Don't add.
|
return # Don't add.
|
||||||
|
|
||||||
super().addContainer(container)
|
super().addContainer(container)
|
||||||
|
|
||||||
|
@ -71,9 +73,9 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
# \param new_name \type{string} Base name, which may not be unique
|
# \param new_name \type{string} Base name, which may not be unique
|
||||||
# \param fallback_name \type{string} Name to use when (stripped) new_name is empty
|
# \param fallback_name \type{string} Name to use when (stripped) new_name is empty
|
||||||
# \return \type{string} Name that is unique for the specified type and name/id
|
# \return \type{string} Name that is unique for the specified type and name/id
|
||||||
def createUniqueName(self, container_type, current_name, new_name, fallback_name):
|
def createUniqueName(self, container_type: str, current_name: str, new_name: str, fallback_name: str) -> str:
|
||||||
new_name = new_name.strip()
|
new_name = new_name.strip()
|
||||||
num_check = re.compile("(.*?)\s*#\d+$").match(new_name)
|
num_check = re.compile(r"(.*?)\s*#\d+$").match(new_name)
|
||||||
if num_check:
|
if num_check:
|
||||||
new_name = num_check.group(1)
|
new_name = num_check.group(1)
|
||||||
if new_name == "":
|
if new_name == "":
|
||||||
|
@ -92,7 +94,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
# Both the id and the name are checked, because they may not be the same and it is better if they are both unique
|
# Both the id and the name are checked, because they may not be the same and it is better if they are both unique
|
||||||
# \param container_type \type{string} Type of the container (machine, quality, ...)
|
# \param container_type \type{string} Type of the container (machine, quality, ...)
|
||||||
# \param container_name \type{string} Name to check
|
# \param container_name \type{string} Name to check
|
||||||
def _containerExists(self, container_type, container_name):
|
def _containerExists(self, container_type: str, container_name: str):
|
||||||
container_class = ContainerStack if container_type == "machine" else InstanceContainer
|
container_class = ContainerStack if container_type == "machine" else InstanceContainer
|
||||||
|
|
||||||
return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \
|
return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \
|
||||||
|
@ -100,11 +102,11 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
|
|
||||||
## Exports an profile to a file
|
## Exports an profile to a file
|
||||||
#
|
#
|
||||||
# \param instance_ids \type{list} the IDs of the profiles to export.
|
# \param container_list \type{list} the containers to export
|
||||||
# \param file_name \type{str} the full path and filename to export to.
|
# \param file_name \type{str} the full path and filename to export to.
|
||||||
# \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
|
# \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
|
||||||
# \return True if the export succeeded, false otherwise.
|
# \return True if the export succeeded, false otherwise.
|
||||||
def exportQualityProfile(self, container_list, file_name, file_type) -> bool:
|
def exportQualityProfile(self, container_list: List[InstanceContainer], file_name: str, file_type: str) -> bool:
|
||||||
# Parse the fileType to deduce what plugin can save the file format.
|
# Parse the fileType to deduce what plugin can save the file format.
|
||||||
# fileType has the format "<description> (*.<extension>)"
|
# fileType has the format "<description> (*.<extension>)"
|
||||||
split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
|
split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
|
||||||
|
@ -126,6 +128,8 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
|
|
||||||
profile_writer = self._findProfileWriter(extension, description)
|
profile_writer = self._findProfileWriter(extension, description)
|
||||||
try:
|
try:
|
||||||
|
if profile_writer is None:
|
||||||
|
raise Exception("Unable to find a profile writer")
|
||||||
success = profile_writer.write(file_name, container_list)
|
success = profile_writer.write(file_name, container_list)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e))
|
Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e))
|
||||||
|
@ -150,7 +154,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
# \param extension
|
# \param extension
|
||||||
# \param description
|
# \param description
|
||||||
# \return The plugin object matching the given extension and description.
|
# \return The plugin object matching the given extension and description.
|
||||||
def _findProfileWriter(self, extension, description):
|
def _findProfileWriter(self, extension: str, description: str) -> Optional[ProfileWriter]:
|
||||||
plugin_registry = PluginRegistry.getInstance()
|
plugin_registry = PluginRegistry.getInstance()
|
||||||
for plugin_id, meta_data in self._getIOPlugins("profile_writer"):
|
for plugin_id, meta_data in self._getIOPlugins("profile_writer"):
|
||||||
for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write.
|
for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write.
|
||||||
|
@ -158,7 +162,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
if supported_extension == extension: # This plugin supports a file type with the same extension.
|
if supported_extension == extension: # This plugin supports a file type with the same extension.
|
||||||
supported_description = supported_type.get("description", None)
|
supported_description = supported_type.get("description", None)
|
||||||
if supported_description == description: # The description is also identical. Assume it's the same file type.
|
if supported_description == description: # The description is also identical. Assume it's the same file type.
|
||||||
return plugin_registry.getPluginObject(plugin_id)
|
return cast(ProfileWriter, plugin_registry.getPluginObject(plugin_id))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
## Imports a profile from a file
|
## Imports a profile from a file
|
||||||
|
@ -324,7 +328,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)}
|
return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)}
|
||||||
|
|
||||||
@override(ContainerRegistry)
|
@override(ContainerRegistry)
|
||||||
def load(self):
|
def load(self) -> None:
|
||||||
super().load()
|
super().load()
|
||||||
self._registerSingleExtrusionMachinesExtruderStacks()
|
self._registerSingleExtrusionMachinesExtruderStacks()
|
||||||
self._connectUpgradedExtruderStacksToMachines()
|
self._connectUpgradedExtruderStacksToMachines()
|
||||||
|
@ -406,7 +410,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack.
|
## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack.
|
||||||
def _convertContainerStack(self, container):
|
def _convertContainerStack(self, container: ContainerStack) -> Union[ExtruderStack.ExtruderStack, GlobalStack.GlobalStack]:
|
||||||
assert type(container) == ContainerStack
|
assert type(container) == ContainerStack
|
||||||
|
|
||||||
container_type = container.getMetaDataEntry("type")
|
container_type = container.getMetaDataEntry("type")
|
||||||
|
@ -430,14 +434,14 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
|
|
||||||
return new_stack
|
return new_stack
|
||||||
|
|
||||||
def _registerSingleExtrusionMachinesExtruderStacks(self):
|
def _registerSingleExtrusionMachinesExtruderStacks(self) -> None:
|
||||||
machines = self.findContainerStacks(type = "machine", machine_extruder_trains = {"0": "fdmextruder"})
|
machines = self.findContainerStacks(type = "machine", machine_extruder_trains = {"0": "fdmextruder"})
|
||||||
for machine in machines:
|
for machine in machines:
|
||||||
extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = machine.getId())
|
extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = machine.getId())
|
||||||
if not extruder_stacks:
|
if not extruder_stacks:
|
||||||
self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder")
|
self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder")
|
||||||
|
|
||||||
def _onContainerAdded(self, container):
|
def _onContainerAdded(self, container: ContainerInterface) -> None:
|
||||||
# We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack
|
# We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack
|
||||||
# for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack
|
# for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack
|
||||||
# is added, we check to see if an extruder stack needs to be added.
|
# is added, we check to see if an extruder stack needs to be added.
|
||||||
|
@ -671,7 +675,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
|
|
||||||
return extruder_stack
|
return extruder_stack
|
||||||
|
|
||||||
def _findQualityChangesContainerInCuraFolder(self, name):
|
def _findQualityChangesContainerInCuraFolder(self, name: str) -> Optional[InstanceContainer]:
|
||||||
quality_changes_dir = Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.QualityChangesInstanceContainer)
|
quality_changes_dir = Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.QualityChangesInstanceContainer)
|
||||||
|
|
||||||
instance_container = None
|
instance_container = None
|
||||||
|
@ -684,7 +688,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
parser = configparser.ConfigParser(interpolation = None)
|
parser = configparser.ConfigParser(interpolation = None)
|
||||||
try:
|
try:
|
||||||
parser.read([file_path])
|
parser.read([file_path])
|
||||||
except:
|
except Exception:
|
||||||
# Skip, it is not a valid stack file
|
# Skip, it is not a valid stack file
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -716,7 +720,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
# due to problems with loading order, some stacks may not have the proper next stack
|
# due to problems with loading order, some stacks may not have the proper next stack
|
||||||
# set after upgrading, because the proper global stack was not yet loaded. This method
|
# set after upgrading, because the proper global stack was not yet loaded. This method
|
||||||
# makes sure those extruders also get the right stack set.
|
# makes sure those extruders also get the right stack set.
|
||||||
def _connectUpgradedExtruderStacksToMachines(self):
|
def _connectUpgradedExtruderStacksToMachines(self) -> None:
|
||||||
extruder_stacks = self.findContainers(container_type = ExtruderStack.ExtruderStack)
|
extruder_stacks = self.findContainers(container_type = ExtruderStack.ExtruderStack)
|
||||||
for extruder_stack in extruder_stacks:
|
for extruder_stack in extruder_stacks:
|
||||||
if extruder_stack.getNextStack():
|
if extruder_stack.getNextStack():
|
||||||
|
|
|
@ -121,8 +121,9 @@ class CuraStackBuilder:
|
||||||
extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0]
|
extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0]
|
||||||
except IndexError as e:
|
except IndexError as e:
|
||||||
# It still needs to break, but we want to know what extruder ID made it break.
|
# It still needs to break, but we want to know what extruder ID made it break.
|
||||||
Logger.log("e", "Unable to find extruder with the id %s", extruder_definition_id)
|
msg = "Unable to find extruder definition with the id [%s]" % extruder_definition_id
|
||||||
raise e
|
Logger.logException("e", msg)
|
||||||
|
raise IndexError(msg)
|
||||||
|
|
||||||
# get material container for extruders
|
# get material container for extruders
|
||||||
material_container = application.empty_material_container
|
material_container = application.empty_material_container
|
||||||
|
|
|
@ -12,7 +12,7 @@ from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Scene.Selection import Selection
|
from UM.Scene.Selection import Selection
|
||||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID.
|
from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID.
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
from UM.Decorators import deprecated
|
||||||
|
|
||||||
from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union
|
from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union
|
||||||
|
|
||||||
|
@ -95,6 +95,7 @@ class ExtruderManager(QObject):
|
||||||
#
|
#
|
||||||
# \param index The index of the extruder whose name to get.
|
# \param index The index of the extruder whose name to get.
|
||||||
@pyqtSlot(int, result = str)
|
@pyqtSlot(int, result = str)
|
||||||
|
@deprecated("Use Cura.MachineManager.activeMachine.extruders[index].name instead", "4.3")
|
||||||
def getExtruderName(self, index: int) -> str:
|
def getExtruderName(self, index: int) -> str:
|
||||||
try:
|
try:
|
||||||
return self.getActiveExtruderStacks()[index].getName()
|
return self.getActiveExtruderStacks()[index].getName()
|
||||||
|
@ -114,7 +115,7 @@ class ExtruderManager(QObject):
|
||||||
selected_nodes = [] # type: List["SceneNode"]
|
selected_nodes = [] # type: List["SceneNode"]
|
||||||
for node in Selection.getAllSelectedObjects():
|
for node in Selection.getAllSelectedObjects():
|
||||||
if node.callDecoration("isGroup"):
|
if node.callDecoration("isGroup"):
|
||||||
for grouped_node in BreadthFirstIterator(node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
for grouped_node in BreadthFirstIterator(node):
|
||||||
if grouped_node.callDecoration("isGroup"):
|
if grouped_node.callDecoration("isGroup"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -131,7 +132,7 @@ class ExtruderManager(QObject):
|
||||||
elif current_extruder_trains:
|
elif current_extruder_trains:
|
||||||
object_extruders.add(current_extruder_trains[0].getId())
|
object_extruders.add(current_extruder_trains[0].getId())
|
||||||
|
|
||||||
self._selected_object_extruders = list(object_extruders) # type: List[Union[str, "ExtruderStack"]]
|
self._selected_object_extruders = list(object_extruders)
|
||||||
|
|
||||||
return self._selected_object_extruders
|
return self._selected_object_extruders
|
||||||
|
|
||||||
|
@ -140,7 +141,7 @@ class ExtruderManager(QObject):
|
||||||
# This will trigger a recalculation of the extruders used for the
|
# This will trigger a recalculation of the extruders used for the
|
||||||
# selection.
|
# selection.
|
||||||
def resetSelectedObjectExtruders(self) -> None:
|
def resetSelectedObjectExtruders(self) -> None:
|
||||||
self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]]
|
self._selected_object_extruders = []
|
||||||
self.selectedObjectExtrudersChanged.emit()
|
self.selectedObjectExtrudersChanged.emit()
|
||||||
|
|
||||||
@pyqtSlot(result = QObject)
|
@pyqtSlot(result = QObject)
|
||||||
|
@ -380,7 +381,13 @@ class ExtruderManager(QObject):
|
||||||
elif extruder_stack_0.definition.getId() != expected_extruder_definition_0_id:
|
elif extruder_stack_0.definition.getId() != expected_extruder_definition_0_id:
|
||||||
Logger.log("e", "Single extruder printer [{printer}] expected extruder [{expected}], but got [{got}]. I'm making it [{expected}].".format(
|
Logger.log("e", "Single extruder printer [{printer}] expected extruder [{expected}], but got [{got}]. I'm making it [{expected}].".format(
|
||||||
printer = global_stack.getId(), expected = expected_extruder_definition_0_id, got = extruder_stack_0.definition.getId()))
|
printer = global_stack.getId(), expected = expected_extruder_definition_0_id, got = extruder_stack_0.definition.getId()))
|
||||||
|
try:
|
||||||
extruder_definition = container_registry.findDefinitionContainers(id = expected_extruder_definition_0_id)[0]
|
extruder_definition = container_registry.findDefinitionContainers(id = expected_extruder_definition_0_id)[0]
|
||||||
|
except IndexError as e:
|
||||||
|
# It still needs to break, but we want to know what extruder ID made it break.
|
||||||
|
msg = "Unable to find extruder definition with the id [%s]" % expected_extruder_definition_0_id
|
||||||
|
Logger.logException("e", msg)
|
||||||
|
raise IndexError(msg)
|
||||||
extruder_stack_0.definition = extruder_definition
|
extruder_stack_0.definition = extruder_definition
|
||||||
|
|
||||||
## Get all extruder values for a certain setting.
|
## Get all extruder values for a certain setting.
|
||||||
|
|
|
@ -118,7 +118,7 @@ class GlobalStack(CuraContainerStack):
|
||||||
## \sa configuredConnectionTypes
|
## \sa configuredConnectionTypes
|
||||||
def removeConfiguredConnectionType(self, connection_type: int) -> None:
|
def removeConfiguredConnectionType(self, connection_type: int) -> None:
|
||||||
configured_connection_types = self.configuredConnectionTypes
|
configured_connection_types = self.configuredConnectionTypes
|
||||||
if connection_type in self.configured_connection_types:
|
if connection_type in configured_connection_types:
|
||||||
# Store the values as a string.
|
# Store the values as a string.
|
||||||
configured_connection_types.remove(connection_type)
|
configured_connection_types.remove(connection_type)
|
||||||
self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types]))
|
self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types]))
|
||||||
|
|
|
@ -22,8 +22,8 @@ from UM.Message import Message
|
||||||
from UM.Settings.SettingFunction import SettingFunction
|
from UM.Settings.SettingFunction import SettingFunction
|
||||||
from UM.Signal import postponeSignals, CompressTechnique
|
from UM.Signal import postponeSignals, CompressTechnique
|
||||||
|
|
||||||
from cura.Machines.MaterialManager import MaterialManager
|
|
||||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch, QualityManager
|
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch, QualityManager
|
||||||
|
from cura.Machines.MaterialManager import MaterialManager
|
||||||
|
|
||||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionType
|
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionType
|
||||||
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||||
|
@ -40,11 +40,10 @@ from .CuraStackBuilder import CuraStackBuilder
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.Settings.CuraContainerStack import CuraContainerStack
|
from cura.Settings.CuraContainerStack import CuraContainerStack
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
|
||||||
from cura.Machines.ContainerNode import ContainerNode
|
from cura.Machines.ContainerNode import ContainerNode
|
||||||
from cura.Machines.QualityChangesGroup import QualityChangesGroup
|
from cura.Machines.QualityChangesGroup import QualityChangesGroup
|
||||||
from cura.Machines.QualityGroup import QualityGroup
|
from cura.Machines.QualityGroup import QualityGroup
|
||||||
|
@ -377,7 +376,7 @@ class MachineManager(QObject):
|
||||||
machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
|
machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
|
||||||
for machine in machines:
|
for machine in machines:
|
||||||
if machine.definition.getId() == definition_id:
|
if machine.definition.getId() == definition_id:
|
||||||
return machine
|
return cast(GlobalStack, machine)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
|
@ -588,7 +587,7 @@ class MachineManager(QObject):
|
||||||
def activeStack(self) -> Optional["ExtruderStack"]:
|
def activeStack(self) -> Optional["ExtruderStack"]:
|
||||||
return self._active_container_stack
|
return self._active_container_stack
|
||||||
|
|
||||||
@pyqtProperty(str, notify=activeMaterialChanged)
|
@pyqtProperty(str, notify = activeMaterialChanged)
|
||||||
def activeMaterialId(self) -> str:
|
def activeMaterialId(self) -> str:
|
||||||
if self._active_container_stack:
|
if self._active_container_stack:
|
||||||
material = self._active_container_stack.material
|
material = self._active_container_stack.material
|
||||||
|
@ -939,7 +938,7 @@ class MachineManager(QObject):
|
||||||
|
|
||||||
# Check to see if any objects are set to print with an extruder that will no longer exist
|
# Check to see if any objects are set to print with an extruder that will no longer exist
|
||||||
root_node = self._application.getController().getScene().getRoot()
|
root_node = self._application.getController().getScene().getRoot()
|
||||||
for node in DepthFirstIterator(root_node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
for node in DepthFirstIterator(root_node):
|
||||||
if node.getMeshData():
|
if node.getMeshData():
|
||||||
extruder_nr = node.callDecoration("getActiveExtruderPosition")
|
extruder_nr = node.callDecoration("getActiveExtruderPosition")
|
||||||
|
|
||||||
|
@ -975,10 +974,13 @@ class MachineManager(QObject):
|
||||||
self._application.globalContainerStackChanged.emit()
|
self._application.globalContainerStackChanged.emit()
|
||||||
self.forceUpdateAllSettings()
|
self.forceUpdateAllSettings()
|
||||||
|
|
||||||
# Note that this function is deprecated, but the decorators for this don't play well together!
|
|
||||||
# @deprecated("use Cura.MachineManager.activeMachine.extruders instead", "4.2")
|
|
||||||
@pyqtSlot(int, result = QObject)
|
@pyqtSlot(int, result = QObject)
|
||||||
def getExtruder(self, position: int) -> Optional[ExtruderStack]:
|
def getExtruder(self, position: int) -> Optional[ExtruderStack]:
|
||||||
|
return self._getExtruder(position)
|
||||||
|
|
||||||
|
# This is a workaround for the deprecated decorator and the pyqtSlot not playing well together.
|
||||||
|
@deprecated("use Cura.MachineManager.activeMachine.extruders instead", "4.2")
|
||||||
|
def _getExtruder(self, position) -> Optional[ExtruderStack]:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return self._global_container_stack.extruders.get(str(position))
|
return self._global_container_stack.extruders.get(str(position))
|
||||||
return None
|
return None
|
||||||
|
@ -1101,9 +1103,17 @@ class MachineManager(QObject):
|
||||||
def _onRootMaterialChanged(self) -> None:
|
def _onRootMaterialChanged(self) -> None:
|
||||||
self._current_root_material_id = {}
|
self._current_root_material_id = {}
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
for position in self._global_container_stack.extruders:
|
for position in self._global_container_stack.extruders:
|
||||||
self._current_root_material_id[position] = self._global_container_stack.extruders[position].material.getMetaDataEntry("base_file")
|
material_id = self._global_container_stack.extruders[position].material.getMetaDataEntry("base_file")
|
||||||
|
if position not in self._current_root_material_id or material_id != self._current_root_material_id[position]:
|
||||||
|
changed = True
|
||||||
|
self._current_root_material_id[position] = material_id
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
self.activeMaterialChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty("QVariant", notify = rootMaterialChanged)
|
@pyqtProperty("QVariant", notify = rootMaterialChanged)
|
||||||
def currentRootMaterialId(self) -> Dict[str, str]:
|
def currentRootMaterialId(self) -> Dict[str, str]:
|
||||||
|
|
|
@ -50,6 +50,7 @@ class ObjectsModel(ListModel):
|
||||||
|
|
||||||
Application.getInstance().getController().getScene().sceneChanged.connect(self._updateSceneDelayed)
|
Application.getInstance().getController().getScene().sceneChanged.connect(self._updateSceneDelayed)
|
||||||
Application.getInstance().getPreferences().preferenceChanged.connect(self._updateDelayed)
|
Application.getInstance().getPreferences().preferenceChanged.connect(self._updateDelayed)
|
||||||
|
Selection.selectionChanged.connect(self._updateDelayed)
|
||||||
|
|
||||||
self._update_timer = QTimer()
|
self._update_timer = QTimer()
|
||||||
self._update_timer.setInterval(200)
|
self._update_timer.setInterval(200)
|
||||||
|
|
|
@ -419,13 +419,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
if parser.has_option("metadata", "enabled"):
|
if parser.has_option("metadata", "enabled"):
|
||||||
extruder_info.enabled = parser["metadata"]["enabled"]
|
extruder_info.enabled = parser["metadata"]["enabled"]
|
||||||
if variant_id not in ("empty", "empty_variant"):
|
if variant_id not in ("empty", "empty_variant"):
|
||||||
|
if variant_id in instance_container_info_dict:
|
||||||
extruder_info.variant_info = instance_container_info_dict[variant_id]
|
extruder_info.variant_info = instance_container_info_dict[variant_id]
|
||||||
|
|
||||||
if material_id not in ("empty", "empty_material"):
|
if material_id not in ("empty", "empty_material"):
|
||||||
root_material_id = reverse_material_id_dict[material_id]
|
root_material_id = reverse_material_id_dict[material_id]
|
||||||
extruder_info.root_material_id = root_material_id
|
extruder_info.root_material_id = root_material_id
|
||||||
|
|
||||||
definition_changes_id = parser["containers"][str(_ContainerIndexes.DefinitionChanges)]
|
definition_changes_id = parser["containers"][str(_ContainerIndexes.DefinitionChanges)]
|
||||||
if definition_changes_id not in ("empty", "empty_definition_changes"):
|
if definition_changes_id not in ("empty", "empty_definition_changes"):
|
||||||
extruder_info.definition_changes_info = instance_container_info_dict[definition_changes_id]
|
extruder_info.definition_changes_info = instance_container_info_dict[definition_changes_id]
|
||||||
|
|
||||||
user_changes_id = parser["containers"][str(_ContainerIndexes.UserChanges)]
|
user_changes_id = parser["containers"][str(_ContainerIndexes.UserChanges)]
|
||||||
if user_changes_id not in ("empty", "empty_user_changes"):
|
if user_changes_id not in ("empty", "empty_user_changes"):
|
||||||
extruder_info.user_changes_info = instance_container_info_dict[user_changes_id]
|
extruder_info.user_changes_info = instance_container_info_dict[user_changes_id]
|
||||||
|
@ -905,6 +909,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
continue
|
continue
|
||||||
extruder_info = self._machine_info.extruder_info_dict[position]
|
extruder_info = self._machine_info.extruder_info_dict[position]
|
||||||
if extruder_info.variant_info is None:
|
if extruder_info.variant_info is None:
|
||||||
|
# If there is no variant_info, try to use the default variant. Otherwise, leave it be.
|
||||||
|
node = variant_manager.getDefaultVariantNode(global_stack.definition, VariantType.NOZZLE, global_stack)
|
||||||
|
if node is not None and node.getContainer() is not None:
|
||||||
|
extruder_stack.variant = node.getContainer()
|
||||||
continue
|
continue
|
||||||
parser = extruder_info.variant_info.parser
|
parser = extruder_info.variant_info.parser
|
||||||
|
|
||||||
|
|
|
@ -369,7 +369,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
|
|
||||||
elif job.getResult() == StartJobResult.ObjectSettingError:
|
elif job.getResult() == StartJobResult.ObjectSettingError:
|
||||||
errors = {}
|
errors = {}
|
||||||
for node in DepthFirstIterator(self._application.getController().getScene().getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
for node in DepthFirstIterator(self._application.getController().getScene().getRoot()):
|
||||||
stack = node.callDecoration("getStack")
|
stack = node.callDecoration("getStack")
|
||||||
if not stack:
|
if not stack:
|
||||||
continue
|
continue
|
||||||
|
@ -438,7 +438,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
|
|
||||||
if not self._application.getPreferences().getValue("general/auto_slice"):
|
if not self._application.getPreferences().getValue("general/auto_slice"):
|
||||||
enable_timer = False
|
enable_timer = False
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
if node.callDecoration("isBlockSlicing"):
|
if node.callDecoration("isBlockSlicing"):
|
||||||
enable_timer = False
|
enable_timer = False
|
||||||
self.setState(BackendState.Disabled)
|
self.setState(BackendState.Disabled)
|
||||||
|
@ -460,7 +460,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
## Return a dict with number of objects per build plate
|
## Return a dict with number of objects per build plate
|
||||||
def _numObjectsPerBuildPlate(self) -> Dict[int, int]:
|
def _numObjectsPerBuildPlate(self) -> Dict[int, int]:
|
||||||
num_objects = defaultdict(int) #type: Dict[int, int]
|
num_objects = defaultdict(int) #type: Dict[int, int]
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
# Only count sliceable objects
|
# Only count sliceable objects
|
||||||
if node.callDecoration("isSliceable"):
|
if node.callDecoration("isSliceable"):
|
||||||
build_plate_number = node.callDecoration("getBuildPlateNumber")
|
build_plate_number = node.callDecoration("getBuildPlateNumber")
|
||||||
|
@ -548,10 +548,11 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
# Clear out any old gcode
|
# Clear out any old gcode
|
||||||
self._scene.gcode_dict = {} # type: ignore
|
self._scene.gcode_dict = {} # type: ignore
|
||||||
|
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
if node.callDecoration("getLayerData"):
|
if node.callDecoration("getLayerData"):
|
||||||
if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers:
|
if not build_plate_numbers or node.callDecoration("getBuildPlateNumber") in build_plate_numbers:
|
||||||
node.getParent().removeChild(node)
|
# We can asume that all nodes have a parent as we're looping through the scene (and filter out root)
|
||||||
|
cast(SceneNode, node.getParent()).removeChild(node)
|
||||||
|
|
||||||
def markSliceAll(self) -> None:
|
def markSliceAll(self) -> None:
|
||||||
for build_plate_number in range(self._application.getMultiBuildPlateModel().maxBuildPlate + 1):
|
for build_plate_number in range(self._application.getMultiBuildPlateModel().maxBuildPlate + 1):
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#Copyright (c) 2017 Ultimaker B.V.
|
#Copyright (c) 2019 Ultimaker B.V.
|
||||||
#Cura is released under the terms of the LGPLv3 or higher.
|
#Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import gc
|
import gc
|
||||||
|
@ -136,23 +136,23 @@ class ProcessSlicedLayersJob(Job):
|
||||||
|
|
||||||
extruder = polygon.extruder
|
extruder = polygon.extruder
|
||||||
|
|
||||||
line_types = numpy.fromstring(polygon.line_type, dtype="u1") # Convert bytearray to numpy array
|
line_types = numpy.fromstring(polygon.line_type, dtype = "u1") # Convert bytearray to numpy array
|
||||||
|
|
||||||
line_types = line_types.reshape((-1,1))
|
line_types = line_types.reshape((-1,1))
|
||||||
|
|
||||||
points = numpy.fromstring(polygon.points, dtype="f4") # Convert bytearray to numpy array
|
points = numpy.fromstring(polygon.points, dtype = "f4") # Convert bytearray to numpy array
|
||||||
if polygon.point_type == 0: # Point2D
|
if polygon.point_type == 0: # Point2D
|
||||||
points = points.reshape((-1,2)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
|
points = points.reshape((-1,2)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
|
||||||
else: # Point3D
|
else: # Point3D
|
||||||
points = points.reshape((-1,3))
|
points = points.reshape((-1,3))
|
||||||
|
|
||||||
line_widths = numpy.fromstring(polygon.line_width, dtype="f4") # Convert bytearray to numpy array
|
line_widths = numpy.fromstring(polygon.line_width, dtype = "f4") # Convert bytearray to numpy array
|
||||||
line_widths = line_widths.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
|
line_widths = line_widths.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
|
||||||
|
|
||||||
line_thicknesses = numpy.fromstring(polygon.line_thickness, dtype="f4") # Convert bytearray to numpy array
|
line_thicknesses = numpy.fromstring(polygon.line_thickness, dtype = "f4") # Convert bytearray to numpy array
|
||||||
line_thicknesses = line_thicknesses.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
|
line_thicknesses = line_thicknesses.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
|
||||||
|
|
||||||
line_feedrates = numpy.fromstring(polygon.line_feedrate, dtype="f4") # Convert bytearray to numpy array
|
line_feedrates = numpy.fromstring(polygon.line_feedrate, dtype = "f4") # Convert bytearray to numpy array
|
||||||
line_feedrates = line_feedrates.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
|
line_feedrates = line_feedrates.reshape((-1,1)) # We get a linear list of pairs that make up the points, so make numpy interpret them correctly.
|
||||||
|
|
||||||
# Create a new 3D-array, copy the 2D points over and insert the right height.
|
# Create a new 3D-array, copy the 2D points over and insert the right height.
|
||||||
|
@ -194,7 +194,7 @@ class ProcessSlicedLayersJob(Job):
|
||||||
manager = ExtruderManager.getInstance()
|
manager = ExtruderManager.getInstance()
|
||||||
extruders = manager.getActiveExtruderStacks()
|
extruders = manager.getActiveExtruderStacks()
|
||||||
if extruders:
|
if extruders:
|
||||||
material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32)
|
material_color_map = numpy.zeros((len(extruders), 4), dtype = numpy.float32)
|
||||||
for extruder in extruders:
|
for extruder in extruders:
|
||||||
position = int(extruder.getMetaDataEntry("position", default = "0"))
|
position = int(extruder.getMetaDataEntry("position", default = "0"))
|
||||||
try:
|
try:
|
||||||
|
@ -206,8 +206,8 @@ class ProcessSlicedLayersJob(Job):
|
||||||
material_color_map[position, :] = color
|
material_color_map[position, :] = color
|
||||||
else:
|
else:
|
||||||
# Single extruder via global stack.
|
# Single extruder via global stack.
|
||||||
material_color_map = numpy.zeros((1, 4), dtype=numpy.float32)
|
material_color_map = numpy.zeros((1, 4), dtype = numpy.float32)
|
||||||
color_code = global_container_stack.material.getMetaDataEntry("color_code", default="#e0e000")
|
color_code = global_container_stack.material.getMetaDataEntry("color_code", default = "#e0e000")
|
||||||
color = colorCodeToRGBA(color_code)
|
color = colorCodeToRGBA(color_code)
|
||||||
material_color_map[0, :] = color
|
material_color_map[0, :] = color
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,7 @@ import Arcus #For typing.
|
||||||
|
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Settings.ContainerStack import ContainerStack #For typing.
|
from UM.Settings.ContainerStack import ContainerStack #For typing.
|
||||||
from UM.Settings.SettingRelation import SettingRelation #For typing.
|
from UM.Settings.SettingRelation import SettingRelation #For typing.
|
||||||
|
|
||||||
|
@ -133,6 +134,14 @@ class StartSliceJob(Job):
|
||||||
self.setResult(StartJobResult.BuildPlateError)
|
self.setResult(StartJobResult.BuildPlateError)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
# Wait for error checker to be done.
|
||||||
|
while CuraApplication.getInstance().getMachineErrorChecker().needToWaitForResult:
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
if CuraApplication.getInstance().getMachineErrorChecker().hasError:
|
||||||
|
self.setResult(StartJobResult.SettingError)
|
||||||
|
return
|
||||||
|
|
||||||
# Don't slice if the buildplate or the nozzle type is incompatible with the materials
|
# Don't slice if the buildplate or the nozzle type is incompatible with the materials
|
||||||
if not CuraApplication.getInstance().getMachineManager().variantBuildplateCompatible and \
|
if not CuraApplication.getInstance().getMachineManager().variantBuildplateCompatible and \
|
||||||
not CuraApplication.getInstance().getMachineManager().variantBuildplateUsable:
|
not CuraApplication.getInstance().getMachineManager().variantBuildplateUsable:
|
||||||
|
@ -150,7 +159,7 @@ class StartSliceJob(Job):
|
||||||
|
|
||||||
|
|
||||||
# Don't slice if there is a per object setting with an error value.
|
# Don't slice if there is a per object setting with an error value.
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
if not isinstance(node, CuraSceneNode) or not node.isSelectable():
|
if not isinstance(node, CuraSceneNode) or not node.isSelectable():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
@ -160,15 +169,16 @@ class StartSliceJob(Job):
|
||||||
|
|
||||||
with self._scene.getSceneLock():
|
with self._scene.getSceneLock():
|
||||||
# Remove old layer data.
|
# Remove old layer data.
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
|
if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
|
||||||
node.getParent().removeChild(node)
|
# Singe we walk through all nodes in the scene, they always have a parent.
|
||||||
|
cast(SceneNode, node.getParent()).removeChild(node)
|
||||||
break
|
break
|
||||||
|
|
||||||
# Get the objects in their groups to print.
|
# Get the objects in their groups to print.
|
||||||
object_groups = []
|
object_groups = []
|
||||||
if stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
if stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
||||||
for node in OneAtATimeIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
for node in OneAtATimeIterator(self._scene.getRoot()):
|
||||||
temp_list = []
|
temp_list = []
|
||||||
|
|
||||||
# Node can't be printed, so don't bother sending it.
|
# Node can't be printed, so don't bother sending it.
|
||||||
|
@ -183,7 +193,8 @@ class StartSliceJob(Job):
|
||||||
children = node.getAllChildren()
|
children = node.getAllChildren()
|
||||||
children.append(node)
|
children.append(node)
|
||||||
for child_node in children:
|
for child_node in children:
|
||||||
if child_node.getMeshData() and child_node.getMeshData().getVertices() is not None:
|
mesh_data = child_node.getMeshData()
|
||||||
|
if mesh_data and mesh_data.getVertices() is not None:
|
||||||
temp_list.append(child_node)
|
temp_list.append(child_node)
|
||||||
|
|
||||||
if temp_list:
|
if temp_list:
|
||||||
|
@ -194,8 +205,9 @@ class StartSliceJob(Job):
|
||||||
else:
|
else:
|
||||||
temp_list = []
|
temp_list = []
|
||||||
has_printing_mesh = False
|
has_printing_mesh = False
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
if node.callDecoration("isSliceable") and node.getMeshData() and node.getMeshData().getVertices() is not None:
|
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"))
|
is_non_printing_mesh = bool(node.callDecoration("isNonPrintingMesh"))
|
||||||
|
|
||||||
# Find a reason not to add the node
|
# Find a reason not to add the node
|
||||||
|
@ -210,7 +222,7 @@ class StartSliceJob(Job):
|
||||||
|
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
||||||
#If the list doesn't have any model with suitable settings then clean the list
|
# If the list doesn't have any model with suitable settings then clean the list
|
||||||
# otherwise CuraEngine will crash
|
# otherwise CuraEngine will crash
|
||||||
if not has_printing_mesh:
|
if not has_printing_mesh:
|
||||||
temp_list.clear()
|
temp_list.clear()
|
||||||
|
@ -261,10 +273,14 @@ class StartSliceJob(Job):
|
||||||
|
|
||||||
for group in filtered_object_groups:
|
for group in filtered_object_groups:
|
||||||
group_message = self._slice_message.addRepeatedMessage("object_lists")
|
group_message = self._slice_message.addRepeatedMessage("object_lists")
|
||||||
if group[0].getParent() is not None and group[0].getParent().callDecoration("isGroup"):
|
parent = group[0].getParent()
|
||||||
self._handlePerObjectSettings(group[0].getParent(), group_message)
|
if parent is not None and parent.callDecoration("isGroup"):
|
||||||
|
self._handlePerObjectSettings(cast(CuraSceneNode, parent), group_message)
|
||||||
|
|
||||||
for object in group:
|
for object in group:
|
||||||
mesh_data = object.getMeshData()
|
mesh_data = object.getMeshData()
|
||||||
|
if mesh_data is None:
|
||||||
|
continue
|
||||||
rot_scale = object.getWorldTransformation().getTransposed().getData()[0:3, 0:3]
|
rot_scale = object.getWorldTransformation().getTransposed().getData()[0:3, 0:3]
|
||||||
translate = object.getWorldTransformation().getData()[:3, 3]
|
translate = object.getWorldTransformation().getData()[:3, 3]
|
||||||
|
|
||||||
|
@ -288,7 +304,7 @@ class StartSliceJob(Job):
|
||||||
|
|
||||||
obj.vertices = flat_verts
|
obj.vertices = flat_verts
|
||||||
|
|
||||||
self._handlePerObjectSettings(object, obj)
|
self._handlePerObjectSettings(cast(CuraSceneNode, object), obj)
|
||||||
|
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ from UM.PluginRegistry import PluginRegistry
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Settings.ContainerFormatError import ContainerFormatError
|
from UM.Settings.ContainerFormatError import ContainerFormatError
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make.
|
from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make.
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.ReaderWriters.ProfileReader import ProfileReader
|
from cura.ReaderWriters.ProfileReader import ProfileReader
|
||||||
|
|
||||||
import zipfile
|
import zipfile
|
||||||
|
@ -67,9 +68,10 @@ class CuraProfileReader(ProfileReader):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
version = int(parser["general"]["version"])
|
version = int(parser["general"]["version"])
|
||||||
|
setting_version = int(parser["metadata"].get("setting_version", "0"))
|
||||||
if InstanceContainer.Version != version:
|
if InstanceContainer.Version != version:
|
||||||
name = parser["general"]["name"]
|
name = parser["general"]["name"]
|
||||||
return self._upgradeProfileVersion(serialized, name, version)
|
return self._upgradeProfileVersion(serialized, name, version, setting_version)
|
||||||
else:
|
else:
|
||||||
return [(serialized, profile_id)]
|
return [(serialized, profile_id)]
|
||||||
|
|
||||||
|
@ -83,7 +85,7 @@ class CuraProfileReader(ProfileReader):
|
||||||
profile = InstanceContainer(profile_id)
|
profile = InstanceContainer(profile_id)
|
||||||
profile.setMetaDataEntry("type", "quality_changes")
|
profile.setMetaDataEntry("type", "quality_changes")
|
||||||
try:
|
try:
|
||||||
profile.deserialize(serialized)
|
profile.deserialize(serialized, file_name = profile_id)
|
||||||
except ContainerFormatError as e:
|
except ContainerFormatError as e:
|
||||||
Logger.log("e", "Error in the format of a container: %s", str(e))
|
Logger.log("e", "Error in the format of a container: %s", str(e))
|
||||||
return None
|
return None
|
||||||
|
@ -98,19 +100,27 @@ class CuraProfileReader(ProfileReader):
|
||||||
# \param profile_id The name of the profile.
|
# \param profile_id The name of the profile.
|
||||||
# \param source_version The profile version of 'serialized'.
|
# \param source_version The profile version of 'serialized'.
|
||||||
# \return List of serialized profile strings and matching profile names.
|
# \return List of serialized profile strings and matching profile names.
|
||||||
def _upgradeProfileVersion(self, serialized: str, profile_id: str, source_version: int) -> List[Tuple[str, str]]:
|
def _upgradeProfileVersion(self, serialized: str, profile_id: str, main_version: int, setting_version: int) -> List[Tuple[str, str]]:
|
||||||
converter_plugins = PluginRegistry.getInstance().getAllMetaData(filter = {"version_upgrade": {} }, active_only = True)
|
source_version = main_version * 1000000 + setting_version
|
||||||
|
|
||||||
source_format = ("profile", source_version)
|
from UM.VersionUpgradeManager import VersionUpgradeManager
|
||||||
profile_convert_funcs = [plugin["version_upgrade"][source_format][2] for plugin in converter_plugins
|
results = VersionUpgradeManager.getInstance().updateFilesData("quality_changes", source_version, [serialized], [profile_id])
|
||||||
if source_format in plugin["version_upgrade"] and plugin["version_upgrade"][source_format][1] == InstanceContainer.Version]
|
if results is None:
|
||||||
|
|
||||||
if not profile_convert_funcs:
|
|
||||||
Logger.log("w", "Unable to find an upgrade path for the profile [%s]", profile_id)
|
|
||||||
return []
|
return []
|
||||||
|
|
||||||
filenames, outputs = profile_convert_funcs[0](serialized, profile_id)
|
serialized = results.files_data[0]
|
||||||
if filenames is None and outputs is None:
|
|
||||||
Logger.log("w", "The conversion failed to return any usable data for [%s]", profile_id)
|
parser = configparser.ConfigParser(interpolation = None)
|
||||||
|
parser.read_string(serialized)
|
||||||
|
if "general" not in parser:
|
||||||
|
Logger.log("w", "Missing required section 'general'.")
|
||||||
return []
|
return []
|
||||||
return list(zip(outputs, filenames))
|
|
||||||
|
new_source_version = results.version
|
||||||
|
if int(new_source_version / 1000000) != InstanceContainer.Version or new_source_version % 1000000 != CuraApplication.SettingVersion:
|
||||||
|
Logger.log("e", "Failed to upgrade profile [%s]", profile_id)
|
||||||
|
|
||||||
|
if int(parser["general"]["version"]) != InstanceContainer.Version:
|
||||||
|
Logger.log("e", "Failed to upgrade profile [%s]", profile_id)
|
||||||
|
return []
|
||||||
|
return [(serialized, profile_id)]
|
||||||
|
|
|
@ -97,7 +97,7 @@ Rectangle
|
||||||
horizontalCenter: parent.horizontalCenter
|
horizontalCenter: parent.horizontalCenter
|
||||||
}
|
}
|
||||||
visible: isNetworkConfigured && !isConnected
|
visible: isNetworkConfigured && !isConnected
|
||||||
text: catalog.i18nc("@info", "Please make sure your printer has a connection:\n- Check if the printer is turned on.\n- Check if the printer is connected to the network.")
|
text: catalog.i18nc("@info", "Please make sure your printer has a connection:\n- Check if the printer is turned on.\n- Check if the printer is connected to the network.\n- Check if you are signed in to discover cloud-connected printers.")
|
||||||
font: UM.Theme.getFont("medium")
|
font: UM.Theme.getFont("medium")
|
||||||
color: UM.Theme.getColor("monitor_text_primary")
|
color: UM.Theme.getColor("monitor_text_primary")
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
|
|
@ -29,6 +29,17 @@ UM.TooltipArea
|
||||||
UM.ActiveTool.forceUpdate();
|
UM.ActiveTool.forceUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the user removes settings from the list addedSettingsModel, we need to recheck if the
|
||||||
|
// setting is visible or not to show a mark in the CheckBox.
|
||||||
|
Connections
|
||||||
|
{
|
||||||
|
target: addedSettingsModel
|
||||||
|
onVisibleCountChanged:
|
||||||
|
{
|
||||||
|
check.checked = addedSettingsModel.getVisible(model.key)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,13 @@
|
||||||
# Cura PostProcessingPlugin
|
# Cura PostProcessingPlugin
|
||||||
# Author: Amanda de Castilho
|
# Author: Amanda de Castilho
|
||||||
# Date: August 28, 2018
|
# Date: August 28, 2018
|
||||||
|
# Modified: November 16, 2018 by Joshua Pope-Lewis
|
||||||
|
|
||||||
# Description: This plugin inserts a line at the start of each layer,
|
# Description: This plugin shows custom messages about your print on the Status bar...
|
||||||
# M117 - displays the filename and layer height to the LCD
|
# Please look at the 3 options
|
||||||
# Alternatively, user can override the filename to display alt text + layer height
|
# - Scolling (SCROLL_LONG_FILENAMES) if enabled in Marlin and you arent printing a small item select this option.
|
||||||
|
# - Name: By default it will use the name generated by Cura (EG: TT_Test_Cube) - Type a custom name in here
|
||||||
|
# - Max Layer: Enabling this will show how many layers are in the entire print (EG: Layer 1 of 265!)
|
||||||
|
|
||||||
from ..Script import Script
|
from ..Script import Script
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
@ -15,35 +18,72 @@ class DisplayFilenameAndLayerOnLCD(Script):
|
||||||
|
|
||||||
def getSettingDataString(self):
|
def getSettingDataString(self):
|
||||||
return """{
|
return """{
|
||||||
"name": "Display filename and layer on LCD",
|
"name": "Display Filename And Layer On LCD",
|
||||||
"key": "DisplayFilenameAndLayerOnLCD",
|
"key": "DisplayFilenameAndLayerOnLCD",
|
||||||
"metadata": {},
|
"metadata": {},
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"settings":
|
"settings":
|
||||||
{
|
{
|
||||||
|
"scroll":
|
||||||
|
{
|
||||||
|
"label": "Scroll enabled/Small layers?",
|
||||||
|
"description": "If SCROLL_LONG_FILENAMES is enabled select this setting however, if the model is small disable this setting!",
|
||||||
|
"type": "bool",
|
||||||
|
"default_value": false
|
||||||
|
},
|
||||||
"name":
|
"name":
|
||||||
{
|
{
|
||||||
"label": "text to display:",
|
"label": "Text to display:",
|
||||||
"description": "By default the current filename will be displayed on the LCD. Enter text here to override the filename and display something else.",
|
"description": "By default the current filename will be displayed on the LCD. Enter text here to override the filename and display something else.",
|
||||||
"type": "str",
|
"type": "str",
|
||||||
"default_value": ""
|
"default_value": ""
|
||||||
|
},
|
||||||
|
"startNum":
|
||||||
|
{
|
||||||
|
"label": "Initial layer number:",
|
||||||
|
"description": "Choose which number you prefer for the initial layer, 0 or 1",
|
||||||
|
"type": "int",
|
||||||
|
"default_value": 0,
|
||||||
|
"minimum_value": 0,
|
||||||
|
"maximum_value": 1
|
||||||
|
},
|
||||||
|
"maxlayer":
|
||||||
|
{
|
||||||
|
"label": "Display max layer?:",
|
||||||
|
"description": "Display how many layers are in the entire print on status bar?",
|
||||||
|
"type": "bool",
|
||||||
|
"default_value": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}"""
|
}"""
|
||||||
|
|
||||||
def execute(self, data):
|
def execute(self, data):
|
||||||
|
max_layer = 0
|
||||||
if self.getSettingValueByKey("name") != "":
|
if self.getSettingValueByKey("name") != "":
|
||||||
name = self.getSettingValueByKey("name")
|
name = self.getSettingValueByKey("name")
|
||||||
else:
|
else:
|
||||||
name = Application.getInstance().getPrintInformation().jobName
|
name = Application.getInstance().getPrintInformation().jobName
|
||||||
lcd_text = "M117 " + name + " layer "
|
if not self.getSettingValueByKey("scroll"):
|
||||||
i = 0
|
if self.getSettingValueByKey("maxlayer"):
|
||||||
|
lcd_text = "M117 Layer "
|
||||||
|
else:
|
||||||
|
lcd_text = "M117 Printing Layer "
|
||||||
|
else:
|
||||||
|
lcd_text = "M117 Printing " + name + " - Layer "
|
||||||
|
i = self.getSettingValueByKey("startNum")
|
||||||
for layer in data:
|
for layer in data:
|
||||||
display_text = lcd_text + str(i)
|
display_text = lcd_text + str(i) + " " + name
|
||||||
layer_index = data.index(layer)
|
layer_index = data.index(layer)
|
||||||
lines = layer.split("\n")
|
lines = layer.split("\n")
|
||||||
for line in lines:
|
for line in lines:
|
||||||
|
if line.startswith(";LAYER_COUNT:"):
|
||||||
|
max_layer = line
|
||||||
|
max_layer = max_layer.split(":")[1]
|
||||||
if line.startswith(";LAYER:"):
|
if line.startswith(";LAYER:"):
|
||||||
|
if self.getSettingValueByKey("maxlayer"):
|
||||||
|
display_text = display_text + " of " + max_layer
|
||||||
|
else:
|
||||||
|
display_text = display_text + "!"
|
||||||
line_index = lines.index(line)
|
line_index = lines.index(line)
|
||||||
lines.insert(line_index + 1, display_text)
|
lines.insert(line_index + 1, display_text)
|
||||||
i += 1
|
i += 1
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from ..Script import Script
|
from ..Script import Script
|
||||||
|
|
||||||
from UM.Application import Application #To get the current printer's settings.
|
from UM.Application import Application #To get the current printer's settings.
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
class PauseAtHeight(Script):
|
class PauseAtHeight(Script):
|
||||||
def __init__(self):
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
def getSettingDataString(self):
|
def getSettingDataString(self) -> str:
|
||||||
return """{
|
return """{
|
||||||
"name": "Pause at height",
|
"name": "Pause at height",
|
||||||
"key": "PauseAtHeight",
|
"key": "PauseAtHeight",
|
||||||
|
@ -113,11 +114,9 @@ class PauseAtHeight(Script):
|
||||||
}
|
}
|
||||||
}"""
|
}"""
|
||||||
|
|
||||||
def getNextXY(self, layer: str):
|
## Get the X and Y values for a layer (will be used to get X and Y of the
|
||||||
"""
|
# layer after the pause).
|
||||||
Get the X and Y values for a layer (will be used to get X and Y of
|
def getNextXY(self, layer: str) -> Tuple[float, float]:
|
||||||
the layer after the pause
|
|
||||||
"""
|
|
||||||
lines = layer.split("\n")
|
lines = layer.split("\n")
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if self.getValue(line, "X") is not None and self.getValue(line, "Y") is not None:
|
if self.getValue(line, "X") is not None and self.getValue(line, "Y") is not None:
|
||||||
|
@ -126,8 +125,10 @@ class PauseAtHeight(Script):
|
||||||
return x, y
|
return x, y
|
||||||
return 0, 0
|
return 0, 0
|
||||||
|
|
||||||
def execute(self, data: list):
|
## Inserts the pause commands.
|
||||||
"""data is a list. Each index contains a layer"""
|
# \param data: List of layers.
|
||||||
|
# \return New list of layers.
|
||||||
|
def execute(self, data: List[str]) -> List[str]:
|
||||||
pause_at = self.getSettingValueByKey("pause_at")
|
pause_at = self.getSettingValueByKey("pause_at")
|
||||||
pause_height = self.getSettingValueByKey("pause_height")
|
pause_height = self.getSettingValueByKey("pause_height")
|
||||||
pause_layer = self.getSettingValueByKey("pause_layer")
|
pause_layer = self.getSettingValueByKey("pause_layer")
|
||||||
|
|
|
@ -128,7 +128,24 @@ class Stretcher():
|
||||||
onestep = GCodeStep(0, in_relative_movement)
|
onestep = GCodeStep(0, in_relative_movement)
|
||||||
onestep.copyPosFrom(current)
|
onestep.copyPosFrom(current)
|
||||||
elif _getValue(line, "G") == 1:
|
elif _getValue(line, "G") == 1:
|
||||||
|
last_x = current.step_x
|
||||||
|
last_y = current.step_y
|
||||||
|
last_z = current.step_z
|
||||||
|
last_e = current.step_e
|
||||||
current.readStep(line)
|
current.readStep(line)
|
||||||
|
if (current.step_x == last_x and current.step_y == last_y and
|
||||||
|
current.step_z == last_z and current.step_e != last_e
|
||||||
|
):
|
||||||
|
# It's an extruder only move. Preserve it rather than process it as an
|
||||||
|
# extruded move. Otherwise, the stretched output might contain slight
|
||||||
|
# motion in X and Y in addition to E. This can cause problems with
|
||||||
|
# firmwares that implement pressure advance.
|
||||||
|
onestep = GCodeStep(-1, in_relative_movement)
|
||||||
|
onestep.copyPosFrom(current)
|
||||||
|
# Rather than copy the original line, write a new one with consistent
|
||||||
|
# extruder coordinates
|
||||||
|
onestep.comment = "G1 F{} E{}".format(onestep.step_f, onestep.step_e)
|
||||||
|
else:
|
||||||
onestep = GCodeStep(1, in_relative_movement)
|
onestep = GCodeStep(1, in_relative_movement)
|
||||||
onestep.copyPosFrom(current)
|
onestep.copyPosFrom(current)
|
||||||
|
|
||||||
|
|
|
@ -218,10 +218,10 @@ class SimulationView(CuraView):
|
||||||
if theme is not None:
|
if theme is not None:
|
||||||
self._ghost_shader.setUniformValue("u_color", Color(*theme.getColor("layerview_ghost").getRgb()))
|
self._ghost_shader.setUniformValue("u_color", Color(*theme.getColor("layerview_ghost").getRgb()))
|
||||||
|
|
||||||
for node in DepthFirstIterator(scene.getRoot()): # type: ignore
|
for node in DepthFirstIterator(scene.getRoot()):
|
||||||
# We do not want to render ConvexHullNode as it conflicts with the bottom layers.
|
# We do not want to render ConvexHullNode as it conflicts with the bottom layers.
|
||||||
# However, it is somewhat relevant when the node is selected, so do render it then.
|
# However, it is somewhat relevant when the node is selected, so do render it then.
|
||||||
if type(node) is ConvexHullNode and not Selection.isSelected(node.getWatchedNode()):
|
if type(node) is ConvexHullNode and not Selection.isSelected(cast(ConvexHullNode, node).getWatchedNode()):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not node.render(renderer):
|
if not node.render(renderer):
|
||||||
|
@ -572,14 +572,14 @@ class SimulationView(CuraView):
|
||||||
self._current_layer_jumps = job.getResult().get("jumps")
|
self._current_layer_jumps = job.getResult().get("jumps")
|
||||||
self._controller.getScene().sceneChanged.emit(self._controller.getScene().getRoot())
|
self._controller.getScene().sceneChanged.emit(self._controller.getScene().getRoot())
|
||||||
|
|
||||||
self._top_layers_job = None # type: Optional["_CreateTopLayersJob"]
|
self._top_layers_job = None
|
||||||
|
|
||||||
def _updateWithPreferences(self) -> None:
|
def _updateWithPreferences(self) -> None:
|
||||||
self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count"))
|
self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count"))
|
||||||
self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers"))
|
self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers"))
|
||||||
self._compatibility_mode = self._evaluateCompatibilityMode()
|
self._compatibility_mode = self._evaluateCompatibilityMode()
|
||||||
|
|
||||||
self.setSimulationViewType(int(float(Application.getInstance().getPreferences().getValue("layerview/layer_view_type"))));
|
self.setSimulationViewType(int(float(Application.getInstance().getPreferences().getValue("layerview/layer_view_type"))))
|
||||||
|
|
||||||
for extruder_nr, extruder_opacity in enumerate(Application.getInstance().getPreferences().getValue("layerview/extruder_opacities").split("|")):
|
for extruder_nr, extruder_opacity in enumerate(Application.getInstance().getPreferences().getValue("layerview/extruder_opacities").split("|")):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -126,6 +126,10 @@ class SliceInfo(QObject, Extension):
|
||||||
else:
|
else:
|
||||||
data["active_mode"] = "custom"
|
data["active_mode"] = "custom"
|
||||||
|
|
||||||
|
data["camera_view"] = application.getPreferences().getValue("general/camera_perspective_mode")
|
||||||
|
if data["camera_view"] == "orthographic":
|
||||||
|
data["camera_view"] = "orthogonal" #The database still only recognises the old name "orthogonal".
|
||||||
|
|
||||||
definition_changes = global_stack.definitionChanges
|
definition_changes = global_stack.definitionChanges
|
||||||
machine_settings_changed_by_user = False
|
machine_settings_changed_by_user = False
|
||||||
if definition_changes.getId() != "empty":
|
if definition_changes.getId() != "empty":
|
||||||
|
|
|
@ -8,6 +8,8 @@ import ssl
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import urllib.error
|
import urllib.error
|
||||||
|
|
||||||
|
import certifi
|
||||||
|
|
||||||
|
|
||||||
class SliceInfoJob(Job):
|
class SliceInfoJob(Job):
|
||||||
def __init__(self, url, data):
|
def __init__(self, url, data):
|
||||||
|
@ -20,11 +22,14 @@ class SliceInfoJob(Job):
|
||||||
Logger.log("e", "URL or DATA for sending slice info was not set!")
|
Logger.log("e", "URL or DATA for sending slice info was not set!")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Submit data
|
# CURA-6698 Create an SSL context and use certifi CA certificates for verification.
|
||||||
kwoptions = {"data" : self._data, "timeout" : 5}
|
context = ssl.SSLContext(protocol = ssl.PROTOCOL_TLSv1_2)
|
||||||
|
context.load_verify_locations(cafile = certifi.where())
|
||||||
|
|
||||||
if Platform.isOSX():
|
# Submit data
|
||||||
kwoptions["context"] = ssl._create_unverified_context()
|
kwoptions = {"data": self._data,
|
||||||
|
"timeout": 5,
|
||||||
|
"context": context}
|
||||||
|
|
||||||
Logger.log("i", "Sending anonymous slice info to [%s]...", self._url)
|
Logger.log("i", "Sending anonymous slice info to [%s]...", self._url)
|
||||||
|
|
||||||
|
|
|
@ -48,32 +48,32 @@ Window
|
||||||
ToolboxLoadingPage
|
ToolboxLoadingPage
|
||||||
{
|
{
|
||||||
id: viewLoading
|
id: viewLoading
|
||||||
visible: toolbox.viewCategory != "installed" && toolbox.viewPage == "loading"
|
visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "loading"
|
||||||
}
|
}
|
||||||
ToolboxErrorPage
|
ToolboxErrorPage
|
||||||
{
|
{
|
||||||
id: viewErrored
|
id: viewErrored
|
||||||
visible: toolbox.viewCategory != "installed" && toolbox.viewPage == "errored"
|
visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "errored"
|
||||||
}
|
}
|
||||||
ToolboxDownloadsPage
|
ToolboxDownloadsPage
|
||||||
{
|
{
|
||||||
id: viewDownloads
|
id: viewDownloads
|
||||||
visible: toolbox.viewCategory != "installed" && toolbox.viewPage == "overview"
|
visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "overview"
|
||||||
}
|
}
|
||||||
ToolboxDetailPage
|
ToolboxDetailPage
|
||||||
{
|
{
|
||||||
id: viewDetail
|
id: viewDetail
|
||||||
visible: toolbox.viewCategory != "installed" && toolbox.viewPage == "detail"
|
visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "detail"
|
||||||
}
|
}
|
||||||
ToolboxAuthorPage
|
ToolboxAuthorPage
|
||||||
{
|
{
|
||||||
id: viewAuthor
|
id: viewAuthor
|
||||||
visible: toolbox.viewCategory != "installed" && toolbox.viewPage == "author"
|
visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "author"
|
||||||
}
|
}
|
||||||
ToolboxInstalledPage
|
ToolboxInstalledPage
|
||||||
{
|
{
|
||||||
id: installedPluginList
|
id: installedPluginList
|
||||||
visible: toolbox.viewCategory == "installed"
|
visible: toolbox.viewCategory === "installed"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Toolbox is released under the terms of the LGPLv3 or higher.
|
// Toolbox is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.10
|
import QtQuick 2.10
|
||||||
import QtQuick.Controls 1.4
|
import QtQuick.Controls 1.4
|
||||||
import QtQuick.Controls.Styles 1.4
|
|
||||||
import UM 1.1 as UM
|
import UM 1.1 as UM
|
||||||
|
|
||||||
Item
|
Item
|
||||||
|
@ -11,48 +11,17 @@ Item
|
||||||
id: base
|
id: base
|
||||||
|
|
||||||
property var packageData
|
property var packageData
|
||||||
property var technicalDataSheetUrl:
|
property var technicalDataSheetUrl: packageData.links.technicalDataSheet
|
||||||
{
|
property var safetyDataSheetUrl: packageData.links.safetyDataSheet
|
||||||
var link = undefined
|
property var printingGuidelinesUrl: packageData.links.printingGuidelines
|
||||||
if ("Technical Data Sheet" in packageData.links)
|
property var materialWebsiteUrl: packageData.links.website
|
||||||
{
|
|
||||||
// HACK: This is the way the old API (used in 3.6-beta) used to do it. For safety it's still here,
|
|
||||||
// but it can be removed over time.
|
|
||||||
link = packageData.links["Technical Data Sheet"]
|
|
||||||
}
|
|
||||||
else if ("technicalDataSheet" in packageData.links)
|
|
||||||
{
|
|
||||||
link = packageData.links["technicalDataSheet"]
|
|
||||||
}
|
|
||||||
return link
|
|
||||||
}
|
|
||||||
property var safetyDataSheetUrl:
|
|
||||||
{
|
|
||||||
var sds_name = "safetyDataSheet"
|
|
||||||
return (sds_name in packageData.links) ? packageData.links[sds_name] : undefined
|
|
||||||
}
|
|
||||||
property var printingGuidelinesUrl:
|
|
||||||
{
|
|
||||||
var pg_name = "printingGuidelines"
|
|
||||||
return (pg_name in packageData.links) ? packageData.links[pg_name] : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
property var materialWebsiteUrl:
|
height: childrenRect.height
|
||||||
{
|
onVisibleChanged: packageData.type === "material" && (compatibilityItem.visible || dataSheetLinks.visible)
|
||||||
var pg_name = "website"
|
|
||||||
return (pg_name in packageData.links) ? packageData.links[pg_name] : undefined
|
|
||||||
}
|
|
||||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
|
||||||
height: visible ? childrenRect.height : 0
|
|
||||||
|
|
||||||
visible: packageData.type == "material" &&
|
Column
|
||||||
(packageData.has_configs || technicalDataSheetUrl !== undefined ||
|
|
||||||
safetyDataSheetUrl !== undefined || printingGuidelinesUrl !== undefined ||
|
|
||||||
materialWebsiteUrl !== undefined)
|
|
||||||
|
|
||||||
Item
|
|
||||||
{
|
{
|
||||||
id: combatibilityItem
|
id: compatibilityItem
|
||||||
visible: packageData.has_configs
|
visible: packageData.has_configs
|
||||||
width: parent.width
|
width: parent.width
|
||||||
// This is a bit of a hack, but the whole QML is pretty messy right now. This needs a big overhaul.
|
// This is a bit of a hack, but the whole QML is pretty messy right now. This needs a big overhaul.
|
||||||
|
@ -61,7 +30,6 @@ Item
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
id: heading
|
id: heading
|
||||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: catalog.i18nc("@label", "Compatibility")
|
text: catalog.i18nc("@label", "Compatibility")
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
@ -73,8 +41,6 @@ Item
|
||||||
TableView
|
TableView
|
||||||
{
|
{
|
||||||
id: table
|
id: table
|
||||||
anchors.top: heading.bottom
|
|
||||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
frameVisible: false
|
frameVisible: false
|
||||||
|
|
||||||
|
@ -155,32 +121,32 @@ Item
|
||||||
TableViewColumn
|
TableViewColumn
|
||||||
{
|
{
|
||||||
role: "machine"
|
role: "machine"
|
||||||
title: "Machine"
|
title: catalog.i18nc("@label:table_header", "Machine")
|
||||||
width: Math.floor(table.width * 0.25)
|
width: Math.floor(table.width * 0.25)
|
||||||
delegate: columnTextDelegate
|
delegate: columnTextDelegate
|
||||||
}
|
}
|
||||||
TableViewColumn
|
TableViewColumn
|
||||||
{
|
{
|
||||||
role: "print_core"
|
role: "print_core"
|
||||||
title: "Print Core"
|
title: catalog.i18nc("@label:table_header", "Print Core")
|
||||||
width: Math.floor(table.width * 0.2)
|
width: Math.floor(table.width * 0.2)
|
||||||
}
|
}
|
||||||
TableViewColumn
|
TableViewColumn
|
||||||
{
|
{
|
||||||
role: "build_plate"
|
role: "build_plate"
|
||||||
title: "Build Plate"
|
title: catalog.i18nc("@label:table_header", "Build Plate")
|
||||||
width: Math.floor(table.width * 0.225)
|
width: Math.floor(table.width * 0.225)
|
||||||
}
|
}
|
||||||
TableViewColumn
|
TableViewColumn
|
||||||
{
|
{
|
||||||
role: "support_material"
|
role: "support_material"
|
||||||
title: "Support"
|
title: catalog.i18nc("@label:table_header", "Support")
|
||||||
width: Math.floor(table.width * 0.225)
|
width: Math.floor(table.width * 0.225)
|
||||||
}
|
}
|
||||||
TableViewColumn
|
TableViewColumn
|
||||||
{
|
{
|
||||||
role: "quality"
|
role: "quality"
|
||||||
title: "Quality"
|
title: catalog.i18nc("@label:table_header", "Quality")
|
||||||
width: Math.floor(table.width * 0.1)
|
width: Math.floor(table.width * 0.1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -188,13 +154,14 @@ Item
|
||||||
|
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
id: data_sheet_links
|
id: dataSheetLinks
|
||||||
anchors.top: combatibilityItem.bottom
|
anchors.top: compatibilityItem.bottom
|
||||||
anchors.topMargin: UM.Theme.getSize("default_margin").height / 2
|
anchors.topMargin: UM.Theme.getSize("narrow_margin").height
|
||||||
visible: base.technicalDataSheetUrl !== undefined ||
|
visible: base.technicalDataSheetUrl !== undefined ||
|
||||||
base.safetyDataSheetUrl !== undefined || base.printingGuidelinesUrl !== undefined ||
|
base.safetyDataSheetUrl !== undefined ||
|
||||||
|
base.printingGuidelinesUrl !== undefined ||
|
||||||
base.materialWebsiteUrl !== undefined
|
base.materialWebsiteUrl !== undefined
|
||||||
height: visible ? contentHeight : 0
|
|
||||||
text:
|
text:
|
||||||
{
|
{
|
||||||
var result = ""
|
var result = ""
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Toolbox is released under the terms of the LGPLv3 or higher.
|
// Toolbox is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.7
|
import QtQuick 2.10
|
||||||
import QtQuick.Controls 1.4
|
import QtQuick.Controls 2.3
|
||||||
import QtQuick.Controls.Styles 1.4
|
|
||||||
import UM 1.1 as UM
|
import UM 1.1 as UM
|
||||||
|
|
||||||
Item
|
Item
|
||||||
|
@ -11,10 +10,9 @@ Item
|
||||||
id: detailList
|
id: detailList
|
||||||
ScrollView
|
ScrollView
|
||||||
{
|
{
|
||||||
frameVisible: false
|
clip: true
|
||||||
anchors.fill: detailList
|
anchors.fill: detailList
|
||||||
style: UM.Theme.styles.scrollview
|
|
||||||
flickableItem.flickableDirection: Flickable.VerticalFlick
|
|
||||||
Column
|
Column
|
||||||
{
|
{
|
||||||
anchors
|
anchors
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Toolbox is released under the terms of the LGPLv3 or higher.
|
// Toolbox is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.10
|
import QtQuick 2.10
|
||||||
import QtQuick.Controls 1.4
|
import QtQuick.Controls 2.3
|
||||||
import QtQuick.Controls.Styles 1.4
|
|
||||||
import UM 1.1 as UM
|
import UM 1.1 as UM
|
||||||
|
|
||||||
Item
|
Item
|
||||||
{
|
{
|
||||||
id: tile
|
id: tile
|
||||||
width: detailList.width - UM.Theme.getSize("wide_margin").width
|
width: detailList.width - UM.Theme.getSize("wide_margin").width
|
||||||
height: normalData.height + compatibilityChart.height + 4 * UM.Theme.getSize("default_margin").height
|
height: normalData.height + 2 * UM.Theme.getSize("wide_margin").height
|
||||||
Item
|
Column
|
||||||
{
|
{
|
||||||
id: normalData
|
id: normalData
|
||||||
height: childrenRect.height
|
|
||||||
anchors
|
anchors
|
||||||
{
|
{
|
||||||
|
top: parent.top
|
||||||
left: parent.left
|
left: parent.left
|
||||||
right: controls.left
|
right: controls.left
|
||||||
rightMargin: UM.Theme.getSize("default_margin").width * 2 + UM.Theme.getSize("toolbox_loader").width
|
rightMargin: UM.Theme.getSize("wide_margin").width
|
||||||
top: parent.top
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
id: packageName
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: UM.Theme.getSize("toolbox_property_label").height
|
height: UM.Theme.getSize("toolbox_property_label").height
|
||||||
text: model.name
|
text: model.name
|
||||||
|
@ -33,9 +33,9 @@ Item
|
||||||
font: UM.Theme.getFont("medium_bold")
|
font: UM.Theme.getFont("medium_bold")
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
anchors.top: packageName.bottom
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
text: model.description
|
text: model.description
|
||||||
maximumLineCount: 25
|
maximumLineCount: 25
|
||||||
|
@ -45,6 +45,12 @@ Item
|
||||||
font: UM.Theme.getFont("default")
|
font: UM.Theme.getFont("default")
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ToolboxCompatibilityChart
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
packageData: model
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolboxDetailTileActions
|
ToolboxDetailTileActions
|
||||||
|
@ -57,20 +63,12 @@ Item
|
||||||
packageData: model
|
packageData: model
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolboxCompatibilityChart
|
|
||||||
{
|
|
||||||
id: compatibilityChart
|
|
||||||
anchors.top: normalData.bottom
|
|
||||||
width: normalData.width
|
|
||||||
packageData: model
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle
|
Rectangle
|
||||||
{
|
{
|
||||||
color: UM.Theme.getColor("lining")
|
color: UM.Theme.getColor("lining")
|
||||||
width: tile.width
|
width: tile.width
|
||||||
height: UM.Theme.getSize("default_lining").height
|
height: UM.Theme.getSize("default_lining").height
|
||||||
anchors.top: compatibilityChart.bottom
|
anchors.top: normalData.bottom
|
||||||
anchors.topMargin: UM.Theme.getSize("default_margin").height + UM.Theme.getSize("wide_margin").height //Normal margin for spacing after chart, wide margin between items.
|
anchors.topMargin: UM.Theme.getSize("default_margin").height + UM.Theme.getSize("wide_margin").height //Normal margin for spacing after chart, wide margin between items.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,7 @@ Column
|
||||||
// Don't allow installing while another download is running
|
// Don't allow installing while another download is running
|
||||||
enabled: installed || (!(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired)
|
enabled: installed || (!(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired)
|
||||||
opacity: enabled ? 1.0 : 0.5
|
opacity: enabled ? 1.0 : 0.5
|
||||||
visible: !updateButton.visible && !installed// Don't show when the update button is visible
|
visible: !updateButton.visible && !installed // Don't show when the update button is visible
|
||||||
}
|
}
|
||||||
|
|
||||||
Cura.SecondaryButton
|
Cura.SecondaryButton
|
||||||
|
|
|
@ -2,9 +2,7 @@
|
||||||
// Toolbox is released under the terms of the LGPLv3 or higher.
|
// Toolbox is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.10
|
import QtQuick 2.10
|
||||||
import QtQuick.Controls 1.4
|
import QtQuick.Controls 2.3
|
||||||
import QtQuick.Controls.Styles 1.4
|
|
||||||
import QtQuick.Layouts 1.3
|
|
||||||
import UM 1.1 as UM
|
import UM 1.1 as UM
|
||||||
|
|
||||||
Column
|
Column
|
||||||
|
|
|
@ -1,25 +1,20 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Toolbox is released under the terms of the LGPLv3 or higher.
|
// Toolbox is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.7
|
import QtQuick 2.10
|
||||||
import QtQuick.Controls 1.4
|
import QtQuick.Controls 2.3
|
||||||
import QtQuick.Controls.Styles 1.4
|
|
||||||
import UM 1.1 as UM
|
import UM 1.1 as UM
|
||||||
|
|
||||||
ScrollView
|
ScrollView
|
||||||
{
|
{
|
||||||
frameVisible: false
|
clip: true
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height
|
height: parent.height
|
||||||
style: UM.Theme.styles.scrollview
|
|
||||||
|
|
||||||
flickableItem.flickableDirection: Flickable.VerticalFlick
|
|
||||||
|
|
||||||
Column
|
Column
|
||||||
{
|
{
|
||||||
width: base.width
|
width: base.width
|
||||||
spacing: UM.Theme.getSize("default_margin").height
|
spacing: UM.Theme.getSize("default_margin").height
|
||||||
height: childrenRect.height
|
|
||||||
|
|
||||||
ToolboxDownloadsShowcase
|
ToolboxDownloadsShowcase
|
||||||
{
|
{
|
||||||
|
@ -31,14 +26,14 @@ ScrollView
|
||||||
{
|
{
|
||||||
id: allPlugins
|
id: allPlugins
|
||||||
width: parent.width
|
width: parent.width
|
||||||
heading: toolbox.viewCategory == "material" ? catalog.i18nc("@label", "Community Contributions") : catalog.i18nc("@label", "Community Plugins")
|
heading: toolbox.viewCategory === "material" ? catalog.i18nc("@label", "Community Contributions") : catalog.i18nc("@label", "Community Plugins")
|
||||||
model: toolbox.viewCategory == "material" ? toolbox.materialsAvailableModel : toolbox.pluginsAvailableModel
|
model: toolbox.viewCategory === "material" ? toolbox.materialsAvailableModel : toolbox.pluginsAvailableModel
|
||||||
}
|
}
|
||||||
|
|
||||||
ToolboxDownloadsGrid
|
ToolboxDownloadsGrid
|
||||||
{
|
{
|
||||||
id: genericMaterials
|
id: genericMaterials
|
||||||
visible: toolbox.viewCategory == "material"
|
visible: toolbox.viewCategory === "material"
|
||||||
width: parent.width
|
width: parent.width
|
||||||
heading: catalog.i18nc("@label", "Generic Materials")
|
heading: catalog.i18nc("@label", "Generic Materials")
|
||||||
model: toolbox.materialsGenericModel
|
model: toolbox.materialsGenericModel
|
||||||
|
|
|
@ -1,50 +1,50 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Toolbox is released under the terms of the LGPLv3 or higher.
|
// Toolbox is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.10
|
import QtQuick 2.10
|
||||||
import QtQuick.Dialogs 1.1
|
import QtQuick.Controls 2.3
|
||||||
import QtQuick.Window 2.2
|
|
||||||
import QtQuick.Controls 1.4
|
|
||||||
import QtQuick.Controls.Styles 1.4
|
|
||||||
|
|
||||||
import UM 1.1 as UM
|
import UM 1.1 as UM
|
||||||
|
|
||||||
ScrollView
|
ScrollView
|
||||||
{
|
{
|
||||||
id: page
|
id: page
|
||||||
frameVisible: false
|
clip: true
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: parent.height
|
height: parent.height
|
||||||
style: UM.Theme.styles.scrollview
|
|
||||||
flickableItem.flickableDirection: Flickable.VerticalFlick
|
|
||||||
|
|
||||||
Column
|
Column
|
||||||
{
|
{
|
||||||
|
width: page.width
|
||||||
spacing: UM.Theme.getSize("default_margin").height
|
spacing: UM.Theme.getSize("default_margin").height
|
||||||
|
padding: UM.Theme.getSize("wide_margin").width
|
||||||
visible: toolbox.pluginsInstalledModel.items.length > 0
|
visible: toolbox.pluginsInstalledModel.items.length > 0
|
||||||
height: childrenRect.height + 4 * UM.Theme.getSize("default_margin").height
|
height: childrenRect.height + 2 * UM.Theme.getSize("wide_margin").height
|
||||||
|
|
||||||
anchors
|
|
||||||
{
|
|
||||||
right: parent.right
|
|
||||||
left: parent.left
|
|
||||||
margins: UM.Theme.getSize("default_margin").width
|
|
||||||
top: parent.top
|
|
||||||
}
|
|
||||||
|
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
width: page.width
|
anchors
|
||||||
|
{
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
margins: parent.padding
|
||||||
|
}
|
||||||
text: catalog.i18nc("@title:tab", "Plugins")
|
text: catalog.i18nc("@title:tab", "Plugins")
|
||||||
color: UM.Theme.getColor("text_medium")
|
color: UM.Theme.getColor("text_medium")
|
||||||
font: UM.Theme.getFont("large")
|
font: UM.Theme.getFont("large")
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle
|
Rectangle
|
||||||
{
|
{
|
||||||
|
anchors
|
||||||
|
{
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
margins: parent.padding
|
||||||
|
}
|
||||||
id: installedPlugins
|
id: installedPlugins
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
width: parent.width
|
|
||||||
height: childrenRect.height + UM.Theme.getSize("default_margin").width
|
height: childrenRect.height + UM.Theme.getSize("default_margin").width
|
||||||
border.color: UM.Theme.getColor("lining")
|
border.color: UM.Theme.getColor("lining")
|
||||||
border.width: UM.Theme.getSize("default_lining").width
|
border.width: UM.Theme.getSize("default_lining").width
|
||||||
|
@ -65,8 +65,15 @@ ScrollView
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
|
anchors
|
||||||
|
{
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
margins: parent.padding
|
||||||
|
}
|
||||||
text: catalog.i18nc("@title:tab", "Materials")
|
text: catalog.i18nc("@title:tab", "Materials")
|
||||||
color: UM.Theme.getColor("text_medium")
|
color: UM.Theme.getColor("text_medium")
|
||||||
font: UM.Theme.getFont("medium")
|
font: UM.Theme.getFont("medium")
|
||||||
|
@ -75,9 +82,14 @@ ScrollView
|
||||||
|
|
||||||
Rectangle
|
Rectangle
|
||||||
{
|
{
|
||||||
|
anchors
|
||||||
|
{
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
margins: parent.padding
|
||||||
|
}
|
||||||
id: installedMaterials
|
id: installedMaterials
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
width: parent.width
|
|
||||||
height: childrenRect.height + UM.Theme.getSize("default_margin").width
|
height: childrenRect.height + UM.Theme.getSize("default_margin").width
|
||||||
border.color: UM.Theme.getColor("lining")
|
border.color: UM.Theme.getColor("lining")
|
||||||
border.width: UM.Theme.getSize("default_lining").width
|
border.width: UM.Theme.getSize("default_lining").width
|
||||||
|
|
|
@ -41,7 +41,7 @@ Item
|
||||||
Column
|
Column
|
||||||
{
|
{
|
||||||
id: pluginInfo
|
id: pluginInfo
|
||||||
topPadding: Math.floor(UM.Theme.getSize("default_margin").height / 2)
|
topPadding: UM.Theme.getSize("narrow_margin").height
|
||||||
property var color: model.type === "plugin" && !isEnabled ? UM.Theme.getColor("lining") : UM.Theme.getColor("text")
|
property var color: model.type === "plugin" && !isEnabled ? UM.Theme.getColor("lining") : UM.Theme.getColor("text")
|
||||||
width: Math.floor(tileRow.width - (authorInfo.width + pluginActions.width + 2 * tileRow.spacing + ((disableButton.visible) ? disableButton.width + tileRow.spacing : 0)))
|
width: Math.floor(tileRow.width - (authorInfo.width + pluginActions.width + 2 * tileRow.spacing + ((disableButton.visible) ? disableButton.width + tileRow.spacing : 0)))
|
||||||
Label
|
Label
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Toolbox is released under the terms of the LGPLv3 or higher.
|
// Toolbox is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.10
|
||||||
import QtQuick.Controls 1.4
|
import QtQuick.Controls 2.3
|
||||||
import QtQuick.Controls.Styles 1.4
|
|
||||||
import UM 1.1 as UM
|
import UM 1.1 as UM
|
||||||
import Cura 1.0 as Cura
|
import Cura 1.0 as Cura
|
||||||
|
|
||||||
|
|
||||||
Item
|
Cura.PrimaryButton
|
||||||
{
|
{
|
||||||
id: base
|
id: button
|
||||||
|
|
||||||
property var active: false
|
property var active: false
|
||||||
property var complete: false
|
property var complete: false
|
||||||
|
@ -23,12 +23,6 @@ Item
|
||||||
signal activeAction() // Action when button is active and clicked (likely cancel)
|
signal activeAction() // Action when button is active and clicked (likely cancel)
|
||||||
signal completeAction() // Action when button is complete and clicked (likely go to installed)
|
signal completeAction() // Action when button is complete and clicked (likely go to installed)
|
||||||
|
|
||||||
width: UM.Theme.getSize("toolbox_action_button").width
|
|
||||||
height: UM.Theme.getSize("toolbox_action_button").height
|
|
||||||
|
|
||||||
Cura.PrimaryButton
|
|
||||||
{
|
|
||||||
id: button
|
|
||||||
width: UM.Theme.getSize("toolbox_action_button").width
|
width: UM.Theme.getSize("toolbox_action_button").width
|
||||||
height: UM.Theme.getSize("toolbox_action_button").height
|
height: UM.Theme.getSize("toolbox_action_button").height
|
||||||
fixedWidthMode: true
|
fixedWidthMode: true
|
||||||
|
@ -63,5 +57,4 @@ Item
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
busy: active
|
busy: active
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -655,7 +655,11 @@ class Toolbox(QObject, Extension):
|
||||||
|
|
||||||
# Check if the download was sucessfull
|
# Check if the download was sucessfull
|
||||||
if self._download_reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
|
if self._download_reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
|
||||||
|
try:
|
||||||
Logger.log("w", "Failed to download package. The following error was returned: %s", json.loads(bytes(self._download_reply.readAll()).decode("utf-8")))
|
Logger.log("w", "Failed to download package. The following error was returned: %s", json.loads(bytes(self._download_reply.readAll()).decode("utf-8")))
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
Logger.logException("w", "Failed to download package and failed to parse a response from it")
|
||||||
|
finally:
|
||||||
return
|
return
|
||||||
# Must not delete the temporary file on Windows
|
# Must not delete the temporary file on Windows
|
||||||
self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False)
|
self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from .src import DiscoverUM3Action
|
|
||||||
from .src import UM3OutputDevicePlugin
|
from .src import UM3OutputDevicePlugin
|
||||||
|
from .src import UltimakerNetworkedPrinterAction
|
||||||
|
|
||||||
|
|
||||||
def getMetaData():
|
def getMetaData():
|
||||||
|
@ -11,5 +11,5 @@ def getMetaData():
|
||||||
def register(app):
|
def register(app):
|
||||||
return {
|
return {
|
||||||
"output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin(),
|
"output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin(),
|
||||||
"machine_action": DiscoverUM3Action.DiscoverUM3Action()
|
"machine_action": UltimakerNetworkedPrinterAction.UltimakerNetworkedPrinterAction()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "UM3 Network Connection",
|
"name": "Ultimaker Network Connection",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"description": "Manages network connections to Ultimaker 3 printers.",
|
"description": "Manages network connections to Ultimaker networked printers.",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"api": "6.0",
|
"api": "6.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.3
|
import QtQuick 2.3
|
||||||
|
|
|
@ -23,35 +23,11 @@ Cura.MachineAction
|
||||||
|
|
||||||
function connectToPrinter()
|
function connectToPrinter()
|
||||||
{
|
{
|
||||||
if(base.selectedDevice && base.completeProperties)
|
if (base.selectedDevice && base.completeProperties)
|
||||||
{
|
|
||||||
var printerKey = base.selectedDevice.key
|
|
||||||
var printerName = base.selectedDevice.name // TODO To change when the groups have a name
|
|
||||||
if (manager.getStoredKey() != printerKey)
|
|
||||||
{
|
|
||||||
// Check if there is another instance with the same key
|
|
||||||
if (!manager.existsKey(printerKey))
|
|
||||||
{
|
{
|
||||||
manager.associateActiveMachineWithPrinterDevice(base.selectedDevice)
|
manager.associateActiveMachineWithPrinterDevice(base.selectedDevice)
|
||||||
manager.setGroupName(printerName) // TODO To change when the groups have a name
|
|
||||||
completed()
|
completed()
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
existingConnectionDialog.open()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MessageDialog
|
|
||||||
{
|
|
||||||
id: existingConnectionDialog
|
|
||||||
title: catalog.i18nc("@window:title", "Existing Connection")
|
|
||||||
icon: StandardIcon.Information
|
|
||||||
text: catalog.i18nc("@message:text", "This printer/group is already added to Cura. Please select another printer/group.")
|
|
||||||
standardButtons: StandardButton.Ok
|
|
||||||
modality: Qt.ApplicationModal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column
|
Column
|
||||||
|
@ -151,21 +127,6 @@ Cura.MachineAction
|
||||||
{
|
{
|
||||||
id: listview
|
id: listview
|
||||||
model: manager.foundDevices
|
model: manager.foundDevices
|
||||||
onModelChanged:
|
|
||||||
{
|
|
||||||
var selectedKey = manager.getLastManualEntryKey()
|
|
||||||
// If there is no last manual entry key, then we select the stored key (if any)
|
|
||||||
if (selectedKey == "")
|
|
||||||
selectedKey = manager.getStoredKey()
|
|
||||||
for(var i = 0; i < model.length; i++) {
|
|
||||||
if(model[i].key == selectedKey)
|
|
||||||
{
|
|
||||||
currentIndex = i;
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentIndex = -1;
|
|
||||||
}
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
currentIndex: -1
|
currentIndex: -1
|
||||||
onCurrentIndexChanged:
|
onCurrentIndexChanged:
|
||||||
|
@ -250,31 +211,15 @@ Cura.MachineAction
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
text:
|
text:
|
||||||
{
|
{
|
||||||
if(base.selectedDevice)
|
if (base.selectedDevice) {
|
||||||
{
|
// It would be great to use a more readable machine type here,
|
||||||
if (base.selectedDevice.printerType == "ultimaker3")
|
// but the new discoveredPrintersModel is not used yet in the UM networking actions.
|
||||||
{
|
// TODO: remove actions or replace 'connect via network' button with new flow?
|
||||||
return "Ultimaker 3";
|
return base.selectedDevice.printerType
|
||||||
}
|
}
|
||||||
else if (base.selectedDevice.printerType == "ultimaker3_extended")
|
|
||||||
{
|
|
||||||
return "Ultimaker 3 Extended";
|
|
||||||
}
|
|
||||||
else if (base.selectedDevice.printerType == "ultimaker_s5")
|
|
||||||
{
|
|
||||||
return "Ultimaker S5";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return catalog.i18nc("@label", "Unknown") // We have no idea what type it is. Should not happen 'in the field'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
width: Math.round(parent.width * 0.5)
|
width: Math.round(parent.width * 0.5)
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.3
|
import QtQuick 2.3
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.3
|
import QtQuick 2.3
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.3
|
import QtQuick 2.3
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.3
|
import QtQuick 2.3
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.3
|
import QtQuick 2.3
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
import QtQuick.Controls 2.0
|
import QtQuick.Controls 2.0
|
||||||
import UM 1.3 as UM
|
import UM 1.3 as UM
|
||||||
|
@ -76,6 +75,7 @@ Item
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
height: 18 * screenScaleFactor // TODO: Theme!
|
height: 18 * screenScaleFactor // TODO: Theme!
|
||||||
width: UM.Theme.getSize("monitor_column").width
|
width: UM.Theme.getSize("monitor_column").width
|
||||||
|
|
||||||
Rectangle
|
Rectangle
|
||||||
{
|
{
|
||||||
color: UM.Theme.getColor("monitor_skeleton_loading")
|
color: UM.Theme.getColor("monitor_skeleton_loading")
|
||||||
|
@ -84,6 +84,7 @@ Item
|
||||||
visible: !printJob
|
visible: !printJob
|
||||||
radius: 2 * screenScaleFactor // TODO: Theme!
|
radius: 2 * screenScaleFactor // TODO: Theme!
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
text: printJob ? OutputDevice.formatDuration(printJob.timeTotal) : ""
|
text: printJob ? OutputDevice.formatDuration(printJob.timeTotal) : ""
|
||||||
|
@ -179,13 +180,10 @@ Item
|
||||||
id: printerConfiguration
|
id: printerConfiguration
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
buildplate: catalog.i18nc("@label", "Glass")
|
buildplate: catalog.i18nc("@label", "Glass")
|
||||||
configurations:
|
configurations: base.printJob.configuration.extruderConfigurations
|
||||||
[
|
|
||||||
base.printJob.configuration.extruderConfigurations[0],
|
|
||||||
base.printJob.configuration.extruderConfigurations[1]
|
|
||||||
]
|
|
||||||
height: 72 * screenScaleFactor // TODO: Theme!
|
height: 72 * screenScaleFactor // TODO: Theme!
|
||||||
}
|
}
|
||||||
|
|
||||||
Label {
|
Label {
|
||||||
text: printJob && printJob.owner ? printJob.owner : ""
|
text: printJob && printJob.owner ? printJob.owner : ""
|
||||||
color: UM.Theme.getColor("monitor_text_primary")
|
color: UM.Theme.getColor("monitor_text_primary")
|
||||||
|
@ -243,10 +241,11 @@ Item
|
||||||
enabled: !contextMenuButton.enabled
|
enabled: !contextMenuButton.enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
MonitorInfoBlurb
|
// TODO: uncomment this tooltip as soon as the required firmware is released
|
||||||
{
|
// MonitorInfoBlurb
|
||||||
id: contextMenuDisabledInfo
|
// {
|
||||||
text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.")
|
// id: contextMenuDisabledInfo
|
||||||
target: contextMenuButton
|
// text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.")
|
||||||
}
|
// target: contextMenuButton
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.3
|
import QtQuick 2.3
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.3
|
import QtQuick 2.3
|
||||||
|
@ -90,7 +90,7 @@ Item
|
||||||
verticalCenter: parent.verticalCenter
|
verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
width: 180 * screenScaleFactor // TODO: Theme!
|
width: 180 * screenScaleFactor // TODO: Theme!
|
||||||
height: printerNameLabel.height + printerFamilyPill.height + 6 * screenScaleFactor // TODO: Theme!
|
height: childrenRect.height
|
||||||
|
|
||||||
Rectangle
|
Rectangle
|
||||||
{
|
{
|
||||||
|
@ -135,6 +135,54 @@ Item
|
||||||
}
|
}
|
||||||
text: printer ? printer.type : ""
|
text: printer ? printer.type : ""
|
||||||
}
|
}
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
id: managePrinterLink
|
||||||
|
anchors {
|
||||||
|
top: printerFamilyPill.bottom
|
||||||
|
topMargin: 6 * screenScaleFactor
|
||||||
|
}
|
||||||
|
height: 18 * screenScaleFactor // TODO: Theme!
|
||||||
|
width: childrenRect.width
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
id: managePrinterText
|
||||||
|
anchors.verticalCenter: managePrinterLink.verticalCenter
|
||||||
|
color: UM.Theme.getColor("monitor_text_link")
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
linkColor: UM.Theme.getColor("monitor_text_link")
|
||||||
|
text: catalog.i18nc("@label link to Connect and Cloud interfaces", "Manage printer")
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
UM.RecolorImage
|
||||||
|
{
|
||||||
|
id: externalLinkIcon
|
||||||
|
anchors
|
||||||
|
{
|
||||||
|
left: managePrinterText.right
|
||||||
|
leftMargin: 6 * screenScaleFactor
|
||||||
|
verticalCenter: managePrinterText.verticalCenter
|
||||||
|
}
|
||||||
|
color: UM.Theme.getColor("monitor_text_link")
|
||||||
|
source: UM.Theme.getIcon("external_link")
|
||||||
|
width: 12 * screenScaleFactor
|
||||||
|
height: 12 * screenScaleFactor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MouseArea
|
||||||
|
{
|
||||||
|
anchors.fill: managePrinterLink
|
||||||
|
onClicked: OutputDevice.openPrintJobControlPanel()
|
||||||
|
onEntered:
|
||||||
|
{
|
||||||
|
manageQueueText.font.underline = true
|
||||||
|
}
|
||||||
|
onExited:
|
||||||
|
{
|
||||||
|
manageQueueText.font.underline = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MonitorPrinterConfiguration
|
MonitorPrinterConfiguration
|
||||||
|
@ -202,12 +250,13 @@ Item
|
||||||
enabled: !contextMenuButton.enabled
|
enabled: !contextMenuButton.enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
MonitorInfoBlurb
|
// TODO: uncomment this tooltip as soon as the required firmware is released
|
||||||
{
|
// MonitorInfoBlurb
|
||||||
id: contextMenuDisabledInfo
|
// {
|
||||||
text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.")
|
// id: contextMenuDisabledInfo
|
||||||
target: contextMenuButton
|
// text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.")
|
||||||
}
|
// target: contextMenuButton
|
||||||
|
// }
|
||||||
|
|
||||||
CameraButton
|
CameraButton
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
|
@ -11,20 +11,8 @@ import UM 1.2 as UM
|
||||||
*/
|
*/
|
||||||
Item
|
Item
|
||||||
{
|
{
|
||||||
// The printer name
|
id: monitorPrinterPill
|
||||||
property var text: ""
|
property var text: ""
|
||||||
property var tagText: {
|
|
||||||
switch(text) {
|
|
||||||
case "Ultimaker 3":
|
|
||||||
return "UM 3"
|
|
||||||
case "Ultimaker 3 Extended":
|
|
||||||
return "UM 3 EXT"
|
|
||||||
case "Ultimaker S5":
|
|
||||||
return "UM S5"
|
|
||||||
default:
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
implicitHeight: 18 * screenScaleFactor // TODO: Theme!
|
implicitHeight: 18 * screenScaleFactor // TODO: Theme!
|
||||||
implicitWidth: Math.max(printerNameLabel.contentWidth + 12 * screenScaleFactor, 36 * screenScaleFactor) // TODO: Theme!
|
implicitWidth: Math.max(printerNameLabel.contentWidth + 12 * screenScaleFactor, 36 * screenScaleFactor) // TODO: Theme!
|
||||||
|
@ -40,9 +28,9 @@ Item
|
||||||
id: printerNameLabel
|
id: printerNameLabel
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: UM.Theme.getColor("monitor_text_primary")
|
color: UM.Theme.getColor("monitor_text_primary")
|
||||||
text: tagText
|
text: monitorPrinterPill.text
|
||||||
font.pointSize: 10 // TODO: Theme!
|
font.pointSize: 10 // TODO: Theme!
|
||||||
visible: text !== ""
|
visible: monitorPrinterPill.text !== ""
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
|
@ -95,6 +95,22 @@ Item
|
||||||
}
|
}
|
||||||
spacing: 18 * screenScaleFactor // TODO: Theme!
|
spacing: 18 * screenScaleFactor // TODO: Theme!
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@label", "There are no print jobs in the queue. Slice and send a job to add one.")
|
||||||
|
color: UM.Theme.getColor("monitor_text_primary")
|
||||||
|
elide: Text.ElideRight
|
||||||
|
font: UM.Theme.getFont("medium") // 14pt, regular
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 600 * screenScaleFactor // TODO: Theme! (Should match column size)
|
||||||
|
|
||||||
|
// FIXED-LINE-HEIGHT:
|
||||||
|
height: 18 * screenScaleFactor // TODO: Theme!
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
visible: printJobList.count === 0
|
||||||
|
}
|
||||||
|
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
text: catalog.i18nc("@label", "Print jobs")
|
text: catalog.i18nc("@label", "Print jobs")
|
||||||
|
@ -108,6 +124,7 @@ Item
|
||||||
height: 18 * screenScaleFactor // TODO: Theme!
|
height: 18 * screenScaleFactor // TODO: Theme!
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
|
visible: printJobList.count > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
Label
|
||||||
|
@ -123,6 +140,7 @@ Item
|
||||||
height: 18 * screenScaleFactor // TODO: Theme!
|
height: 18 * screenScaleFactor // TODO: Theme!
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
|
visible: printJobList.count > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
Label
|
Label
|
||||||
|
@ -138,6 +156,7 @@ Item
|
||||||
height: 18 * screenScaleFactor // TODO: Theme!
|
height: 18 * screenScaleFactor // TODO: Theme!
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
|
visible: printJobList.count > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,102 +186,8 @@ Item
|
||||||
}
|
}
|
||||||
printJob: modelData
|
printJob: modelData
|
||||||
}
|
}
|
||||||
model:
|
model: OutputDevice.queuedPrintJobs
|
||||||
{
|
|
||||||
// When printing over the cloud we don't recieve print jobs until there is one, so
|
|
||||||
// unless there's at least one print job we'll be stuck with skeleton loading
|
|
||||||
// indefinitely.
|
|
||||||
if (Cura.MachineManager.activeMachineIsUsingCloudConnection || OutputDevice.receivedPrintJobs)
|
|
||||||
{
|
|
||||||
return OutputDevice.queuedPrintJobs
|
|
||||||
}
|
|
||||||
return [null, null]
|
|
||||||
}
|
|
||||||
spacing: 6 // TODO: Theme!
|
spacing: 6 // TODO: Theme!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle
|
|
||||||
{
|
|
||||||
anchors
|
|
||||||
{
|
|
||||||
horizontalCenter: parent.horizontalCenter
|
|
||||||
top: printJobQueueHeadings.bottom
|
|
||||||
topMargin: 12 * screenScaleFactor // TODO: Theme!
|
|
||||||
}
|
|
||||||
height: 48 * screenScaleFactor // TODO: Theme!
|
|
||||||
width: parent.width
|
|
||||||
color: UM.Theme.getColor("monitor_card_background")
|
|
||||||
border.color: UM.Theme.getColor("monitor_card_border")
|
|
||||||
radius: 2 * screenScaleFactor // TODO: Theme!
|
|
||||||
visible: OutputDevice.printJobs.length == 0
|
|
||||||
|
|
||||||
Row
|
|
||||||
{
|
|
||||||
anchors
|
|
||||||
{
|
|
||||||
left: parent.left
|
|
||||||
leftMargin: 18 * screenScaleFactor // TODO: Theme!
|
|
||||||
verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
spacing: 18 * screenScaleFactor // TODO: Theme!
|
|
||||||
height: 18 * screenScaleFactor // TODO: Theme!
|
|
||||||
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
text: i18n.i18nc("@info", "All jobs are printed.")
|
|
||||||
color: UM.Theme.getColor("monitor_text_primary")
|
|
||||||
font: UM.Theme.getFont("medium") // 14pt, regular
|
|
||||||
renderType: Text.NativeRendering
|
|
||||||
}
|
|
||||||
|
|
||||||
Item
|
|
||||||
{
|
|
||||||
id: viewPrintHistoryLabel
|
|
||||||
|
|
||||||
height: 18 * screenScaleFactor // TODO: Theme!
|
|
||||||
width: childrenRect.width
|
|
||||||
visible: !cloudConnection
|
|
||||||
|
|
||||||
UM.RecolorImage
|
|
||||||
{
|
|
||||||
id: printHistoryIcon
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
color: UM.Theme.getColor("monitor_text_link")
|
|
||||||
source: UM.Theme.getIcon("external_link")
|
|
||||||
width: 16 * screenScaleFactor // TODO: Theme! (Y U NO USE 18 LIKE ALL OTHER ICONS?!)
|
|
||||||
height: 16 * screenScaleFactor // TODO: Theme! (Y U NO USE 18 LIKE ALL OTHER ICONS?!)
|
|
||||||
}
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
id: viewPrintHistoryText
|
|
||||||
anchors
|
|
||||||
{
|
|
||||||
left: printHistoryIcon.right
|
|
||||||
leftMargin: 6 * screenScaleFactor // TODO: Theme!
|
|
||||||
verticalCenter: printHistoryIcon.verticalCenter
|
|
||||||
}
|
|
||||||
color: UM.Theme.getColor("monitor_text_link")
|
|
||||||
font: UM.Theme.getFont("medium") // 14pt, regular
|
|
||||||
linkColor: UM.Theme.getColor("monitor_text_link")
|
|
||||||
text: catalog.i18nc("@label link to connect manager", "Manage in browser")
|
|
||||||
renderType: Text.NativeRendering
|
|
||||||
}
|
|
||||||
MouseArea
|
|
||||||
{
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
onClicked: OutputDevice.openPrintJobControlPanel()
|
|
||||||
onEntered:
|
|
||||||
{
|
|
||||||
viewPrintHistoryText.font.underline = true
|
|
||||||
}
|
|
||||||
onExited:
|
|
||||||
{
|
|
||||||
viewPrintHistoryText.font.underline = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
|
@ -50,17 +50,7 @@ Component
|
||||||
MonitorCarousel
|
MonitorCarousel
|
||||||
{
|
{
|
||||||
id: carousel
|
id: carousel
|
||||||
printers:
|
printers: OutputDevice.printers
|
||||||
{
|
|
||||||
// When printing over the cloud we don't recieve print jobs until there is one, so
|
|
||||||
// unless there's at least one print job we'll be stuck with skeleton loading
|
|
||||||
// indefinitely.
|
|
||||||
if (Cura.MachineManager.activeMachineIsUsingCloudConnection || OutputDevice.receivedPrintJobs)
|
|
||||||
{
|
|
||||||
return OutputDevice.printers
|
|
||||||
}
|
|
||||||
return [null]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.2
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
import QtQuick 2.2
|
|
||||||
import QtQuick.Controls 1.1
|
|
||||||
import QtQuick.Layouts 1.1
|
|
||||||
import QtQuick.Window 2.1
|
|
||||||
import UM 1.2 as UM
|
|
||||||
import Cura 1.0 as Cura
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: base;
|
|
||||||
property string activeQualityDefinitionId: Cura.MachineManager.activeQualityDefinitionId;
|
|
||||||
property bool isUM3: activeQualityDefinitionId == "ultimaker3" || activeQualityDefinitionId.match("ultimaker_") != null;
|
|
||||||
property bool printerConnected: Cura.MachineManager.printerConnected;
|
|
||||||
property bool printerAcceptsCommands:
|
|
||||||
{
|
|
||||||
if (printerConnected && Cura.MachineManager.printerOutputDevices[0])
|
|
||||||
{
|
|
||||||
return Cura.MachineManager.printerOutputDevices[0].acceptsCommands
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
property bool authenticationRequested:
|
|
||||||
{
|
|
||||||
if (printerConnected && Cura.MachineManager.printerOutputDevices[0])
|
|
||||||
{
|
|
||||||
var device = Cura.MachineManager.printerOutputDevices[0]
|
|
||||||
// AuthState.AuthenticationRequested or AuthState.AuthenticationReceived
|
|
||||||
return device.authenticationState == 2 || device.authenticationState == 5
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
property var materialNames:
|
|
||||||
{
|
|
||||||
if (printerConnected && Cura.MachineManager.printerOutputDevices[0])
|
|
||||||
{
|
|
||||||
return Cura.MachineManager.printerOutputDevices[0].materialNames
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
property var hotendIds:
|
|
||||||
{
|
|
||||||
if (printerConnected && Cura.MachineManager.printerOutputDevices[0])
|
|
||||||
{
|
|
||||||
return Cura.MachineManager.printerOutputDevices[0].hotendIds
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
UM.I18nCatalog {
|
|
||||||
id: catalog;
|
|
||||||
name: "cura";
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
objectName: "networkPrinterConnectButton";
|
|
||||||
spacing: UM.Theme.getSize("default_margin").width;
|
|
||||||
visible: isUM3;
|
|
||||||
|
|
||||||
Button {
|
|
||||||
height: UM.Theme.getSize("save_button_save_to_button").height;
|
|
||||||
onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication();
|
|
||||||
style: UM.Theme.styles.print_setup_action_button;
|
|
||||||
text: catalog.i18nc("@action:button", "Request Access");
|
|
||||||
tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer");
|
|
||||||
visible: printerConnected && !printerAcceptsCommands && !authenticationRequested;
|
|
||||||
}
|
|
||||||
|
|
||||||
Button {
|
|
||||||
height: UM.Theme.getSize("save_button_save_to_button").height;
|
|
||||||
onClicked: connectActionDialog.show();
|
|
||||||
style: UM.Theme.styles.print_setup_action_button;
|
|
||||||
text: catalog.i18nc("@action:button", "Connect");
|
|
||||||
tooltip: catalog.i18nc("@info:tooltip", "Connect to a printer");
|
|
||||||
visible: !printerConnected;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UM.Dialog {
|
|
||||||
id: connectActionDialog;
|
|
||||||
rightButtons: Button {
|
|
||||||
iconName: "dialog-close";
|
|
||||||
onClicked: connectActionDialog.reject();
|
|
||||||
text: catalog.i18nc("@action:button", "Close");
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
anchors.fill: parent;
|
|
||||||
source: "DiscoverUM3Action.qml";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
import json
|
import json
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
|
@ -11,18 +11,19 @@ from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManage
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from cura import UltimakerCloudAuthentication
|
from cura import UltimakerCloudAuthentication
|
||||||
from cura.API import Account
|
from cura.API import Account
|
||||||
|
|
||||||
from .ToolPathUploader import ToolPathUploader
|
from .ToolPathUploader import ToolPathUploader
|
||||||
from ..Models import BaseModel
|
from ..Models.BaseModel import BaseModel
|
||||||
from .Models.CloudClusterResponse import CloudClusterResponse
|
from ..Models.Http.CloudClusterResponse import CloudClusterResponse
|
||||||
from .Models.CloudError import CloudError
|
from ..Models.Http.CloudError import CloudError
|
||||||
from .Models.CloudClusterStatus import CloudClusterStatus
|
from ..Models.Http.CloudClusterStatus import CloudClusterStatus
|
||||||
from .Models.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest
|
from ..Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest
|
||||||
from .Models.CloudPrintResponse import CloudPrintResponse
|
from ..Models.Http.CloudPrintResponse import CloudPrintResponse
|
||||||
from .Models.CloudPrintJobResponse import CloudPrintJobResponse
|
from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse
|
||||||
|
|
||||||
|
|
||||||
## The generic type variable used to document the methods below.
|
## The generic type variable used to document the methods below.
|
||||||
CloudApiClientModel = TypeVar("CloudApiClientModel", bound = BaseModel)
|
CloudApiClientModel = TypeVar("CloudApiClientModel", bound=BaseModel)
|
||||||
|
|
||||||
|
|
||||||
## The cloud API client is responsible for handling the requests and responses from the cloud.
|
## The cloud API client is responsible for handling the requests and responses from the cloud.
|
||||||
|
@ -34,6 +35,9 @@ class CloudApiClient:
|
||||||
CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH)
|
CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH)
|
||||||
CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH)
|
CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH)
|
||||||
|
|
||||||
|
# In order to avoid garbage collection we keep the callbacks in this list.
|
||||||
|
_anti_gc_callbacks = [] # type: List[Callable[[], None]]
|
||||||
|
|
||||||
## Initializes a new cloud API client.
|
## Initializes a new cloud API client.
|
||||||
# \param account: The user's account object
|
# \param account: The user's account object
|
||||||
# \param on_error: The callback to be called whenever we receive errors from the server.
|
# \param on_error: The callback to be called whenever we receive errors from the server.
|
||||||
|
@ -43,8 +47,6 @@ class CloudApiClient:
|
||||||
self._account = account
|
self._account = account
|
||||||
self._on_error = on_error
|
self._on_error = on_error
|
||||||
self._upload = None # type: Optional[ToolPathUploader]
|
self._upload = None # type: Optional[ToolPathUploader]
|
||||||
# In order to avoid garbage collection we keep the callbacks in this list.
|
|
||||||
self._anti_gc_callbacks = [] # type: List[Callable[[], None]]
|
|
||||||
|
|
||||||
## Gets the account used for the API.
|
## Gets the account used for the API.
|
||||||
@property
|
@property
|
||||||
|
@ -69,8 +71,8 @@ class CloudApiClient:
|
||||||
## Requests the cloud to register the upload of a print job mesh.
|
## Requests the cloud to register the upload of a print job mesh.
|
||||||
# \param request: The request object.
|
# \param request: The request object.
|
||||||
# \param on_finished: The function to be called after the result is parsed.
|
# \param on_finished: The function to be called after the result is parsed.
|
||||||
def requestUpload(self, request: CloudPrintJobUploadRequest, on_finished: Callable[[CloudPrintJobResponse], Any]
|
def requestUpload(self, request: CloudPrintJobUploadRequest,
|
||||||
) -> None:
|
on_finished: Callable[[CloudPrintJobResponse], Any]) -> None:
|
||||||
url = "{}/jobs/upload".format(self.CURA_API_ROOT)
|
url = "{}/jobs/upload".format(self.CURA_API_ROOT)
|
||||||
body = json.dumps({"data": request.toDict()})
|
body = json.dumps({"data": request.toDict()})
|
||||||
reply = self._manager.put(self._createEmptyRequest(url), body.encode())
|
reply = self._manager.put(self._createEmptyRequest(url), body.encode())
|
||||||
|
@ -100,14 +102,9 @@ class CloudApiClient:
|
||||||
# \param cluster_id: The ID of the cluster.
|
# \param cluster_id: The ID of the cluster.
|
||||||
# \param cluster_job_id: The ID of the print job within the cluster.
|
# \param cluster_job_id: The ID of the print job within the cluster.
|
||||||
# \param action: The name of the action to execute.
|
# \param action: The name of the action to execute.
|
||||||
def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str, data: Optional[Dict[str, Any]] = None) -> None:
|
def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str,
|
||||||
body = b""
|
data: Optional[Dict[str, Any]] = None) -> None:
|
||||||
if data:
|
body = json.dumps({"data": data}).encode() if data else b""
|
||||||
try:
|
|
||||||
body = json.dumps({"data": data}).encode()
|
|
||||||
except JSONDecodeError as err:
|
|
||||||
Logger.log("w", "Could not encode body: %s", err)
|
|
||||||
return
|
|
||||||
url = "{}/clusters/{}/print_jobs/{}/action/{}".format(self.CLUSTER_API_ROOT, cluster_id, cluster_job_id, action)
|
url = "{}/clusters/{}/print_jobs/{}/action/{}".format(self.CLUSTER_API_ROOT, cluster_id, cluster_job_id, action)
|
||||||
self._manager.post(self._createEmptyRequest(url), body)
|
self._manager.post(self._createEmptyRequest(url), body)
|
||||||
|
|
||||||
|
@ -171,12 +168,16 @@ class CloudApiClient:
|
||||||
reply: QNetworkReply,
|
reply: QNetworkReply,
|
||||||
on_finished: Union[Callable[[CloudApiClientModel], Any],
|
on_finished: Union[Callable[[CloudApiClientModel], Any],
|
||||||
Callable[[List[CloudApiClientModel]], Any]],
|
Callable[[List[CloudApiClientModel]], Any]],
|
||||||
model: Type[CloudApiClientModel],
|
model: Type[CloudApiClientModel]) -> None:
|
||||||
) -> None:
|
|
||||||
def parse() -> None:
|
def parse() -> None:
|
||||||
status_code, response = self._parseReply(reply)
|
|
||||||
self._anti_gc_callbacks.remove(parse)
|
self._anti_gc_callbacks.remove(parse)
|
||||||
return self._parseModels(response, on_finished, model)
|
|
||||||
|
# Don't try to parse the reply if we didn't get one
|
||||||
|
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
status_code, response = self._parseReply(reply)
|
||||||
|
self._parseModels(response, on_finished, model)
|
||||||
|
|
||||||
self._anti_gc_callbacks.append(parse)
|
self._anti_gc_callbacks.append(parse)
|
||||||
reply.finished.connect(parse)
|
reply.finished.connect(parse)
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
import os
|
|
||||||
|
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Dict, List, Optional, Set, cast
|
from typing import List, Optional, cast
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, QUrl, pyqtProperty, pyqtSignal, pyqtSlot
|
from PyQt5.QtCore import QObject, QUrl, pyqtProperty, pyqtSignal, pyqtSlot
|
||||||
from PyQt5.QtGui import QDesktopServices
|
from PyQt5.QtGui import QDesktopServices
|
||||||
|
@ -12,30 +10,25 @@ from UM import i18nCatalog
|
||||||
from UM.Backend.Backend import BackendState
|
from UM.Backend.Backend import BackendState
|
||||||
from UM.FileHandler.FileHandler import FileHandler
|
from UM.FileHandler.FileHandler import FileHandler
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Message import Message
|
|
||||||
from UM.PluginRegistry import PluginRegistry
|
|
||||||
from UM.Qt.Duration import Duration, DurationFormat
|
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Version import Version
|
from UM.Version import Version
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice
|
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState
|
||||||
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
|
||||||
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
|
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
|
||||||
|
|
||||||
from .CloudOutputController import CloudOutputController
|
|
||||||
from ..MeshFormatHandler import MeshFormatHandler
|
|
||||||
from ..UM3PrintJobOutputModel import UM3PrintJobOutputModel
|
|
||||||
from .CloudProgressMessage import CloudProgressMessage
|
|
||||||
from .CloudApiClient import CloudApiClient
|
from .CloudApiClient import CloudApiClient
|
||||||
from .Models.CloudClusterResponse import CloudClusterResponse
|
from ..ExportFileJob import ExportFileJob
|
||||||
from .Models.CloudClusterStatus import CloudClusterStatus
|
from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice
|
||||||
from .Models.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest
|
from ..Messages.PrintJobUploadBlockedMessage import PrintJobUploadBlockedMessage
|
||||||
from .Models.CloudPrintResponse import CloudPrintResponse
|
from ..Messages.PrintJobUploadErrorMessage import PrintJobUploadErrorMessage
|
||||||
from .Models.CloudPrintJobResponse import CloudPrintJobResponse
|
from ..Messages.PrintJobUploadSuccessMessage import PrintJobUploadSuccessMessage
|
||||||
from .Models.CloudClusterPrinterStatus import CloudClusterPrinterStatus
|
from ..Models.Http.CloudClusterResponse import CloudClusterResponse
|
||||||
from .Models.CloudClusterPrintJobStatus import CloudClusterPrintJobStatus
|
from ..Models.Http.CloudClusterStatus import CloudClusterStatus
|
||||||
from .Utils import findChanges, formatDateCompleted, formatTimeCompleted
|
from ..Models.Http.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest
|
||||||
|
from ..Models.Http.CloudPrintResponse import CloudPrintResponse
|
||||||
|
from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse
|
||||||
|
from ..Models.Http.ClusterPrinterStatus import ClusterPrinterStatus
|
||||||
|
from ..Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus
|
||||||
|
|
||||||
|
|
||||||
I18N_CATALOG = i18nCatalog("cura")
|
I18N_CATALOG = i18nCatalog("cura")
|
||||||
|
@ -45,10 +38,11 @@ I18N_CATALOG = i18nCatalog("cura")
|
||||||
# Currently it only supports viewing the printer and print job status and adding a new job to the queue.
|
# Currently it only supports viewing the printer and print job status and adding a new job to the queue.
|
||||||
# As such, those methods have been implemented here.
|
# As such, those methods have been implemented here.
|
||||||
# Note that this device represents a single remote cluster, not a list of multiple clusters.
|
# Note that this device represents a single remote cluster, not a list of multiple clusters.
|
||||||
class CloudOutputDevice(NetworkedPrinterOutputDevice):
|
class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
||||||
|
|
||||||
# The interval with which the remote clusters are checked
|
# The interval with which the remote cluster is checked.
|
||||||
CHECK_CLUSTER_INTERVAL = 10.0 # seconds
|
# We can do this relatively often as this API call is quite fast.
|
||||||
|
CHECK_CLUSTER_INTERVAL = 8.0 # seconds
|
||||||
|
|
||||||
# The minimum version of firmware that support print job actions over cloud.
|
# The minimum version of firmware that support print job actions over cloud.
|
||||||
PRINT_JOB_ACTIONS_MIN_VERSION = Version("5.3.0")
|
PRINT_JOB_ACTIONS_MIN_VERSION = Version("5.3.0")
|
||||||
|
@ -80,44 +74,29 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
|
||||||
b"cluster_size": b"1" # cloud devices are always clusters of at least one
|
b"cluster_size": b"1" # cloud devices are always clusters of at least one
|
||||||
}
|
}
|
||||||
|
|
||||||
super().__init__(device_id = cluster.cluster_id, address = "",
|
super().__init__(
|
||||||
connection_type = ConnectionType.CloudConnection, properties = properties, parent = parent)
|
device_id=cluster.cluster_id,
|
||||||
|
address="",
|
||||||
|
connection_type=ConnectionType.CloudConnection,
|
||||||
|
properties=properties,
|
||||||
|
parent=parent
|
||||||
|
)
|
||||||
|
|
||||||
self._api = api_client
|
self._api = api_client
|
||||||
self._cluster = cluster
|
|
||||||
|
|
||||||
self._setInterfaceElements()
|
|
||||||
|
|
||||||
self._account = api_client.account
|
self._account = api_client.account
|
||||||
|
self._cluster = cluster
|
||||||
# We use the Cura Connect monitor tab to get most functionality right away.
|
self.setAuthenticationState(AuthState.NotAuthenticated)
|
||||||
if PluginRegistry.getInstance() is not None:
|
self._setInterfaceElements()
|
||||||
plugin_path = PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting")
|
|
||||||
if plugin_path is None:
|
|
||||||
Logger.log("e", "Cloud not find plugin path for plugin UM3NetworkPrnting")
|
|
||||||
raise RuntimeError("Cloud not find plugin path for plugin UM3NetworkPrnting")
|
|
||||||
self._monitor_view_qml_path = os.path.join(plugin_path, "resources", "qml", "MonitorStage.qml")
|
|
||||||
|
|
||||||
# Trigger the printersChanged signal when the private signal is triggered.
|
# Trigger the printersChanged signal when the private signal is triggered.
|
||||||
self.printersChanged.connect(self._clusterPrintersChanged)
|
self.printersChanged.connect(self._clusterPrintersChanged)
|
||||||
|
|
||||||
# We keep track of which printer is visible in the monitor page.
|
|
||||||
self._active_printer = None # type: Optional[PrinterOutputModel]
|
|
||||||
|
|
||||||
# Properties to populate later on with received cloud data.
|
|
||||||
self._print_jobs = [] # type: List[UM3PrintJobOutputModel]
|
|
||||||
self._number_of_extruders = 2 # All networked printers are dual-extrusion Ultimaker machines.
|
|
||||||
|
|
||||||
# We only allow a single upload at a time.
|
|
||||||
self._progress = CloudProgressMessage()
|
|
||||||
|
|
||||||
# Keep server string of the last generated time to avoid updating models more than once for the same response
|
# Keep server string of the last generated time to avoid updating models more than once for the same response
|
||||||
self._received_printers = None # type: Optional[List[CloudClusterPrinterStatus]]
|
self._received_printers = None # type: Optional[List[ClusterPrinterStatus]]
|
||||||
self._received_print_jobs = None # type: Optional[List[CloudClusterPrintJobStatus]]
|
self._received_print_jobs = None # type: Optional[List[ClusterPrintJobStatus]]
|
||||||
|
|
||||||
# A set of the user's job IDs that have finished
|
|
||||||
self._finished_jobs = set() # type: Set[str]
|
|
||||||
|
|
||||||
# Reference to the uploaded print job / mesh
|
# Reference to the uploaded print job / mesh
|
||||||
|
# We do this to prevent re-uploading the same file multiple times.
|
||||||
self._tool_path = None # type: Optional[bytes]
|
self._tool_path = None # type: Optional[bytes]
|
||||||
self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse]
|
self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse]
|
||||||
|
|
||||||
|
@ -128,9 +107,12 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
|
||||||
super().connect()
|
super().connect()
|
||||||
Logger.log("i", "Connected to cluster %s", self.key)
|
Logger.log("i", "Connected to cluster %s", self.key)
|
||||||
CuraApplication.getInstance().getBackend().backendStateChange.connect(self._onBackendStateChange)
|
CuraApplication.getInstance().getBackend().backendStateChange.connect(self._onBackendStateChange)
|
||||||
|
self._update()
|
||||||
|
|
||||||
## Disconnects the device
|
## Disconnects the device
|
||||||
def disconnect(self) -> None:
|
def disconnect(self) -> None:
|
||||||
|
if not self.isConnected():
|
||||||
|
return
|
||||||
super().disconnect()
|
super().disconnect()
|
||||||
Logger.log("i", "Disconnected from cluster %s", self.key)
|
Logger.log("i", "Disconnected from cluster %s", self.key)
|
||||||
CuraApplication.getInstance().getBackend().backendStateChange.disconnect(self._onBackendStateChange)
|
CuraApplication.getInstance().getBackend().backendStateChange.disconnect(self._onBackendStateChange)
|
||||||
|
@ -140,82 +122,30 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
|
||||||
self._tool_path = None
|
self._tool_path = None
|
||||||
self._uploaded_print_job = None
|
self._uploaded_print_job = None
|
||||||
|
|
||||||
## Gets the cluster response from which this device was created.
|
|
||||||
@property
|
|
||||||
def clusterData(self) -> CloudClusterResponse:
|
|
||||||
return self._cluster
|
|
||||||
|
|
||||||
## Updates the cluster data from the cloud.
|
|
||||||
@clusterData.setter
|
|
||||||
def clusterData(self, value: CloudClusterResponse) -> None:
|
|
||||||
self._cluster = value
|
|
||||||
|
|
||||||
## Checks whether the given network key is found in the cloud's host name
|
## Checks whether the given network key is found in the cloud's host name
|
||||||
def matchesNetworkKey(self, network_key: str) -> bool:
|
def matchesNetworkKey(self, network_key: str) -> bool:
|
||||||
# Typically, a network key looks like "ultimakersystem-aabbccdd0011._ultimaker._tcp.local."
|
# Typically, a network key looks like "ultimakersystem-aabbccdd0011._ultimaker._tcp.local."
|
||||||
# the host name should then be "ultimakersystem-aabbccdd0011"
|
# the host name should then be "ultimakersystem-aabbccdd0011"
|
||||||
if network_key.startswith(self.clusterData.host_name):
|
if network_key.startswith(self.clusterData.host_name):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
# However, for manually added printers, the local IP address is used in lieu of a proper
|
# However, for manually added printers, the local IP address is used in lieu of a proper
|
||||||
# network key, so check for that as well
|
# network key, so check for that as well
|
||||||
if self.clusterData.host_internal_ip is not None and network_key.find(self.clusterData.host_internal_ip):
|
if self.clusterData.host_internal_ip is not None and network_key in self.clusterData.host_internal_ip:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
## Set all the interface elements and texts for this output device.
|
## Set all the interface elements and texts for this output device.
|
||||||
def _setInterfaceElements(self) -> None:
|
def _setInterfaceElements(self) -> None:
|
||||||
self.setPriority(2) # Make sure we end up below the local networking and above 'save to file'
|
self.setPriority(2) # Make sure we end up below the local networking and above 'save to file'.
|
||||||
self.setName(self._id)
|
|
||||||
self.setShortDescription(I18N_CATALOG.i18nc("@action:button", "Print via Cloud"))
|
self.setShortDescription(I18N_CATALOG.i18nc("@action:button", "Print via Cloud"))
|
||||||
self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via Cloud"))
|
self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via Cloud"))
|
||||||
self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via Cloud"))
|
self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via Cloud"))
|
||||||
|
|
||||||
## Called when Cura requests an output device to receive a (G-code) file.
|
|
||||||
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
|
||||||
file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
|
|
||||||
|
|
||||||
# Show an error message if we're already sending a job.
|
|
||||||
if self._progress.visible:
|
|
||||||
message = Message(
|
|
||||||
text = I18N_CATALOG.i18nc("@info:status", "Sending new jobs (temporarily) blocked, still sending the previous print job."),
|
|
||||||
title = I18N_CATALOG.i18nc("@info:title", "Cloud error"),
|
|
||||||
lifetime = 10
|
|
||||||
)
|
|
||||||
message.show()
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._uploaded_print_job:
|
|
||||||
# The mesh didn't change, let's not upload it again
|
|
||||||
self._api.requestPrint(self.key, self._uploaded_print_job.job_id, self._onPrintUploadCompleted)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Indicate we have started sending a job.
|
|
||||||
self.writeStarted.emit(self)
|
|
||||||
|
|
||||||
mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion)
|
|
||||||
if not mesh_format.is_valid:
|
|
||||||
Logger.log("e", "Missing file or mesh writer!")
|
|
||||||
return self._onUploadError(I18N_CATALOG.i18nc("@info:status", "Could not export print job."))
|
|
||||||
|
|
||||||
mesh = mesh_format.getBytes(nodes)
|
|
||||||
|
|
||||||
self._tool_path = mesh
|
|
||||||
request = CloudPrintJobUploadRequest(
|
|
||||||
job_name = file_name or mesh_format.file_extension,
|
|
||||||
file_size = len(mesh),
|
|
||||||
content_type = mesh_format.mime_type,
|
|
||||||
)
|
|
||||||
self._api.requestUpload(request, self._onPrintJobCreated)
|
|
||||||
|
|
||||||
## Called when the network data should be updated.
|
## Called when the network data should be updated.
|
||||||
def _update(self) -> None:
|
def _update(self) -> None:
|
||||||
super()._update()
|
super()._update()
|
||||||
if self._last_request_time and time() - self._last_request_time < self.CHECK_CLUSTER_INTERVAL:
|
if self._last_request_time and time() - self._last_request_time < self.CHECK_CLUSTER_INTERVAL:
|
||||||
return # Avoid calling the cloud too often
|
return # Avoid calling the cloud too often
|
||||||
|
|
||||||
Logger.log("d", "Updating: %s - %s >= %s", time(), self._last_request_time, self.CHECK_CLUSTER_INTERVAL)
|
|
||||||
if self._account.isLoggedIn:
|
if self._account.isLoggedIn:
|
||||||
self.setAuthenticationState(AuthState.Authenticated)
|
self.setAuthenticationState(AuthState.Authenticated)
|
||||||
self._last_request_time = time()
|
self._last_request_time = time()
|
||||||
|
@ -231,108 +161,54 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
|
||||||
if self._received_printers != status.printers:
|
if self._received_printers != status.printers:
|
||||||
self._received_printers = status.printers
|
self._received_printers = status.printers
|
||||||
self._updatePrinters(status.printers)
|
self._updatePrinters(status.printers)
|
||||||
|
|
||||||
if status.print_jobs != self._received_print_jobs:
|
if status.print_jobs != self._received_print_jobs:
|
||||||
self._received_print_jobs = status.print_jobs
|
self._received_print_jobs = status.print_jobs
|
||||||
self._updatePrintJobs(status.print_jobs)
|
self._updatePrintJobs(status.print_jobs)
|
||||||
|
|
||||||
## Updates the local list of printers with the list received from the cloud.
|
## Called when Cura requests an output device to receive a (G-code) file.
|
||||||
# \param jobs: The printers received from the cloud.
|
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||||
def _updatePrinters(self, printers: List[CloudClusterPrinterStatus]) -> None:
|
file_handler: Optional[FileHandler] = None, filter_by_machine: bool = False, **kwargs) -> None:
|
||||||
previous = {p.key: p for p in self._printers} # type: Dict[str, PrinterOutputModel]
|
|
||||||
received = {p.uuid: p for p in printers} # type: Dict[str, CloudClusterPrinterStatus]
|
|
||||||
removed_printers, added_printers, updated_printers = findChanges(previous, received)
|
|
||||||
|
|
||||||
for removed_printer in removed_printers:
|
# Show an error message if we're already sending a job.
|
||||||
if self._active_printer == removed_printer:
|
if self._progress.visible:
|
||||||
self.setActivePrinter(None)
|
PrintJobUploadBlockedMessage().show()
|
||||||
self._printers.remove(removed_printer)
|
|
||||||
|
|
||||||
for added_printer in added_printers:
|
|
||||||
self._printers.append(added_printer.createOutputModel(CloudOutputController(self)))
|
|
||||||
|
|
||||||
for model, printer in updated_printers:
|
|
||||||
printer.updateOutputModel(model)
|
|
||||||
|
|
||||||
# Always have an active printer
|
|
||||||
if self._printers and not self._active_printer:
|
|
||||||
self.setActivePrinter(self._printers[0])
|
|
||||||
|
|
||||||
if added_printers or removed_printers:
|
|
||||||
self.printersChanged.emit()
|
|
||||||
|
|
||||||
## Updates the local list of print jobs with the list received from the cloud.
|
|
||||||
# \param jobs: The print jobs received from the cloud.
|
|
||||||
def _updatePrintJobs(self, jobs: List[CloudClusterPrintJobStatus]) -> None:
|
|
||||||
received = {j.uuid: j for j in jobs} # type: Dict[str, CloudClusterPrintJobStatus]
|
|
||||||
previous = {j.key: j for j in self._print_jobs} # type: Dict[str, UM3PrintJobOutputModel]
|
|
||||||
|
|
||||||
removed_jobs, added_jobs, updated_jobs = findChanges(previous, received)
|
|
||||||
|
|
||||||
for removed_job in removed_jobs:
|
|
||||||
if removed_job.assignedPrinter:
|
|
||||||
removed_job.assignedPrinter.updateActivePrintJob(None)
|
|
||||||
removed_job.stateChanged.disconnect(self._onPrintJobStateChanged)
|
|
||||||
self._print_jobs.remove(removed_job)
|
|
||||||
|
|
||||||
for added_job in added_jobs:
|
|
||||||
self._addPrintJob(added_job)
|
|
||||||
|
|
||||||
for model, job in updated_jobs:
|
|
||||||
job.updateOutputModel(model)
|
|
||||||
if job.printer_uuid:
|
|
||||||
self._updateAssignedPrinter(model, job.printer_uuid)
|
|
||||||
|
|
||||||
# We only have to update when jobs are added or removed
|
|
||||||
# updated jobs push their changes via their output model
|
|
||||||
if added_jobs or removed_jobs:
|
|
||||||
self.printJobsChanged.emit()
|
|
||||||
|
|
||||||
## Registers a new print job received via the cloud API.
|
|
||||||
# \param job: The print job received.
|
|
||||||
def _addPrintJob(self, job: CloudClusterPrintJobStatus) -> None:
|
|
||||||
model = job.createOutputModel(CloudOutputController(self))
|
|
||||||
model.stateChanged.connect(self._onPrintJobStateChanged)
|
|
||||||
if job.printer_uuid:
|
|
||||||
self._updateAssignedPrinter(model, job.printer_uuid)
|
|
||||||
self._print_jobs.append(model)
|
|
||||||
|
|
||||||
## Handles the event of a change in a print job state
|
|
||||||
def _onPrintJobStateChanged(self) -> None:
|
|
||||||
user_name = self._getUserName()
|
|
||||||
# TODO: confirm that notifications in Cura are still required
|
|
||||||
for job in self._print_jobs:
|
|
||||||
if job.state == "wait_cleanup" and job.key not in self._finished_jobs and job.owner == user_name:
|
|
||||||
self._finished_jobs.add(job.key)
|
|
||||||
Message(
|
|
||||||
title = I18N_CATALOG.i18nc("@info:status", "Print finished"),
|
|
||||||
text = (I18N_CATALOG.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.").format(
|
|
||||||
printer_name = job.assignedPrinter.name,
|
|
||||||
job_name = job.name
|
|
||||||
) if job.assignedPrinter else
|
|
||||||
I18N_CATALOG.i18nc("@info:status", "The print job '{job_name}' was finished.").format(
|
|
||||||
job_name = job.name
|
|
||||||
)),
|
|
||||||
).show()
|
|
||||||
|
|
||||||
## Updates the printer assignment for the given print job model.
|
|
||||||
def _updateAssignedPrinter(self, model: UM3PrintJobOutputModel, printer_uuid: str) -> None:
|
|
||||||
printer = next((p for p in self._printers if printer_uuid == p.key), None)
|
|
||||||
if not printer:
|
|
||||||
Logger.log("w", "Missing printer %s for job %s in %s", model.assignedPrinter, model.key,
|
|
||||||
[p.key for p in self._printers])
|
|
||||||
return
|
return
|
||||||
|
|
||||||
printer.updateActivePrintJob(model)
|
# Indicate we have started sending a job.
|
||||||
model.updateAssignedPrinter(printer)
|
self.writeStarted.emit(self)
|
||||||
|
|
||||||
|
# The mesh didn't change, let's not upload it to the cloud again.
|
||||||
|
# Note that self.writeFinished is called in _onPrintUploadCompleted as well.
|
||||||
|
if self._uploaded_print_job:
|
||||||
|
self._api.requestPrint(self.key, self._uploaded_print_job.job_id, self._onPrintUploadCompleted)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Export the scene to the correct file type.
|
||||||
|
job = ExportFileJob(file_handler=file_handler, nodes=nodes, firmware_version=self.firmwareVersion)
|
||||||
|
job.finished.connect(self._onPrintJobCreated)
|
||||||
|
job.start()
|
||||||
|
|
||||||
|
## Handler for when the print job was created locally.
|
||||||
|
# It can now be sent over the cloud.
|
||||||
|
def _onPrintJobCreated(self, job: ExportFileJob) -> None:
|
||||||
|
output = job.getOutput()
|
||||||
|
self._tool_path = output # store the tool path to prevent re-uploading when printing the same file again
|
||||||
|
request = CloudPrintJobUploadRequest(
|
||||||
|
job_name=job.getFileName(),
|
||||||
|
file_size=len(output),
|
||||||
|
content_type=job.getMimeType(),
|
||||||
|
)
|
||||||
|
self._api.requestUpload(request, self._uploadPrintJob)
|
||||||
|
|
||||||
## Uploads the mesh when the print job was registered with the cloud API.
|
## Uploads the mesh when the print job was registered with the cloud API.
|
||||||
# \param job_response: The response received from the cloud API.
|
# \param job_response: The response received from the cloud API.
|
||||||
def _onPrintJobCreated(self, job_response: CloudPrintJobResponse) -> None:
|
def _uploadPrintJob(self, job_response: CloudPrintJobResponse) -> None:
|
||||||
|
if not self._tool_path:
|
||||||
|
return self._onUploadError()
|
||||||
self._progress.show()
|
self._progress.show()
|
||||||
self._uploaded_print_job = job_response
|
self._uploaded_print_job = job_response # store the last uploaded job to prevent re-upload of the same file
|
||||||
tool_path = cast(bytes, self._tool_path)
|
self._api.uploadToolPath(job_response, self._tool_path, self._onPrintJobUploaded, self._progress.update,
|
||||||
self._api.uploadToolPath(job_response, tool_path, self._onPrintJobUploaded, self._progress.update, self._onUploadError)
|
self._onUploadError)
|
||||||
|
|
||||||
## Requests the print to be sent to the printer when we finished uploading the mesh.
|
## Requests the print to be sent to the printer when we finished uploading the mesh.
|
||||||
def _onPrintJobUploaded(self) -> None:
|
def _onPrintJobUploaded(self) -> None:
|
||||||
|
@ -340,128 +216,67 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice):
|
||||||
print_job = cast(CloudPrintJobResponse, self._uploaded_print_job)
|
print_job = cast(CloudPrintJobResponse, self._uploaded_print_job)
|
||||||
self._api.requestPrint(self.key, print_job.job_id, self._onPrintUploadCompleted)
|
self._api.requestPrint(self.key, print_job.job_id, self._onPrintUploadCompleted)
|
||||||
|
|
||||||
|
## Shows a message when the upload has succeeded
|
||||||
|
# \param response: The response from the cloud API.
|
||||||
|
def _onPrintUploadCompleted(self, response: CloudPrintResponse) -> None:
|
||||||
|
self._progress.hide()
|
||||||
|
PrintJobUploadSuccessMessage().show()
|
||||||
|
self.writeFinished.emit()
|
||||||
|
|
||||||
## Displays the given message if uploading the mesh has failed
|
## Displays the given message if uploading the mesh has failed
|
||||||
# \param message: The message to display.
|
# \param message: The message to display.
|
||||||
def _onUploadError(self, message: str = None) -> None:
|
def _onUploadError(self, message: str = None) -> None:
|
||||||
self._progress.hide()
|
self._progress.hide()
|
||||||
self._uploaded_print_job = None
|
self._uploaded_print_job = None
|
||||||
Message(
|
PrintJobUploadErrorMessage(message).show()
|
||||||
text = message or I18N_CATALOG.i18nc("@info:text", "Could not upload the data to the printer."),
|
|
||||||
title = I18N_CATALOG.i18nc("@info:title", "Cloud error"),
|
|
||||||
lifetime = 10
|
|
||||||
).show()
|
|
||||||
self.writeError.emit()
|
self.writeError.emit()
|
||||||
|
|
||||||
## Shows a message when the upload has succeeded
|
|
||||||
# \param response: The response from the cloud API.
|
|
||||||
def _onPrintUploadCompleted(self, response: CloudPrintResponse) -> None:
|
|
||||||
Logger.log("d", "The cluster will be printing this print job with the ID %s", response.cluster_job_id)
|
|
||||||
self._progress.hide()
|
|
||||||
Message(
|
|
||||||
text = I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."),
|
|
||||||
title = I18N_CATALOG.i18nc("@info:title", "Data Sent"),
|
|
||||||
lifetime = 5
|
|
||||||
).show()
|
|
||||||
self.writeFinished.emit()
|
|
||||||
|
|
||||||
## Whether the printer that this output device represents supports print job actions via the cloud.
|
## Whether the printer that this output device represents supports print job actions via the cloud.
|
||||||
@pyqtProperty(bool, notify = _clusterPrintersChanged)
|
@pyqtProperty(bool, notify=_clusterPrintersChanged)
|
||||||
def supportsPrintJobActions(self) -> bool:
|
def supportsPrintJobActions(self) -> bool:
|
||||||
|
if not self._printers:
|
||||||
|
return False
|
||||||
version_number = self.printers[0].firmwareVersion.split(".")
|
version_number = self.printers[0].firmwareVersion.split(".")
|
||||||
firmware_version = Version([version_number[0], version_number[1], version_number[2]])
|
firmware_version = Version([version_number[0], version_number[1], version_number[2]])
|
||||||
return firmware_version >= self.PRINT_JOB_ACTIONS_MIN_VERSION
|
return firmware_version >= self.PRINT_JOB_ACTIONS_MIN_VERSION
|
||||||
|
|
||||||
## Gets the number of printers in the cluster.
|
## Set the remote print job state.
|
||||||
# We use a minimum of 1 because cloud devices are always a cluster and printer discovery needs it.
|
|
||||||
@pyqtProperty(int, notify = _clusterPrintersChanged)
|
|
||||||
def clusterSize(self) -> int:
|
|
||||||
return max(1, len(self._printers))
|
|
||||||
|
|
||||||
## Gets the remote printers.
|
|
||||||
@pyqtProperty("QVariantList", notify=_clusterPrintersChanged)
|
|
||||||
def printers(self) -> List[PrinterOutputModel]:
|
|
||||||
return self._printers
|
|
||||||
|
|
||||||
## Get the active printer in the UI (monitor page).
|
|
||||||
@pyqtProperty(QObject, notify = activePrinterChanged)
|
|
||||||
def activePrinter(self) -> Optional[PrinterOutputModel]:
|
|
||||||
return self._active_printer
|
|
||||||
|
|
||||||
## Set the active printer in the UI (monitor page).
|
|
||||||
@pyqtSlot(QObject)
|
|
||||||
def setActivePrinter(self, printer: Optional[PrinterOutputModel] = None) -> None:
|
|
||||||
if printer != self._active_printer:
|
|
||||||
self._active_printer = printer
|
|
||||||
self.activePrinterChanged.emit()
|
|
||||||
|
|
||||||
## Get remote print jobs.
|
|
||||||
@pyqtProperty("QVariantList", notify = printJobsChanged)
|
|
||||||
def printJobs(self) -> List[UM3PrintJobOutputModel]:
|
|
||||||
return self._print_jobs
|
|
||||||
|
|
||||||
## Get remote print jobs that are still in the print queue.
|
|
||||||
@pyqtProperty("QVariantList", notify = printJobsChanged)
|
|
||||||
def queuedPrintJobs(self) -> List[UM3PrintJobOutputModel]:
|
|
||||||
return [print_job for print_job in self._print_jobs
|
|
||||||
if print_job.state == "queued" or print_job.state == "error"]
|
|
||||||
|
|
||||||
## Get remote print jobs that are assigned to a printer.
|
|
||||||
@pyqtProperty("QVariantList", notify = printJobsChanged)
|
|
||||||
def activePrintJobs(self) -> List[UM3PrintJobOutputModel]:
|
|
||||||
return [print_job for print_job in self._print_jobs if
|
|
||||||
print_job.assignedPrinter is not None and print_job.state != "queued"]
|
|
||||||
|
|
||||||
def setJobState(self, print_job_uuid: str, state: str) -> None:
|
def setJobState(self, print_job_uuid: str, state: str) -> None:
|
||||||
self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, state)
|
self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, state)
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str, name="sendJobToTop")
|
||||||
def sendJobToTop(self, print_job_uuid: str) -> None:
|
def sendJobToTop(self, print_job_uuid: str) -> None:
|
||||||
self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, "move",
|
self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, "move",
|
||||||
{"list": "queued", "to_position": 0})
|
{"list": "queued", "to_position": 0})
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str, name="deleteJobFromQueue")
|
||||||
def deleteJobFromQueue(self, print_job_uuid: str) -> None:
|
def deleteJobFromQueue(self, print_job_uuid: str) -> None:
|
||||||
self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, "remove")
|
self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, "remove")
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str, name="forceSendJob")
|
||||||
def forceSendJob(self, print_job_uuid: str) -> None:
|
def forceSendJob(self, print_job_uuid: str) -> None:
|
||||||
self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, "force")
|
self._api.doPrintJobAction(self._cluster.cluster_id, print_job_uuid, "force")
|
||||||
|
|
||||||
@pyqtSlot(int, result = str)
|
@pyqtSlot(name="openPrintJobControlPanel")
|
||||||
def formatDuration(self, seconds: int) -> str:
|
|
||||||
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
|
|
||||||
|
|
||||||
@pyqtSlot(int, result = str)
|
|
||||||
def getTimeCompleted(self, time_remaining: int) -> str:
|
|
||||||
return formatTimeCompleted(time_remaining)
|
|
||||||
|
|
||||||
@pyqtSlot(int, result = str)
|
|
||||||
def getDateCompleted(self, time_remaining: int) -> str:
|
|
||||||
return formatDateCompleted(time_remaining)
|
|
||||||
|
|
||||||
@pyqtProperty(bool, notify=printJobsChanged)
|
|
||||||
def receivedPrintJobs(self) -> bool:
|
|
||||||
return bool(self._print_jobs)
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def openPrintJobControlPanel(self) -> None:
|
def openPrintJobControlPanel(self) -> None:
|
||||||
QDesktopServices.openUrl(QUrl("https://mycloud.ultimaker.com"))
|
QDesktopServices.openUrl(QUrl(self.clusterCloudUrl))
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot(name="openPrinterControlPanel")
|
||||||
def openPrinterControlPanel(self) -> None:
|
def openPrinterControlPanel(self) -> None:
|
||||||
QDesktopServices.openUrl(QUrl("https://mycloud.ultimaker.com"))
|
QDesktopServices.openUrl(QUrl(self.clusterCloudUrl))
|
||||||
|
|
||||||
## TODO: The following methods are required by the monitor page QML, but are not actually available using cloud.
|
## Gets the cluster response from which this device was created.
|
||||||
# TODO: We fake the methods here to not break the monitor page.
|
@property
|
||||||
|
def clusterData(self) -> CloudClusterResponse:
|
||||||
|
return self._cluster
|
||||||
|
|
||||||
@pyqtProperty(QUrl, notify = _clusterPrintersChanged)
|
## Updates the cluster data from the cloud.
|
||||||
def activeCameraUrl(self) -> "QUrl":
|
@clusterData.setter
|
||||||
return QUrl()
|
def clusterData(self, value: CloudClusterResponse) -> None:
|
||||||
|
self._cluster = value
|
||||||
|
|
||||||
@pyqtSlot(QUrl)
|
## Gets the URL on which to monitor the cluster via the cloud.
|
||||||
def setActiveCameraUrl(self, camera_url: "QUrl") -> None:
|
@property
|
||||||
pass
|
def clusterCloudUrl(self) -> str:
|
||||||
|
root_url_prefix = "-staging" if self._account.is_staging else ""
|
||||||
@pyqtProperty("QVariantList", notify = _clusterPrintersChanged)
|
return "https://mycloud{}.ultimaker.com/app/jobs/{}".format(root_url_prefix, self.clusterData.cluster_id)
|
||||||
def connectedPrintersTypeCount(self) -> List[Dict[str, str]]:
|
|
||||||
return []
|
|
||||||
|
|
|
@ -1,30 +1,27 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from typing import Dict, List
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from PyQt5.QtCore import QTimer
|
from PyQt5.QtCore import QTimer
|
||||||
|
|
||||||
from UM import i18nCatalog
|
from UM import i18nCatalog
|
||||||
from UM.Logger import Logger
|
|
||||||
from UM.Message import Message
|
|
||||||
from UM.Signal import Signal
|
from UM.Signal import Signal
|
||||||
from cura.API import Account
|
from cura.API import Account
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
|
||||||
from .CloudApiClient import CloudApiClient
|
from .CloudApiClient import CloudApiClient
|
||||||
from .CloudOutputDevice import CloudOutputDevice
|
from .CloudOutputDevice import CloudOutputDevice
|
||||||
from .Models.CloudClusterResponse import CloudClusterResponse
|
from ..Models.Http.CloudClusterResponse import CloudClusterResponse
|
||||||
from .Models.CloudError import CloudError
|
|
||||||
from .Utils import findChanges
|
|
||||||
|
|
||||||
|
|
||||||
## The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters.
|
## The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters.
|
||||||
# Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code.
|
# Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code.
|
||||||
#
|
|
||||||
# API spec is available on https://api.ultimaker.com/docs/connect/spec/.
|
# API spec is available on https://api.ultimaker.com/docs/connect/spec/.
|
||||||
#
|
|
||||||
class CloudOutputDeviceManager:
|
class CloudOutputDeviceManager:
|
||||||
|
|
||||||
META_CLUSTER_ID = "um_cloud_cluster_id"
|
META_CLUSTER_ID = "um_cloud_cluster_id"
|
||||||
|
META_NETWORK_KEY = "um_network_key"
|
||||||
|
|
||||||
# The interval with which the remote clusters are checked
|
# The interval with which the remote clusters are checked
|
||||||
CHECK_CLUSTER_INTERVAL = 30.0 # seconds
|
CHECK_CLUSTER_INTERVAL = 30.0 # seconds
|
||||||
|
@ -32,108 +29,120 @@ class CloudOutputDeviceManager:
|
||||||
# The translation catalog for this device.
|
# The translation catalog for this device.
|
||||||
I18N_CATALOG = i18nCatalog("cura")
|
I18N_CATALOG = i18nCatalog("cura")
|
||||||
|
|
||||||
addedCloudCluster = Signal()
|
# Signal emitted when the list of discovered devices changed.
|
||||||
removedCloudCluster = Signal()
|
discoveredDevicesChanged = Signal()
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
# Persistent dict containing the remote clusters for the authenticated user.
|
# Persistent dict containing the remote clusters for the authenticated user.
|
||||||
self._remote_clusters = {} # type: Dict[str, CloudOutputDevice]
|
self._remote_clusters = {} # type: Dict[str, CloudOutputDevice]
|
||||||
|
self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account
|
||||||
self._application = CuraApplication.getInstance()
|
self._api = CloudApiClient(self._account, on_error=lambda error: print(error))
|
||||||
self._output_device_manager = self._application.getOutputDeviceManager()
|
self._account.loginStateChanged.connect(self._onLoginStateChanged)
|
||||||
|
|
||||||
self._account = self._application.getCuraAPI().account # type: Account
|
|
||||||
self._api = CloudApiClient(self._account, self._onApiError)
|
|
||||||
|
|
||||||
# Create a timer to update the remote cluster list
|
# Create a timer to update the remote cluster list
|
||||||
self._update_timer = QTimer()
|
self._update_timer = QTimer()
|
||||||
self._update_timer.setInterval(int(self.CHECK_CLUSTER_INTERVAL * 1000))
|
self._update_timer.setInterval(int(self.CHECK_CLUSTER_INTERVAL * 1000))
|
||||||
self._update_timer.setSingleShot(False)
|
self._update_timer.setSingleShot(False)
|
||||||
|
self._update_timer.timeout.connect(self._getRemoteClusters)
|
||||||
|
|
||||||
|
# Ensure we don't start twice.
|
||||||
self._running = False
|
self._running = False
|
||||||
|
|
||||||
# Called when the uses logs in or out
|
## Starts running the cloud output device manager, thus periodically requesting cloud data.
|
||||||
def _onLoginStateChanged(self, is_logged_in: bool) -> None:
|
def start(self):
|
||||||
Logger.log("d", "Log in state changed to %s", is_logged_in)
|
if self._running:
|
||||||
if is_logged_in:
|
return
|
||||||
|
if not self._account.isLoggedIn:
|
||||||
|
return
|
||||||
|
self._running = True
|
||||||
if not self._update_timer.isActive():
|
if not self._update_timer.isActive():
|
||||||
self._update_timer.start()
|
self._update_timer.start()
|
||||||
self._getRemoteClusters()
|
self._getRemoteClusters()
|
||||||
else:
|
|
||||||
|
## Stops running the cloud output device manager.
|
||||||
|
def stop(self):
|
||||||
|
if not self._running:
|
||||||
|
return
|
||||||
|
self._running = False
|
||||||
if self._update_timer.isActive():
|
if self._update_timer.isActive():
|
||||||
self._update_timer.stop()
|
self._update_timer.stop()
|
||||||
|
self._onGetRemoteClustersFinished([]) # Make sure we remove all cloud output devices.
|
||||||
|
|
||||||
# Notify that all clusters have disappeared
|
## Force refreshing connections.
|
||||||
self._onGetRemoteClustersFinished([])
|
def refreshConnections(self) -> None:
|
||||||
|
self._connectToActiveMachine()
|
||||||
|
|
||||||
|
## Called when the uses logs in or out
|
||||||
|
def _onLoginStateChanged(self, is_logged_in: bool) -> None:
|
||||||
|
if is_logged_in:
|
||||||
|
self.start()
|
||||||
|
else:
|
||||||
|
self.stop()
|
||||||
|
|
||||||
## Gets all remote clusters from the API.
|
## Gets all remote clusters from the API.
|
||||||
def _getRemoteClusters(self) -> None:
|
def _getRemoteClusters(self) -> None:
|
||||||
Logger.log("d", "Retrieving remote clusters")
|
|
||||||
self._api.getClusters(self._onGetRemoteClustersFinished)
|
self._api.getClusters(self._onGetRemoteClustersFinished)
|
||||||
|
|
||||||
## Callback for when the request for getting the clusters. is finished.
|
## Callback for when the request for getting the clusters is finished.
|
||||||
def _onGetRemoteClustersFinished(self, clusters: List[CloudClusterResponse]) -> None:
|
def _onGetRemoteClustersFinished(self, clusters: List[CloudClusterResponse]) -> None:
|
||||||
online_clusters = {c.cluster_id: c for c in clusters if c.is_online} # type: Dict[str, CloudClusterResponse]
|
online_clusters = {c.cluster_id: c for c in clusters if c.is_online} # type: Dict[str, CloudClusterResponse]
|
||||||
|
for device_id, cluster_data in online_clusters.items():
|
||||||
|
if device_id not in self._remote_clusters:
|
||||||
|
self._onDeviceDiscovered(cluster_data)
|
||||||
|
else:
|
||||||
|
self._onDiscoveredDeviceUpdated(cluster_data)
|
||||||
|
|
||||||
removed_devices, added_clusters, updates = findChanges(self._remote_clusters, online_clusters)
|
removed_device_keys = set(self._remote_clusters.keys()) - set(online_clusters.keys())
|
||||||
|
for device_id in removed_device_keys:
|
||||||
|
self._onDiscoveredDeviceRemoved(device_id)
|
||||||
|
|
||||||
Logger.log("d", "Parsed remote clusters to %s", [cluster.toDict() for cluster in online_clusters.values()])
|
def _onDeviceDiscovered(self, cluster_data: CloudClusterResponse) -> None:
|
||||||
Logger.log("d", "Removed: %s, added: %s, updates: %s", len(removed_devices), len(added_clusters), len(updates))
|
device = CloudOutputDevice(self._api, cluster_data)
|
||||||
|
CuraApplication.getInstance().getDiscoveredPrintersModel().addDiscoveredPrinter(
|
||||||
# Remove output devices that are gone
|
ip_address=device.key,
|
||||||
for device in removed_devices:
|
key=device.getId(),
|
||||||
if device.isConnected():
|
name=device.getName(),
|
||||||
device.disconnect()
|
create_callback=self._createMachineFromDiscoveredDevice,
|
||||||
device.close()
|
machine_type=device.printerType,
|
||||||
self._output_device_manager.removeOutputDevice(device.key)
|
device=device
|
||||||
self._application.getDiscoveredPrintersModel().removeDiscoveredPrinter(device.key)
|
|
||||||
self.removedCloudCluster.emit(device)
|
|
||||||
del self._remote_clusters[device.key]
|
|
||||||
|
|
||||||
# Add an output device for each new remote cluster.
|
|
||||||
# We only add when is_online as we don't want the option in the drop down if the cluster is not online.
|
|
||||||
for cluster in added_clusters:
|
|
||||||
device = CloudOutputDevice(self._api, cluster)
|
|
||||||
self._remote_clusters[cluster.cluster_id] = device
|
|
||||||
self._application.getDiscoveredPrintersModel().addDiscoveredPrinter(
|
|
||||||
device.key,
|
|
||||||
device.key,
|
|
||||||
cluster.friendly_name,
|
|
||||||
self._createMachineFromDiscoveredPrinter,
|
|
||||||
device.printerType,
|
|
||||||
device
|
|
||||||
)
|
)
|
||||||
self.addedCloudCluster.emit(cluster)
|
self._remote_clusters[device.getId()] = device
|
||||||
|
self.discoveredDevicesChanged.emit()
|
||||||
# Update the output devices
|
|
||||||
for device, cluster in updates:
|
|
||||||
device.clusterData = cluster
|
|
||||||
self._application.getDiscoveredPrintersModel().updateDiscoveredPrinter(
|
|
||||||
device.key,
|
|
||||||
cluster.friendly_name,
|
|
||||||
device.printerType,
|
|
||||||
)
|
|
||||||
|
|
||||||
self._connectToActiveMachine()
|
self._connectToActiveMachine()
|
||||||
|
|
||||||
def _createMachineFromDiscoveredPrinter(self, key: str) -> None:
|
def _onDiscoveredDeviceUpdated(self, cluster_data: CloudClusterResponse) -> None:
|
||||||
device = self._remote_clusters[key] # type: CloudOutputDevice
|
device = self._remote_clusters.get(cluster_data.cluster_id)
|
||||||
|
if not device:
|
||||||
|
return
|
||||||
|
CuraApplication.getInstance().getDiscoveredPrintersModel().updateDiscoveredPrinter(
|
||||||
|
ip_address=device.key,
|
||||||
|
name=cluster_data.friendly_name,
|
||||||
|
machine_type=device.printerType
|
||||||
|
)
|
||||||
|
self.discoveredDevicesChanged.emit()
|
||||||
|
|
||||||
|
def _onDiscoveredDeviceRemoved(self, device_id: str) -> None:
|
||||||
|
device = self._remote_clusters.pop(device_id, None) # type: Optional[CloudOutputDevice]
|
||||||
|
if not device:
|
||||||
|
return
|
||||||
|
device.close()
|
||||||
|
CuraApplication.getInstance().getDiscoveredPrintersModel().removeDiscoveredPrinter(device.key)
|
||||||
|
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
|
||||||
|
if device.key in output_device_manager.getOutputDeviceIds():
|
||||||
|
output_device_manager.removeOutputDevice(device.key)
|
||||||
|
self.discoveredDevicesChanged.emit()
|
||||||
|
|
||||||
|
def _createMachineFromDiscoveredDevice(self, key: str) -> None:
|
||||||
|
device = self._remote_clusters[key]
|
||||||
if not device:
|
if not device:
|
||||||
Logger.log("e", "Could not find discovered device with key [%s]", key)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
group_name = device.clusterData.friendly_name
|
|
||||||
machine_type_id = device.printerType
|
|
||||||
|
|
||||||
Logger.log("i", "Creating machine from cloud device with key = [%s], group name = [%s], printer type = [%s]",
|
|
||||||
key, group_name, machine_type_id)
|
|
||||||
|
|
||||||
# The newly added machine is automatically activated.
|
# The newly added machine is automatically activated.
|
||||||
self._application.getMachineManager().addMachine(machine_type_id, group_name)
|
machine_manager = CuraApplication.getInstance().getMachineManager()
|
||||||
|
machine_manager.addMachine(device.printerType, device.clusterData.friendly_name)
|
||||||
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
|
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
if not active_machine:
|
if not active_machine:
|
||||||
return
|
return
|
||||||
|
|
||||||
active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
|
active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
|
||||||
self._connectToOutputDevice(device, active_machine)
|
self._connectToOutputDevice(device, active_machine)
|
||||||
|
|
||||||
|
@ -143,69 +152,24 @@ class CloudOutputDeviceManager:
|
||||||
if not active_machine:
|
if not active_machine:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Remove all output devices that we have registered.
|
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
|
||||||
# This is needed because when we switch machines we can only leave
|
|
||||||
# output devices that are meant for that machine.
|
|
||||||
for stored_cluster_id in self._remote_clusters:
|
|
||||||
self._output_device_manager.removeOutputDevice(stored_cluster_id)
|
|
||||||
|
|
||||||
# Check if the stored cluster_id for the active machine is in our list of remote clusters.
|
|
||||||
stored_cluster_id = active_machine.getMetaDataEntry(self.META_CLUSTER_ID)
|
stored_cluster_id = active_machine.getMetaDataEntry(self.META_CLUSTER_ID)
|
||||||
if stored_cluster_id in self._remote_clusters:
|
local_network_key = active_machine.getMetaDataEntry(self.META_NETWORK_KEY)
|
||||||
device = self._remote_clusters[stored_cluster_id]
|
for device in self._remote_clusters.values():
|
||||||
|
if device.key == stored_cluster_id:
|
||||||
|
# Connect to it if the stored ID matches.
|
||||||
self._connectToOutputDevice(device, active_machine)
|
self._connectToOutputDevice(device, active_machine)
|
||||||
Logger.log("d", "Device connected by metadata cluster ID %s", stored_cluster_id)
|
elif local_network_key and device.matchesNetworkKey(local_network_key):
|
||||||
else:
|
# Connect to it if we can match the local network key that was already present.
|
||||||
self._connectByNetworkKey(active_machine)
|
|
||||||
|
|
||||||
## Tries to match the local network key to the cloud cluster host name.
|
|
||||||
def _connectByNetworkKey(self, active_machine: GlobalStack) -> None:
|
|
||||||
# Check if the active printer has a local network connection and match this key to the remote cluster.
|
|
||||||
local_network_key = active_machine.getMetaDataEntry("um_network_key")
|
|
||||||
if not local_network_key:
|
|
||||||
return
|
|
||||||
|
|
||||||
device = next((c for c in self._remote_clusters.values() if c.matchesNetworkKey(local_network_key)), None)
|
|
||||||
if not device:
|
|
||||||
return
|
|
||||||
|
|
||||||
Logger.log("i", "Found cluster %s with network key %s", device, local_network_key)
|
|
||||||
active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
|
active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
|
||||||
self._connectToOutputDevice(device, active_machine)
|
self._connectToOutputDevice(device, active_machine)
|
||||||
|
elif device.key in output_device_manager.getOutputDeviceIds():
|
||||||
|
# Remove device if it is not meant for the active machine.
|
||||||
|
output_device_manager.removeOutputDevice(device.key)
|
||||||
|
|
||||||
## Connects to an output device and makes sure it is registered in the output device manager.
|
## Connects to an output device and makes sure it is registered in the output device manager.
|
||||||
def _connectToOutputDevice(self, device: CloudOutputDevice, active_machine: GlobalStack) -> None:
|
@staticmethod
|
||||||
|
def _connectToOutputDevice(device: CloudOutputDevice, active_machine: GlobalStack) -> None:
|
||||||
device.connect()
|
device.connect()
|
||||||
self._output_device_manager.addOutputDevice(device)
|
|
||||||
active_machine.addConfiguredConnectionType(device.connectionType.value)
|
active_machine.addConfiguredConnectionType(device.connectionType.value)
|
||||||
|
CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device)
|
||||||
## Handles an API error received from the cloud.
|
|
||||||
# \param errors: The errors received
|
|
||||||
def _onApiError(self, errors: List[CloudError] = None) -> None:
|
|
||||||
Logger.log("w", str(errors))
|
|
||||||
message = Message(
|
|
||||||
text = self.I18N_CATALOG.i18nc("@info:description", "There was an error connecting to the cloud."),
|
|
||||||
title = self.I18N_CATALOG.i18nc("@info:title", "Error"),
|
|
||||||
lifetime = 10
|
|
||||||
)
|
|
||||||
message.show()
|
|
||||||
|
|
||||||
## Starts running the cloud output device manager, thus periodically requesting cloud data.
|
|
||||||
def start(self):
|
|
||||||
if self._running:
|
|
||||||
return
|
|
||||||
self._account.loginStateChanged.connect(self._onLoginStateChanged)
|
|
||||||
# When switching machines we check if we have to activate a remote cluster.
|
|
||||||
self._application.globalContainerStackChanged.connect(self._connectToActiveMachine)
|
|
||||||
self._update_timer.timeout.connect(self._getRemoteClusters)
|
|
||||||
self._onLoginStateChanged(is_logged_in = self._account.isLoggedIn)
|
|
||||||
|
|
||||||
## Stops running the cloud output device manager.
|
|
||||||
def stop(self):
|
|
||||||
if not self._running:
|
|
||||||
return
|
|
||||||
self._account.loginStateChanged.disconnect(self._onLoginStateChanged)
|
|
||||||
# When switching machines we check if we have to activate a remote cluster.
|
|
||||||
self._application.globalContainerStackChanged.disconnect(self._connectToActiveMachine)
|
|
||||||
self._update_timer.timeout.disconnect(self._getRemoteClusters)
|
|
||||||
self._onLoginStateChanged(is_logged_in = False)
|
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
from .BaseCloudModel import BaseCloudModel
|
|
||||||
|
|
||||||
|
|
||||||
## Class representing a cluster printer
|
|
||||||
# Spec: https://api-staging.ultimaker.com/connect/v1/spec
|
|
||||||
class CloudClusterBuildPlate(BaseCloudModel):
|
|
||||||
## Create a new build plate
|
|
||||||
# \param type: The type of buildplate glass or aluminium
|
|
||||||
def __init__(self, type: str = "glass", **kwargs) -> None:
|
|
||||||
self.type = type
|
|
||||||
super().__init__(**kwargs)
|
|
|
@ -1,2 +0,0 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# !/usr/bin/env python
|
# !/usr/bin/env python
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from PyQt5.QtCore import QUrl
|
from PyQt5.QtCore import QUrl
|
||||||
|
@ -6,7 +6,8 @@ from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManage
|
||||||
from typing import Optional, Callable, Any, Tuple, cast
|
from typing import Optional, Callable, Any, Tuple, cast
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from .Models.CloudPrintJobResponse import CloudPrintJobResponse
|
|
||||||
|
from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse
|
||||||
|
|
||||||
|
|
||||||
## Class responsible for uploading meshes to the cloud in separate requests.
|
## Class responsible for uploading meshes to the cloud in separate requests.
|
||||||
|
|
|
@ -1,54 +0,0 @@
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from typing import TypeVar, Dict, Tuple, List
|
|
||||||
|
|
||||||
from UM import i18nCatalog
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
|
||||||
U = TypeVar("U")
|
|
||||||
|
|
||||||
|
|
||||||
## Splits the given dictionaries into three lists (in a tuple):
|
|
||||||
# - `removed`: Items that were in the first argument but removed in the second one.
|
|
||||||
# - `added`: Items that were not in the first argument but were included in the second one.
|
|
||||||
# - `updated`: Items that were in both dictionaries. Both values are given in a tuple.
|
|
||||||
# \param previous: The previous items
|
|
||||||
# \param received: The received items
|
|
||||||
# \return: The tuple (removed, added, updated) as explained above.
|
|
||||||
def findChanges(previous: Dict[str, T], received: Dict[str, U]) -> Tuple[List[T], List[U], List[Tuple[T, U]]]:
|
|
||||||
previous_ids = set(previous)
|
|
||||||
received_ids = set(received)
|
|
||||||
|
|
||||||
removed_ids = previous_ids.difference(received_ids)
|
|
||||||
new_ids = received_ids.difference(previous_ids)
|
|
||||||
updated_ids = received_ids.intersection(previous_ids)
|
|
||||||
|
|
||||||
removed = [previous[removed_id] for removed_id in removed_ids]
|
|
||||||
added = [received[new_id] for new_id in new_ids]
|
|
||||||
updated = [(previous[updated_id], received[updated_id]) for updated_id in updated_ids]
|
|
||||||
|
|
||||||
return removed, added, updated
|
|
||||||
|
|
||||||
|
|
||||||
def formatTimeCompleted(seconds_remaining: int) -> str:
|
|
||||||
completed = datetime.now() + timedelta(seconds=seconds_remaining)
|
|
||||||
return "{hour:02d}:{minute:02d}".format(hour = completed.hour, minute = completed.minute)
|
|
||||||
|
|
||||||
|
|
||||||
def formatDateCompleted(seconds_remaining: int) -> str:
|
|
||||||
now = datetime.now()
|
|
||||||
completed = now + timedelta(seconds=seconds_remaining)
|
|
||||||
days = (completed.date() - now.date()).days
|
|
||||||
i18n = i18nCatalog("cura")
|
|
||||||
|
|
||||||
# If finishing date is more than 7 days out, using "Mon Dec 3 at HH:MM" format
|
|
||||||
if days >= 7:
|
|
||||||
return completed.strftime("%a %b ") + "{day}".format(day = completed.day)
|
|
||||||
# If finishing date is within the next week, use "Monday at HH:MM" format
|
|
||||||
elif days >= 2:
|
|
||||||
return completed.strftime("%a")
|
|
||||||
# If finishing tomorrow, use "tomorrow at HH:MM" format
|
|
||||||
elif days >= 1:
|
|
||||||
return i18n.i18nc("@info:status", "tomorrow")
|
|
||||||
# If finishing today, use "today at HH:MM" format
|
|
||||||
else:
|
|
||||||
return i18n.i18nc("@info:status", "today")
|
|
|
@ -1,19 +1,14 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
|
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||||
|
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .CloudOutputDevice import CloudOutputDevice
|
|
||||||
|
|
||||||
|
|
||||||
class CloudOutputController(PrinterOutputController):
|
class ClusterOutputController(PrinterOutputController):
|
||||||
def __init__(self, output_device: "CloudOutputDevice") -> None:
|
|
||||||
|
def __init__(self, output_device: PrinterOutputDevice) -> None:
|
||||||
super().__init__(output_device)
|
super().__init__(output_device)
|
||||||
|
|
||||||
# The cloud connection only supports fetching the printer and queue status and adding a job to the queue.
|
|
||||||
# To let the UI know this we mark all features below as False.
|
|
||||||
self.can_pause = True
|
self.can_pause = True
|
||||||
self.can_abort = True
|
self.can_abort = True
|
||||||
self.can_pre_heat_bed = False
|
self.can_pre_heat_bed = False
|
||||||
|
@ -22,5 +17,5 @@ class CloudOutputController(PrinterOutputController):
|
||||||
self.can_control_manually = False
|
self.can_control_manually = False
|
||||||
self.can_update_firmware = False
|
self.can_update_firmware = False
|
||||||
|
|
||||||
def setJobState(self, job: "PrintJobOutputModel", state: str):
|
def setJobState(self, job: PrintJobOutputModel, state: str):
|
||||||
self._output_device.setJobState(job.key, state)
|
self._output_device.setJobState(job.key, state)
|
|
@ -1,717 +0,0 @@
|
||||||
# Copyright (c) 2019 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
from typing import Any, cast, Tuple, Union, Optional, Dict, List
|
|
||||||
from time import time
|
|
||||||
|
|
||||||
import io # To create the correct buffers for sending data to the printer.
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
from UM.FileHandler.FileHandler import FileHandler
|
|
||||||
from UM.FileHandler.WriteFileJob import WriteFileJob # To call the file writer asynchronously.
|
|
||||||
from UM.i18n import i18nCatalog
|
|
||||||
from UM.Logger import Logger
|
|
||||||
from UM.Message import Message
|
|
||||||
from UM.PluginRegistry import PluginRegistry
|
|
||||||
from UM.Qt.Duration import Duration, DurationFormat
|
|
||||||
from UM.Scene.SceneNode import SceneNode # For typing.
|
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
|
||||||
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
|
||||||
from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
|
|
||||||
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice
|
|
||||||
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
|
||||||
from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel
|
|
||||||
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
|
|
||||||
|
|
||||||
from .Cloud.Utils import formatTimeCompleted, formatDateCompleted
|
|
||||||
from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController
|
|
||||||
from .ConfigurationChangeModel import ConfigurationChangeModel
|
|
||||||
from .MeshFormatHandler import MeshFormatHandler
|
|
||||||
from .SendMaterialJob import SendMaterialJob
|
|
||||||
from .UM3PrintJobOutputModel import UM3PrintJobOutputModel
|
|
||||||
|
|
||||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
|
|
||||||
from PyQt5.QtGui import QDesktopServices, QImage
|
|
||||||
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
|
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
|
||||||
|
|
||||||
|
|
||||||
class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
|
|
||||||
printJobsChanged = pyqtSignal()
|
|
||||||
activePrinterChanged = pyqtSignal()
|
|
||||||
activeCameraUrlChanged = pyqtSignal()
|
|
||||||
receivedPrintJobsChanged = pyqtSignal()
|
|
||||||
|
|
||||||
# Notify can only use signals that are defined by the class that they are in, not inherited ones.
|
|
||||||
# Therefore we create a private signal used to trigger the printersChanged signal.
|
|
||||||
_clusterPrintersChanged = pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self, device_id, address, properties, parent = None) -> None:
|
|
||||||
super().__init__(device_id = device_id, address = address, properties=properties, connection_type = ConnectionType.NetworkConnection, parent = parent)
|
|
||||||
self._api_prefix = "/cluster-api/v1/"
|
|
||||||
|
|
||||||
self._application = CuraApplication.getInstance()
|
|
||||||
|
|
||||||
self._number_of_extruders = 2
|
|
||||||
|
|
||||||
self._dummy_lambdas = (
|
|
||||||
"", {}, io.BytesIO()
|
|
||||||
) # type: Tuple[Optional[str], Dict[str, Union[str, int, bool]], Union[io.StringIO, io.BytesIO]]
|
|
||||||
|
|
||||||
self._print_jobs = [] # type: List[UM3PrintJobOutputModel]
|
|
||||||
self._received_print_jobs = False # type: bool
|
|
||||||
|
|
||||||
if PluginRegistry.getInstance() is not None:
|
|
||||||
plugin_path = PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting")
|
|
||||||
if plugin_path is None:
|
|
||||||
Logger.log("e", "Cloud not find plugin path for plugin UM3NetworkPrnting")
|
|
||||||
raise RuntimeError("Cloud not find plugin path for plugin UM3NetworkPrnting")
|
|
||||||
self._monitor_view_qml_path = os.path.join(plugin_path, "resources", "qml", "MonitorStage.qml")
|
|
||||||
|
|
||||||
# Trigger the printersChanged signal when the private signal is triggered
|
|
||||||
self.printersChanged.connect(self._clusterPrintersChanged)
|
|
||||||
|
|
||||||
self._accepts_commands = True # type: bool
|
|
||||||
|
|
||||||
# Cluster does not have authentication, so default to authenticated
|
|
||||||
self._authentication_state = AuthState.Authenticated
|
|
||||||
|
|
||||||
self._error_message = None # type: Optional[Message]
|
|
||||||
self._write_job_progress_message = None # type: Optional[Message]
|
|
||||||
self._progress_message = None # type: Optional[Message]
|
|
||||||
|
|
||||||
self._active_printer = None # type: Optional[PrinterOutputModel]
|
|
||||||
|
|
||||||
self._printer_selection_dialog = None # type: QObject
|
|
||||||
|
|
||||||
self.setPriority(3) # Make sure the output device gets selected above local file output
|
|
||||||
self.setName(self._id)
|
|
||||||
self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
|
|
||||||
self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))
|
|
||||||
|
|
||||||
self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network"))
|
|
||||||
|
|
||||||
self._printer_uuid_to_unique_name_mapping = {} # type: Dict[str, str]
|
|
||||||
|
|
||||||
self._finished_jobs = [] # type: List[UM3PrintJobOutputModel]
|
|
||||||
|
|
||||||
self._cluster_size = int(properties.get(b"cluster_size", 0)) # type: int
|
|
||||||
|
|
||||||
self._latest_reply_handler = None # type: Optional[QNetworkReply]
|
|
||||||
self._sending_job = None
|
|
||||||
|
|
||||||
self._active_camera_url = QUrl() # type: QUrl
|
|
||||||
|
|
||||||
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
|
||||||
file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
|
|
||||||
self.writeStarted.emit(self)
|
|
||||||
|
|
||||||
self.sendMaterialProfiles()
|
|
||||||
|
|
||||||
mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion)
|
|
||||||
|
|
||||||
# This function pauses with the yield, waiting on instructions on which printer it needs to print with.
|
|
||||||
if not mesh_format.is_valid:
|
|
||||||
Logger.log("e", "Missing file or mesh writer!")
|
|
||||||
return
|
|
||||||
self._sending_job = self._sendPrintJob(mesh_format, nodes)
|
|
||||||
if self._sending_job is not None:
|
|
||||||
self._sending_job.send(None) # Start the generator.
|
|
||||||
|
|
||||||
if len(self._printers) > 1: # We need to ask the user.
|
|
||||||
self._spawnPrinterSelectionDialog()
|
|
||||||
is_job_sent = True
|
|
||||||
else: # Just immediately continue.
|
|
||||||
self._sending_job.send("") # No specifically selected printer.
|
|
||||||
is_job_sent = self._sending_job.send(None)
|
|
||||||
|
|
||||||
def _spawnPrinterSelectionDialog(self):
|
|
||||||
if self._printer_selection_dialog is None:
|
|
||||||
if PluginRegistry.getInstance() is not None:
|
|
||||||
path = os.path.join(
|
|
||||||
PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
|
|
||||||
"resources", "qml", "PrintWindow.qml"
|
|
||||||
)
|
|
||||||
self._printer_selection_dialog = self._application.createQmlComponent(path, {"OutputDevice": self})
|
|
||||||
if self._printer_selection_dialog is not None:
|
|
||||||
self._printer_selection_dialog.show()
|
|
||||||
|
|
||||||
## Whether the printer that this output device represents supports print job actions via the local network.
|
|
||||||
@pyqtProperty(bool, constant=True)
|
|
||||||
def supportsPrintJobActions(self) -> bool:
|
|
||||||
return True
|
|
||||||
|
|
||||||
@pyqtProperty(int, constant=True)
|
|
||||||
def clusterSize(self) -> int:
|
|
||||||
return self._cluster_size
|
|
||||||
|
|
||||||
## Allows the user to choose a printer to print with from the printer
|
|
||||||
# selection dialogue.
|
|
||||||
# \param target_printer The name of the printer to target.
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def selectPrinter(self, target_printer: str = "") -> None:
|
|
||||||
if self._sending_job is not None:
|
|
||||||
self._sending_job.send(target_printer)
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def cancelPrintSelection(self) -> None:
|
|
||||||
self._sending_gcode = False
|
|
||||||
|
|
||||||
## Greenlet to send a job to the printer over the network.
|
|
||||||
#
|
|
||||||
# This greenlet gets called asynchronously in requestWrite. It is a
|
|
||||||
# greenlet in order to optionally wait for selectPrinter() to select a
|
|
||||||
# printer.
|
|
||||||
# The greenlet yields exactly three times: First time None,
|
|
||||||
# \param mesh_format Object responsible for choosing the right kind of format to write with.
|
|
||||||
def _sendPrintJob(self, mesh_format: MeshFormatHandler, nodes: List[SceneNode]):
|
|
||||||
Logger.log("i", "Sending print job to printer.")
|
|
||||||
if self._sending_gcode:
|
|
||||||
self._error_message = Message(
|
|
||||||
i18n_catalog.i18nc("@info:status",
|
|
||||||
"Sending new jobs (temporarily) blocked, still sending the previous print job."))
|
|
||||||
self._error_message.show()
|
|
||||||
yield #Wait on the user to select a target printer.
|
|
||||||
yield #Wait for the write job to be finished.
|
|
||||||
yield False #Return whether this was a success or not.
|
|
||||||
yield #Prevent StopIteration.
|
|
||||||
|
|
||||||
self._sending_gcode = True
|
|
||||||
|
|
||||||
# Potentially wait on the user to select a target printer.
|
|
||||||
target_printer = yield # type: Optional[str]
|
|
||||||
|
|
||||||
# Using buffering greatly reduces the write time for many lines of gcode
|
|
||||||
|
|
||||||
stream = mesh_format.createStream()
|
|
||||||
|
|
||||||
job = WriteFileJob(mesh_format.writer, stream, nodes, mesh_format.file_mode)
|
|
||||||
|
|
||||||
self._write_job_progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"),
|
|
||||||
lifetime = 0, dismissable = False, progress = -1,
|
|
||||||
title = i18n_catalog.i18nc("@info:title", "Sending Data"),
|
|
||||||
use_inactivity_timer = False)
|
|
||||||
self._write_job_progress_message.show()
|
|
||||||
|
|
||||||
if mesh_format.preferred_format is not None:
|
|
||||||
self._dummy_lambdas = (target_printer, mesh_format.preferred_format, stream)
|
|
||||||
job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished)
|
|
||||||
job.start()
|
|
||||||
yield True # Return that we had success!
|
|
||||||
yield # To prevent having to catch the StopIteration exception.
|
|
||||||
|
|
||||||
def _sendPrintJobWaitOnWriteJobFinished(self, job: WriteFileJob) -> None:
|
|
||||||
if self._write_job_progress_message:
|
|
||||||
self._write_job_progress_message.hide()
|
|
||||||
|
|
||||||
self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0,
|
|
||||||
dismissable = False, progress = -1,
|
|
||||||
title = i18n_catalog.i18nc("@info:title", "Sending Data"))
|
|
||||||
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), icon = "",
|
|
||||||
description = "")
|
|
||||||
self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
|
|
||||||
self._progress_message.show()
|
|
||||||
parts = []
|
|
||||||
|
|
||||||
target_printer, preferred_format, stream = self._dummy_lambdas
|
|
||||||
|
|
||||||
# If a specific printer was selected, it should be printed with that machine.
|
|
||||||
if target_printer:
|
|
||||||
target_printer = self._printer_uuid_to_unique_name_mapping[target_printer]
|
|
||||||
parts.append(self._createFormPart("name=require_printer_name", bytes(target_printer, "utf-8"), "text/plain"))
|
|
||||||
|
|
||||||
# Add user name to the print_job
|
|
||||||
parts.append(self._createFormPart("name=owner", bytes(self._getUserName(), "utf-8"), "text/plain"))
|
|
||||||
|
|
||||||
file_name = self._application.getPrintInformation().jobName + "." + preferred_format["extension"]
|
|
||||||
|
|
||||||
output = stream.getvalue() # Either str or bytes depending on the output mode.
|
|
||||||
if isinstance(stream, io.StringIO):
|
|
||||||
output = cast(str, output).encode("utf-8")
|
|
||||||
output = cast(bytes, output)
|
|
||||||
|
|
||||||
parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output))
|
|
||||||
|
|
||||||
self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts,
|
|
||||||
on_finished = self._onPostPrintJobFinished,
|
|
||||||
on_progress = self._onUploadPrintJobProgress)
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify = activePrinterChanged)
|
|
||||||
def activePrinter(self) -> Optional[PrinterOutputModel]:
|
|
||||||
return self._active_printer
|
|
||||||
|
|
||||||
@pyqtSlot(QObject)
|
|
||||||
def setActivePrinter(self, printer: Optional[PrinterOutputModel]) -> None:
|
|
||||||
if self._active_printer != printer:
|
|
||||||
self._active_printer = printer
|
|
||||||
self.activePrinterChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(QUrl, notify = activeCameraUrlChanged)
|
|
||||||
def activeCameraUrl(self) -> "QUrl":
|
|
||||||
return self._active_camera_url
|
|
||||||
|
|
||||||
@pyqtSlot(QUrl)
|
|
||||||
def setActiveCameraUrl(self, camera_url: "QUrl") -> None:
|
|
||||||
if self._active_camera_url != camera_url:
|
|
||||||
self._active_camera_url = camera_url
|
|
||||||
self.activeCameraUrlChanged.emit()
|
|
||||||
|
|
||||||
def _onPostPrintJobFinished(self, reply: QNetworkReply) -> None:
|
|
||||||
if self._progress_message:
|
|
||||||
self._progress_message.hide()
|
|
||||||
self._compressing_gcode = False
|
|
||||||
self._sending_gcode = False
|
|
||||||
|
|
||||||
## The IP address of the printer.
|
|
||||||
@pyqtProperty(str, constant = True)
|
|
||||||
def address(self) -> str:
|
|
||||||
return self._address
|
|
||||||
|
|
||||||
def _onUploadPrintJobProgress(self, bytes_sent: int, bytes_total: int) -> None:
|
|
||||||
if bytes_total > 0:
|
|
||||||
new_progress = bytes_sent / bytes_total * 100
|
|
||||||
# Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
|
|
||||||
# timeout responses if this happens.
|
|
||||||
self._last_response_time = time()
|
|
||||||
if self._progress_message is not None and new_progress != self._progress_message.getProgress():
|
|
||||||
self._progress_message.show() # Ensure that the message is visible.
|
|
||||||
self._progress_message.setProgress(bytes_sent / bytes_total * 100)
|
|
||||||
|
|
||||||
# If successfully sent:
|
|
||||||
if bytes_sent == bytes_total:
|
|
||||||
# Show a confirmation to the user so they know the job was sucessful and provide the option to switch to
|
|
||||||
# the monitor tab.
|
|
||||||
self._success_message = Message(
|
|
||||||
i18n_catalog.i18nc("@info:status", "Print job was successfully sent to the printer."),
|
|
||||||
lifetime=5, dismissable=True,
|
|
||||||
title=i18n_catalog.i18nc("@info:title", "Data Sent"))
|
|
||||||
self._success_message.addAction("View", i18n_catalog.i18nc("@action:button", "View in Monitor"), icon = "",
|
|
||||||
description="")
|
|
||||||
self._success_message.actionTriggered.connect(self._successMessageActionTriggered)
|
|
||||||
self._success_message.show()
|
|
||||||
else:
|
|
||||||
if self._progress_message is not None:
|
|
||||||
self._progress_message.setProgress(0)
|
|
||||||
self._progress_message.hide()
|
|
||||||
|
|
||||||
def _progressMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None:
|
|
||||||
if action_id == "Abort":
|
|
||||||
Logger.log("d", "User aborted sending print to remote.")
|
|
||||||
if self._progress_message is not None:
|
|
||||||
self._progress_message.hide()
|
|
||||||
self._compressing_gcode = False
|
|
||||||
self._sending_gcode = False
|
|
||||||
self._application.getController().setActiveStage("PrepareStage")
|
|
||||||
|
|
||||||
# After compressing the sliced model Cura sends data to printer, to stop receiving updates from the request
|
|
||||||
# the "reply" should be disconnected
|
|
||||||
if self._latest_reply_handler:
|
|
||||||
self._latest_reply_handler.disconnect()
|
|
||||||
self._latest_reply_handler = None
|
|
||||||
|
|
||||||
def _successMessageActionTriggered(self, message_id: Optional[str] = None, action_id: Optional[str] = None) -> None:
|
|
||||||
if action_id == "View":
|
|
||||||
self._application.getController().setActiveStage("MonitorStage")
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def openPrintJobControlPanel(self) -> None:
|
|
||||||
Logger.log("d", "Opening print job control panel...")
|
|
||||||
QDesktopServices.openUrl(QUrl("http://" + self._address + "/print_jobs"))
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def openPrinterControlPanel(self) -> None:
|
|
||||||
Logger.log("d", "Opening printer control panel...")
|
|
||||||
QDesktopServices.openUrl(QUrl("http://" + self._address + "/printers"))
|
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify = printJobsChanged)
|
|
||||||
def printJobs(self)-> List[UM3PrintJobOutputModel]:
|
|
||||||
return self._print_jobs
|
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = receivedPrintJobsChanged)
|
|
||||||
def receivedPrintJobs(self) -> bool:
|
|
||||||
return self._received_print_jobs
|
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify = printJobsChanged)
|
|
||||||
def queuedPrintJobs(self) -> List[UM3PrintJobOutputModel]:
|
|
||||||
return [print_job for print_job in self._print_jobs if print_job.state == "queued" or print_job.state == "error"]
|
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify = printJobsChanged)
|
|
||||||
def activePrintJobs(self) -> List[UM3PrintJobOutputModel]:
|
|
||||||
return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued"]
|
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify = _clusterPrintersChanged)
|
|
||||||
def connectedPrintersTypeCount(self) -> List[Dict[str, str]]:
|
|
||||||
printer_count = {} # type: Dict[str, int]
|
|
||||||
for printer in self._printers:
|
|
||||||
if printer.type in printer_count:
|
|
||||||
printer_count[printer.type] += 1
|
|
||||||
else:
|
|
||||||
printer_count[printer.type] = 1
|
|
||||||
result = []
|
|
||||||
for machine_type in printer_count:
|
|
||||||
result.append({"machine_type": machine_type, "count": str(printer_count[machine_type])})
|
|
||||||
return result
|
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify=_clusterPrintersChanged)
|
|
||||||
def printers(self):
|
|
||||||
return self._printers
|
|
||||||
|
|
||||||
@pyqtSlot(int, result = str)
|
|
||||||
def getTimeCompleted(self, time_remaining: int) -> str:
|
|
||||||
return formatTimeCompleted(time_remaining)
|
|
||||||
|
|
||||||
@pyqtSlot(int, result = str)
|
|
||||||
def getDateCompleted(self, time_remaining: int) -> str:
|
|
||||||
return formatDateCompleted(time_remaining)
|
|
||||||
|
|
||||||
@pyqtSlot(int, result = str)
|
|
||||||
def formatDuration(self, seconds: int) -> str:
|
|
||||||
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def sendJobToTop(self, print_job_uuid: str) -> None:
|
|
||||||
# This function is part of the output device (and not of the printjob output model) as this type of operation
|
|
||||||
# is a modification of the cluster queue and not of the actual job.
|
|
||||||
data = "{\"to_position\": 0}"
|
|
||||||
self.put("print_jobs/{uuid}/move_to_position".format(uuid = print_job_uuid), data, on_finished=None)
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def deleteJobFromQueue(self, print_job_uuid: str) -> None:
|
|
||||||
# This function is part of the output device (and not of the printjob output model) as this type of operation
|
|
||||||
# is a modification of the cluster queue and not of the actual job.
|
|
||||||
self.delete("print_jobs/{uuid}".format(uuid = print_job_uuid), on_finished=None)
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def forceSendJob(self, print_job_uuid: str) -> None:
|
|
||||||
data = "{\"force\": true}"
|
|
||||||
self.put("print_jobs/{uuid}".format(uuid=print_job_uuid), data, on_finished=None)
|
|
||||||
|
|
||||||
def _printJobStateChanged(self) -> None:
|
|
||||||
username = self._getUserName()
|
|
||||||
|
|
||||||
if username is None:
|
|
||||||
return # We only want to show notifications if username is set.
|
|
||||||
|
|
||||||
finished_jobs = [job for job in self._print_jobs if job.state == "wait_cleanup"]
|
|
||||||
|
|
||||||
newly_finished_jobs = [job for job in finished_jobs if job not in self._finished_jobs and job.owner == username]
|
|
||||||
for job in newly_finished_jobs:
|
|
||||||
if job.assignedPrinter:
|
|
||||||
job_completed_text = i18n_catalog.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.").format(printer_name=job.assignedPrinter.name, job_name = job.name)
|
|
||||||
else:
|
|
||||||
job_completed_text = i18n_catalog.i18nc("@info:status", "The print job '{job_name}' was finished.").format(job_name = job.name)
|
|
||||||
job_completed_message = Message(text=job_completed_text, title = i18n_catalog.i18nc("@info:status", "Print finished"))
|
|
||||||
job_completed_message.show()
|
|
||||||
|
|
||||||
# Ensure UI gets updated
|
|
||||||
self.printJobsChanged.emit()
|
|
||||||
|
|
||||||
# Keep a list of all completed jobs so we know if something changed next time.
|
|
||||||
self._finished_jobs = finished_jobs
|
|
||||||
|
|
||||||
## Called when the connection to the cluster changes.
|
|
||||||
def connect(self) -> None:
|
|
||||||
super().connect()
|
|
||||||
self.sendMaterialProfiles()
|
|
||||||
|
|
||||||
def _onGetPreviewImageFinished(self, reply: QNetworkReply) -> None:
|
|
||||||
reply_url = reply.url().toString()
|
|
||||||
|
|
||||||
uuid = reply_url[reply_url.find("print_jobs/")+len("print_jobs/"):reply_url.rfind("/preview_image")]
|
|
||||||
|
|
||||||
print_job = findByKey(self._print_jobs, uuid)
|
|
||||||
if print_job:
|
|
||||||
image = QImage()
|
|
||||||
image.loadFromData(reply.readAll())
|
|
||||||
print_job.updatePreviewImage(image)
|
|
||||||
|
|
||||||
def _update(self) -> None:
|
|
||||||
super()._update()
|
|
||||||
self.get("printers/", on_finished = self._onGetPrintersDataFinished)
|
|
||||||
self.get("print_jobs/", on_finished = self._onGetPrintJobsFinished)
|
|
||||||
|
|
||||||
for print_job in self._print_jobs:
|
|
||||||
if print_job.getPreviewImage() is None:
|
|
||||||
self.get("print_jobs/{uuid}/preview_image".format(uuid=print_job.key), on_finished=self._onGetPreviewImageFinished)
|
|
||||||
|
|
||||||
def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None:
|
|
||||||
self._received_print_jobs = True
|
|
||||||
self.receivedPrintJobsChanged.emit()
|
|
||||||
|
|
||||||
if not checkValidGetReply(reply):
|
|
||||||
return
|
|
||||||
|
|
||||||
result = loadJsonFromReply(reply)
|
|
||||||
if result is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
print_jobs_seen = []
|
|
||||||
job_list_changed = False
|
|
||||||
for idx, print_job_data in enumerate(result):
|
|
||||||
print_job = findByKey(self._print_jobs, print_job_data["uuid"])
|
|
||||||
if print_job is None:
|
|
||||||
print_job = self._createPrintJobModel(print_job_data)
|
|
||||||
job_list_changed = True
|
|
||||||
elif not job_list_changed:
|
|
||||||
# Check if the order of the jobs has changed since the last check
|
|
||||||
if self._print_jobs.index(print_job) != idx:
|
|
||||||
job_list_changed = True
|
|
||||||
|
|
||||||
self._updatePrintJob(print_job, print_job_data)
|
|
||||||
|
|
||||||
if print_job.state != "queued" and print_job.state != "error": # Print job should be assigned to a printer.
|
|
||||||
if print_job.state in ["failed", "finished", "aborted", "none"]:
|
|
||||||
# Print job was already completed, so don't attach it to a printer.
|
|
||||||
printer = None
|
|
||||||
else:
|
|
||||||
printer = self._getPrinterByKey(print_job_data["printer_uuid"])
|
|
||||||
else: # The job can "reserve" a printer if some changes are required.
|
|
||||||
printer = self._getPrinterByKey(print_job_data["assigned_to"])
|
|
||||||
|
|
||||||
if printer:
|
|
||||||
printer.updateActivePrintJob(print_job)
|
|
||||||
|
|
||||||
print_jobs_seen.append(print_job)
|
|
||||||
|
|
||||||
# Check what jobs need to be removed.
|
|
||||||
removed_jobs = [print_job for print_job in self._print_jobs if print_job not in print_jobs_seen]
|
|
||||||
|
|
||||||
for removed_job in removed_jobs:
|
|
||||||
job_list_changed = job_list_changed or self._removeJob(removed_job)
|
|
||||||
|
|
||||||
if job_list_changed:
|
|
||||||
# Override the old list with the new list (either because jobs were removed / added or order changed)
|
|
||||||
self._print_jobs = print_jobs_seen
|
|
||||||
self.printJobsChanged.emit() # Do a single emit for all print job changes.
|
|
||||||
|
|
||||||
def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None:
|
|
||||||
if not checkValidGetReply(reply):
|
|
||||||
return
|
|
||||||
|
|
||||||
result = loadJsonFromReply(reply)
|
|
||||||
if result is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
printer_list_changed = False
|
|
||||||
printers_seen = []
|
|
||||||
|
|
||||||
for printer_data in result:
|
|
||||||
printer = findByKey(self._printers, printer_data["uuid"])
|
|
||||||
|
|
||||||
if printer is None:
|
|
||||||
printer = self._createPrinterModel(printer_data)
|
|
||||||
printer_list_changed = True
|
|
||||||
|
|
||||||
printers_seen.append(printer)
|
|
||||||
|
|
||||||
self._updatePrinter(printer, printer_data)
|
|
||||||
|
|
||||||
removed_printers = [printer for printer in self._printers if printer not in printers_seen]
|
|
||||||
for printer in removed_printers:
|
|
||||||
self._removePrinter(printer)
|
|
||||||
|
|
||||||
if removed_printers or printer_list_changed:
|
|
||||||
self.printersChanged.emit()
|
|
||||||
|
|
||||||
def _createPrinterModel(self, data: Dict[str, Any]) -> PrinterOutputModel:
|
|
||||||
printer = PrinterOutputModel(output_controller = ClusterUM3PrinterOutputController(self),
|
|
||||||
number_of_extruders = self._number_of_extruders)
|
|
||||||
printer.setCameraUrl(QUrl("http://" + data["ip_address"] + ":8080/?action=stream"))
|
|
||||||
self._printers.append(printer)
|
|
||||||
return printer
|
|
||||||
|
|
||||||
def _createPrintJobModel(self, data: Dict[str, Any]) -> UM3PrintJobOutputModel:
|
|
||||||
print_job = UM3PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
|
|
||||||
key=data["uuid"], name= data["name"])
|
|
||||||
|
|
||||||
configuration = PrinterConfigurationModel()
|
|
||||||
extruders = [ExtruderConfigurationModel(position = idx) for idx in range(0, self._number_of_extruders)]
|
|
||||||
for index in range(0, self._number_of_extruders):
|
|
||||||
try:
|
|
||||||
extruder_data = data["configuration"][index]
|
|
||||||
except IndexError:
|
|
||||||
continue
|
|
||||||
extruder = extruders[int(data["configuration"][index]["extruder_index"])]
|
|
||||||
extruder.setHotendID(extruder_data.get("print_core_id", ""))
|
|
||||||
extruder.setMaterial(self._createMaterialOutputModel(extruder_data.get("material", {})))
|
|
||||||
|
|
||||||
configuration.setExtruderConfigurations(extruders)
|
|
||||||
configuration.setPrinterType(data.get("machine_variant", ""))
|
|
||||||
print_job.updateConfiguration(configuration)
|
|
||||||
print_job.setCompatibleMachineFamilies(data.get("compatible_machine_families", []))
|
|
||||||
print_job.stateChanged.connect(self._printJobStateChanged)
|
|
||||||
return print_job
|
|
||||||
|
|
||||||
def _updatePrintJob(self, print_job: UM3PrintJobOutputModel, data: Dict[str, Any]) -> None:
|
|
||||||
print_job.updateTimeTotal(data["time_total"])
|
|
||||||
print_job.updateTimeElapsed(data["time_elapsed"])
|
|
||||||
impediments_to_printing = data.get("impediments_to_printing", [])
|
|
||||||
print_job.updateOwner(data["owner"])
|
|
||||||
|
|
||||||
status_set_by_impediment = False
|
|
||||||
for impediment in impediments_to_printing:
|
|
||||||
if impediment["severity"] == "UNFIXABLE":
|
|
||||||
status_set_by_impediment = True
|
|
||||||
print_job.updateState("error")
|
|
||||||
break
|
|
||||||
|
|
||||||
if not status_set_by_impediment:
|
|
||||||
print_job.updateState(data["status"])
|
|
||||||
|
|
||||||
print_job.updateConfigurationChanges(self._createConfigurationChanges(data["configuration_changes_required"]))
|
|
||||||
|
|
||||||
def _createConfigurationChanges(self, data: List[Dict[str, Any]]) -> List[ConfigurationChangeModel]:
|
|
||||||
result = []
|
|
||||||
for change in data:
|
|
||||||
result.append(ConfigurationChangeModel(type_of_change=change["type_of_change"],
|
|
||||||
index=change["index"],
|
|
||||||
target_name=change["target_name"],
|
|
||||||
origin_name=change["origin_name"]))
|
|
||||||
return result
|
|
||||||
|
|
||||||
def _createMaterialOutputModel(self, material_data: Dict[str, Any]) -> "MaterialOutputModel":
|
|
||||||
material_manager = self._application.getMaterialManager()
|
|
||||||
material_group_list = None
|
|
||||||
|
|
||||||
# Avoid crashing if there is no "guid" field in the metadata
|
|
||||||
material_guid = material_data.get("guid")
|
|
||||||
if material_guid:
|
|
||||||
material_group_list = material_manager.getMaterialGroupListByGUID(material_guid)
|
|
||||||
|
|
||||||
# This can happen if the connected machine has no material in one or more extruders (if GUID is empty), or the
|
|
||||||
# material is unknown to Cura, so we should return an "empty" or "unknown" material model.
|
|
||||||
if material_group_list is None:
|
|
||||||
material_name = i18n_catalog.i18nc("@label:material", "Empty") if len(material_data.get("guid", "")) == 0 \
|
|
||||||
else i18n_catalog.i18nc("@label:material", "Unknown")
|
|
||||||
|
|
||||||
return MaterialOutputModel(guid = material_data.get("guid", ""),
|
|
||||||
type = material_data.get("material", ""),
|
|
||||||
color = material_data.get("color", ""),
|
|
||||||
brand = material_data.get("brand", ""),
|
|
||||||
name = material_data.get("name", material_name)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Sort the material groups by "is_read_only = True" first, and then the name alphabetically.
|
|
||||||
read_only_material_group_list = list(filter(lambda x: x.is_read_only, material_group_list))
|
|
||||||
non_read_only_material_group_list = list(filter(lambda x: not x.is_read_only, material_group_list))
|
|
||||||
material_group = None
|
|
||||||
if read_only_material_group_list:
|
|
||||||
read_only_material_group_list = sorted(read_only_material_group_list, key = lambda x: x.name)
|
|
||||||
material_group = read_only_material_group_list[0]
|
|
||||||
elif non_read_only_material_group_list:
|
|
||||||
non_read_only_material_group_list = sorted(non_read_only_material_group_list, key = lambda x: x.name)
|
|
||||||
material_group = non_read_only_material_group_list[0]
|
|
||||||
|
|
||||||
if material_group:
|
|
||||||
container = material_group.root_material_node.getContainer()
|
|
||||||
color = container.getMetaDataEntry("color_code")
|
|
||||||
brand = container.getMetaDataEntry("brand")
|
|
||||||
material_type = container.getMetaDataEntry("material")
|
|
||||||
name = container.getName()
|
|
||||||
else:
|
|
||||||
Logger.log("w",
|
|
||||||
"Unable to find material with guid {guid}. Using data as provided by cluster".format(
|
|
||||||
guid=material_data["guid"]))
|
|
||||||
color = material_data["color"]
|
|
||||||
brand = material_data["brand"]
|
|
||||||
material_type = material_data["material"]
|
|
||||||
name = i18n_catalog.i18nc("@label:material", "Empty") if material_data["material"] == "empty" \
|
|
||||||
else i18n_catalog.i18nc("@label:material", "Unknown")
|
|
||||||
return MaterialOutputModel(guid = material_data["guid"], type = material_type,
|
|
||||||
brand = brand, color = color, name = name)
|
|
||||||
|
|
||||||
def _updatePrinter(self, printer: PrinterOutputModel, data: Dict[str, Any]) -> None:
|
|
||||||
# For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer.
|
|
||||||
# Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping.
|
|
||||||
self._printer_uuid_to_unique_name_mapping[data["uuid"]] = data["unique_name"]
|
|
||||||
|
|
||||||
definitions = ContainerRegistry.getInstance().findDefinitionContainers(name = data["machine_variant"])
|
|
||||||
if not definitions:
|
|
||||||
Logger.log("w", "Unable to find definition for machine variant %s", data["machine_variant"])
|
|
||||||
return
|
|
||||||
|
|
||||||
machine_definition = definitions[0]
|
|
||||||
|
|
||||||
printer.updateName(data["friendly_name"])
|
|
||||||
printer.updateKey(data["uuid"])
|
|
||||||
printer.updateType(data["machine_variant"])
|
|
||||||
|
|
||||||
if data["status"] != "unreachable":
|
|
||||||
self._application.getDiscoveredPrintersModel().updateDiscoveredPrinter(data["ip_address"],
|
|
||||||
name = data["friendly_name"],
|
|
||||||
machine_type = data["machine_variant"])
|
|
||||||
|
|
||||||
# Do not store the build plate information that comes from connect if the current printer has not build plate information
|
|
||||||
if "build_plate" in data and machine_definition.getMetaDataEntry("has_variant_buildplates", False):
|
|
||||||
printer.updateBuildplate(data["build_plate"]["type"])
|
|
||||||
if not data["enabled"]:
|
|
||||||
printer.updateState("disabled")
|
|
||||||
else:
|
|
||||||
printer.updateState(data["status"])
|
|
||||||
|
|
||||||
for index in range(0, self._number_of_extruders):
|
|
||||||
extruder = printer.extruders[index]
|
|
||||||
try:
|
|
||||||
extruder_data = data["configuration"][index]
|
|
||||||
except IndexError:
|
|
||||||
break
|
|
||||||
|
|
||||||
extruder.updateHotendID(extruder_data.get("print_core_id", ""))
|
|
||||||
|
|
||||||
material_data = extruder_data["material"]
|
|
||||||
if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]:
|
|
||||||
material = self._createMaterialOutputModel(material_data)
|
|
||||||
extruder.updateActiveMaterial(material)
|
|
||||||
|
|
||||||
def _removeJob(self, job: UM3PrintJobOutputModel) -> bool:
|
|
||||||
if job not in self._print_jobs:
|
|
||||||
return False
|
|
||||||
|
|
||||||
if job.assignedPrinter:
|
|
||||||
job.assignedPrinter.updateActivePrintJob(None)
|
|
||||||
job.stateChanged.disconnect(self._printJobStateChanged)
|
|
||||||
self._print_jobs.remove(job)
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _removePrinter(self, printer: PrinterOutputModel) -> None:
|
|
||||||
self._printers.remove(printer)
|
|
||||||
if self._active_printer == printer:
|
|
||||||
self._active_printer = None
|
|
||||||
self.activePrinterChanged.emit()
|
|
||||||
|
|
||||||
## Sync the material profiles in Cura with the printer.
|
|
||||||
#
|
|
||||||
# This gets called when connecting to a printer as well as when sending a
|
|
||||||
# print.
|
|
||||||
def sendMaterialProfiles(self) -> None:
|
|
||||||
job = SendMaterialJob(device = self)
|
|
||||||
job.run()
|
|
||||||
|
|
||||||
def loadJsonFromReply(reply: QNetworkReply) -> Optional[List[Dict[str, Any]]]:
|
|
||||||
try:
|
|
||||||
result = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
|
||||||
except json.decoder.JSONDecodeError:
|
|
||||||
Logger.logException("w", "Unable to decode JSON from reply.")
|
|
||||||
return None
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def checkValidGetReply(reply: QNetworkReply) -> bool:
|
|
||||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
|
||||||
|
|
||||||
if status_code != 200:
|
|
||||||
Logger.log("w", "Got status code {status_code} while trying to get data".format(status_code=status_code))
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def findByKey(lst: List[Union[UM3PrintJobOutputModel, PrinterOutputModel]], key: str) -> Optional[UM3PrintJobOutputModel]:
|
|
||||||
for item in lst:
|
|
||||||
if item.key == key:
|
|
||||||
return item
|
|
||||||
return None
|
|
|
@ -1,20 +0,0 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
|
||||||
|
|
||||||
MYPY = False
|
|
||||||
if MYPY:
|
|
||||||
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
|
|
||||||
|
|
||||||
class ClusterUM3PrinterOutputController(PrinterOutputController):
|
|
||||||
def __init__(self, output_device):
|
|
||||||
super().__init__(output_device)
|
|
||||||
self.can_pre_heat_bed = False
|
|
||||||
self.can_pre_heat_hotends = False
|
|
||||||
self.can_control_manually = False
|
|
||||||
self.can_send_raw_gcode = False
|
|
||||||
|
|
||||||
def setJobState(self, job: "PrintJobOutputModel", state: str):
|
|
||||||
data = "{\"action\": \"%s\"}" % state
|
|
||||||
self._output_device.put("print_jobs/%s/action" % job.key, data, on_finished=None)
|
|
|
@ -1,179 +0,0 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
import os.path
|
|
||||||
import time
|
|
||||||
from typing import Optional, TYPE_CHECKING
|
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QObject
|
|
||||||
|
|
||||||
from UM.PluginRegistry import PluginRegistry
|
|
||||||
from UM.Logger import Logger
|
|
||||||
from UM.i18n import i18nCatalog
|
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
|
||||||
from cura.MachineAction import MachineAction
|
|
||||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
|
||||||
|
|
||||||
from .UM3OutputDevicePlugin import UM3OutputDevicePlugin
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
|
||||||
|
|
||||||
|
|
||||||
class DiscoverUM3Action(MachineAction):
|
|
||||||
discoveredDevicesChanged = pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__("DiscoverUM3Action", catalog.i18nc("@action","Connect via Network"))
|
|
||||||
self._qml_url = "resources/qml/DiscoverUM3Action.qml"
|
|
||||||
|
|
||||||
self._network_plugin = None #type: Optional[UM3OutputDevicePlugin]
|
|
||||||
|
|
||||||
self.__additional_components_view = None #type: Optional[QObject]
|
|
||||||
|
|
||||||
CuraApplication.getInstance().engineCreatedSignal.connect(self._createAdditionalComponentsView)
|
|
||||||
|
|
||||||
self._last_zero_conf_event_time = time.time() #type: float
|
|
||||||
|
|
||||||
# Time to wait after a zero-conf service change before allowing a zeroconf reset
|
|
||||||
self._zero_conf_change_grace_period = 0.25 #type: float
|
|
||||||
|
|
||||||
# Overrides the one in MachineAction.
|
|
||||||
# This requires not attention from the user (any more), so we don't need to show any 'upgrade screens'.
|
|
||||||
def needsUserInteraction(self) -> bool:
|
|
||||||
return False
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def startDiscovery(self):
|
|
||||||
if not self._network_plugin:
|
|
||||||
Logger.log("d", "Starting device discovery.")
|
|
||||||
self._network_plugin = CuraApplication.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting")
|
|
||||||
self._network_plugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged)
|
|
||||||
self.discoveredDevicesChanged.emit()
|
|
||||||
|
|
||||||
## Re-filters the list of devices.
|
|
||||||
@pyqtSlot()
|
|
||||||
def reset(self):
|
|
||||||
Logger.log("d", "Reset the list of found devices.")
|
|
||||||
if self._network_plugin:
|
|
||||||
self._network_plugin.resetLastManualDevice()
|
|
||||||
self.discoveredDevicesChanged.emit()
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def restartDiscovery(self):
|
|
||||||
# Ensure that there is a bit of time after a printer has been discovered.
|
|
||||||
# This is a work around for an issue with Qt 5.5.1 up to Qt 5.7 which can segfault if we do this too often.
|
|
||||||
# It's most likely that the QML engine is still creating delegates, where the python side already deleted or
|
|
||||||
# garbage collected the data.
|
|
||||||
# Whatever the case, waiting a bit ensures that it doesn't crash.
|
|
||||||
if time.time() - self._last_zero_conf_event_time > self._zero_conf_change_grace_period:
|
|
||||||
if not self._network_plugin:
|
|
||||||
self.startDiscovery()
|
|
||||||
else:
|
|
||||||
self._network_plugin.startDiscovery()
|
|
||||||
|
|
||||||
@pyqtSlot(str, str)
|
|
||||||
def removeManualDevice(self, key, address):
|
|
||||||
if not self._network_plugin:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._network_plugin.removeManualDevice(key, address)
|
|
||||||
|
|
||||||
@pyqtSlot(str, str)
|
|
||||||
def setManualDevice(self, key, address):
|
|
||||||
if key != "":
|
|
||||||
# This manual printer replaces a current manual printer
|
|
||||||
self._network_plugin.removeManualDevice(key)
|
|
||||||
|
|
||||||
if address != "":
|
|
||||||
self._network_plugin.addManualDevice(address)
|
|
||||||
|
|
||||||
def _onDeviceDiscoveryChanged(self, *args):
|
|
||||||
self._last_zero_conf_event_time = time.time()
|
|
||||||
self.discoveredDevicesChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify = discoveredDevicesChanged)
|
|
||||||
def foundDevices(self):
|
|
||||||
if self._network_plugin:
|
|
||||||
|
|
||||||
printers = list(self._network_plugin.getDiscoveredDevices().values())
|
|
||||||
printers.sort(key = lambda k: k.name)
|
|
||||||
return printers
|
|
||||||
else:
|
|
||||||
return []
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def setGroupName(self, group_name: str) -> None:
|
|
||||||
Logger.log("d", "Attempting to set the group name of the active machine to %s", group_name)
|
|
||||||
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
|
||||||
if global_container_stack:
|
|
||||||
# Update a GlobalStacks in the same group with the new group name.
|
|
||||||
group_id = global_container_stack.getMetaDataEntry("group_id")
|
|
||||||
machine_manager = CuraApplication.getInstance().getMachineManager()
|
|
||||||
for machine in machine_manager.getMachinesInGroup(group_id):
|
|
||||||
machine.setMetaDataEntry("group_name", group_name)
|
|
||||||
|
|
||||||
# Set the default value for "hidden", which is used when you have a group with multiple types of printers
|
|
||||||
global_container_stack.setMetaDataEntry("hidden", False)
|
|
||||||
|
|
||||||
if self._network_plugin:
|
|
||||||
# Ensure that the connection states are refreshed.
|
|
||||||
self._network_plugin.refreshConnections()
|
|
||||||
|
|
||||||
# Associates the currently active machine with the given printer device. The network connection information will be
|
|
||||||
# stored into the metadata of the currently active machine.
|
|
||||||
@pyqtSlot(QObject)
|
|
||||||
def associateActiveMachineWithPrinterDevice(self, printer_device: Optional["PrinterOutputDevice"]) -> None:
|
|
||||||
if self._network_plugin:
|
|
||||||
self._network_plugin.associateActiveMachineWithPrinterDevice(printer_device)
|
|
||||||
|
|
||||||
@pyqtSlot(result = str)
|
|
||||||
def getStoredKey(self) -> str:
|
|
||||||
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
|
||||||
if global_container_stack:
|
|
||||||
meta_data = global_container_stack.getMetaData()
|
|
||||||
if "um_network_key" in meta_data:
|
|
||||||
return global_container_stack.getMetaDataEntry("um_network_key")
|
|
||||||
|
|
||||||
return ""
|
|
||||||
|
|
||||||
@pyqtSlot(result = str)
|
|
||||||
def getLastManualEntryKey(self) -> str:
|
|
||||||
if self._network_plugin:
|
|
||||||
return self._network_plugin.getLastManualDevice()
|
|
||||||
return ""
|
|
||||||
|
|
||||||
@pyqtSlot(str, result = bool)
|
|
||||||
def existsKey(self, key: str) -> bool:
|
|
||||||
metadata_filter = {"um_network_key": key}
|
|
||||||
containers = CuraContainerRegistry.getInstance().findContainerStacks(type="machine", **metadata_filter)
|
|
||||||
return bool(containers)
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def loadConfigurationFromPrinter(self) -> None:
|
|
||||||
machine_manager = CuraApplication.getInstance().getMachineManager()
|
|
||||||
hotend_ids = machine_manager.printerOutputDevices[0].hotendIds
|
|
||||||
for index in range(len(hotend_ids)):
|
|
||||||
machine_manager.printerOutputDevices[0].hotendIdChanged.emit(index, hotend_ids[index])
|
|
||||||
material_ids = machine_manager.printerOutputDevices[0].materialIds
|
|
||||||
for index in range(len(material_ids)):
|
|
||||||
machine_manager.printerOutputDevices[0].materialIdChanged.emit(index, material_ids[index])
|
|
||||||
|
|
||||||
def _createAdditionalComponentsView(self) -> None:
|
|
||||||
Logger.log("d", "Creating additional ui components for UM3.")
|
|
||||||
|
|
||||||
# Create networking dialog
|
|
||||||
plugin_path = PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting")
|
|
||||||
if not plugin_path:
|
|
||||||
return
|
|
||||||
path = os.path.join(plugin_path, "resources/qml/UM3InfoComponents.qml")
|
|
||||||
self.__additional_components_view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
|
|
||||||
if not self.__additional_components_view:
|
|
||||||
Logger.log("w", "Could not create ui components for UM3.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Create extra components
|
|
||||||
CuraApplication.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton"))
|
|
39
plugins/UM3NetworkPrinting/src/ExportFileJob.py
Normal file
39
plugins/UM3NetworkPrinting/src/ExportFileJob.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from UM.FileHandler.FileHandler import FileHandler
|
||||||
|
from UM.FileHandler.WriteFileJob import WriteFileJob
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
from .MeshFormatHandler import MeshFormatHandler
|
||||||
|
|
||||||
|
|
||||||
|
## Job that exports the build plate to the correct file format for the target cluster.
|
||||||
|
class ExportFileJob(WriteFileJob):
|
||||||
|
|
||||||
|
def __init__(self, file_handler: Optional[FileHandler], nodes: List[SceneNode], firmware_version: str) -> None:
|
||||||
|
|
||||||
|
self._mesh_format_handler = MeshFormatHandler(file_handler, firmware_version)
|
||||||
|
if not self._mesh_format_handler.is_valid:
|
||||||
|
Logger.log("e", "Missing file or mesh writer!")
|
||||||
|
return
|
||||||
|
|
||||||
|
super().__init__(self._mesh_format_handler.writer, self._mesh_format_handler.createStream(), nodes,
|
||||||
|
self._mesh_format_handler.file_mode)
|
||||||
|
|
||||||
|
# Determine the filename.
|
||||||
|
job_name = CuraApplication.getInstance().getPrintInformation().jobName
|
||||||
|
extension = self._mesh_format_handler.preferred_format.get("extension", "")
|
||||||
|
self.setFileName("{}.{}".format(job_name, extension))
|
||||||
|
|
||||||
|
## Get the mime type of the selected export file type.
|
||||||
|
def getMimeType(self) -> str:
|
||||||
|
return self._mesh_format_handler.mime_type
|
||||||
|
|
||||||
|
## Get the job result as bytes as that is what we need to upload to the cluster.
|
||||||
|
def getOutput(self) -> bytes:
|
||||||
|
output = self.getStream().getvalue()
|
||||||
|
if isinstance(output, str):
|
||||||
|
output = output.encode("utf-8")
|
||||||
|
return output
|
|
@ -1,646 +0,0 @@
|
||||||
from typing import List, Optional
|
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
|
||||||
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
|
|
||||||
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
|
||||||
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
|
|
||||||
from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel
|
|
||||||
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
|
|
||||||
|
|
||||||
from cura.Settings.ContainerManager import ContainerManager
|
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
|
||||||
|
|
||||||
from UM.FileHandler.FileHandler import FileHandler
|
|
||||||
from UM.i18n import i18nCatalog
|
|
||||||
from UM.Logger import Logger
|
|
||||||
from UM.Message import Message
|
|
||||||
from UM.PluginRegistry import PluginRegistry
|
|
||||||
from UM.Scene.SceneNode import SceneNode
|
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
|
||||||
|
|
||||||
from PyQt5.QtNetwork import QNetworkRequest
|
|
||||||
from PyQt5.QtCore import QTimer, QUrl
|
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
|
||||||
|
|
||||||
from .LegacyUM3PrinterOutputController import LegacyUM3PrinterOutputController
|
|
||||||
|
|
||||||
from time import time
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
|
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
|
||||||
|
|
||||||
|
|
||||||
## This is the output device for the "Legacy" API of the UM3. All firmware before 4.0.1 uses this API.
|
|
||||||
# Everything after that firmware uses the ClusterUM3Output.
|
|
||||||
# The Legacy output device can only have one printer (whereas the cluster can have 0 to n).
|
|
||||||
#
|
|
||||||
# Authentication is done in a number of steps;
|
|
||||||
# 1. Request an id / key pair by sending the application & user name. (state = authRequested)
|
|
||||||
# 2. Machine sends this back and will display an approve / deny message on screen. (state = AuthReceived)
|
|
||||||
# 3. OutputDevice will poll if the button was pressed.
|
|
||||||
# 4. At this point the machine either has the state Authenticated or AuthenticationDenied.
|
|
||||||
# 5. As a final step, we verify the authentication, as this forces the QT manager to setup the authenticator.
|
|
||||||
class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
|
|
||||||
def __init__(self, device_id, address: str, properties, parent = None) -> None:
|
|
||||||
super().__init__(device_id = device_id, address = address, properties = properties, connection_type = ConnectionType.NetworkConnection, parent = parent)
|
|
||||||
self._api_prefix = "/api/v1/"
|
|
||||||
self._number_of_extruders = 2
|
|
||||||
|
|
||||||
self._authentication_id = None
|
|
||||||
self._authentication_key = None
|
|
||||||
|
|
||||||
self._authentication_counter = 0
|
|
||||||
self._max_authentication_counter = 5 * 60 # Number of attempts before authentication timed out (5 min)
|
|
||||||
|
|
||||||
self._authentication_timer = QTimer()
|
|
||||||
self._authentication_timer.setInterval(1000) # TODO; Add preference for update interval
|
|
||||||
self._authentication_timer.setSingleShot(False)
|
|
||||||
|
|
||||||
self._authentication_timer.timeout.connect(self._onAuthenticationTimer)
|
|
||||||
|
|
||||||
# The messages are created when connect is called the first time.
|
|
||||||
# This ensures that the messages are only created for devices that actually want to connect.
|
|
||||||
self._authentication_requested_message = None
|
|
||||||
self._authentication_failed_message = None
|
|
||||||
self._authentication_succeeded_message = None
|
|
||||||
self._not_authenticated_message = None
|
|
||||||
|
|
||||||
self.authenticationStateChanged.connect(self._onAuthenticationStateChanged)
|
|
||||||
|
|
||||||
self.setPriority(3) # Make sure the output device gets selected above local file output
|
|
||||||
self.setName(self._id)
|
|
||||||
self.setShortDescription(i18n_catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print over network"))
|
|
||||||
self.setDescription(i18n_catalog.i18nc("@properties:tooltip", "Print over network"))
|
|
||||||
|
|
||||||
self.setIconName("print")
|
|
||||||
|
|
||||||
self._output_controller = LegacyUM3PrinterOutputController(self)
|
|
||||||
|
|
||||||
def _createMonitorViewFromQML(self) -> None:
|
|
||||||
if self._monitor_view_qml_path is None and PluginRegistry.getInstance() is not None:
|
|
||||||
self._monitor_view_qml_path = os.path.join(
|
|
||||||
PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting"),
|
|
||||||
"resources", "qml", "MonitorStage.qml"
|
|
||||||
)
|
|
||||||
super()._createMonitorViewFromQML()
|
|
||||||
|
|
||||||
def _onAuthenticationStateChanged(self):
|
|
||||||
# We only accept commands if we are authenticated.
|
|
||||||
self._setAcceptsCommands(self._authentication_state == AuthState.Authenticated)
|
|
||||||
|
|
||||||
if self._authentication_state == AuthState.Authenticated:
|
|
||||||
self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network."))
|
|
||||||
elif self._authentication_state == AuthState.AuthenticationRequested:
|
|
||||||
self.setConnectionText(i18n_catalog.i18nc("@info:status",
|
|
||||||
"Connected over the network. Please approve the access request on the printer."))
|
|
||||||
elif self._authentication_state == AuthState.AuthenticationDenied:
|
|
||||||
self.setConnectionText(i18n_catalog.i18nc("@info:status", "Connected over the network. No access to control the printer."))
|
|
||||||
|
|
||||||
|
|
||||||
def _setupMessages(self):
|
|
||||||
self._authentication_requested_message = Message(i18n_catalog.i18nc("@info:status",
|
|
||||||
"Access to the printer requested. Please approve the request on the printer"),
|
|
||||||
lifetime=0, dismissable=False, progress=0,
|
|
||||||
title=i18n_catalog.i18nc("@info:title",
|
|
||||||
"Authentication status"))
|
|
||||||
|
|
||||||
self._authentication_failed_message = Message("", title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
|
|
||||||
self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None,
|
|
||||||
i18n_catalog.i18nc("@info:tooltip", "Re-send the access request"))
|
|
||||||
self._authentication_failed_message.actionTriggered.connect(self._messageCallback)
|
|
||||||
self._authentication_succeeded_message = Message(
|
|
||||||
i18n_catalog.i18nc("@info:status", "Access to the printer accepted"),
|
|
||||||
title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
|
|
||||||
|
|
||||||
self._not_authenticated_message = Message(
|
|
||||||
i18n_catalog.i18nc("@info:status", "No access to print with this printer. Unable to send print job."),
|
|
||||||
title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
|
|
||||||
self._not_authenticated_message.addAction("Request", i18n_catalog.i18nc("@action:button", "Request Access"),
|
|
||||||
None, i18n_catalog.i18nc("@info:tooltip",
|
|
||||||
"Send access request to the printer"))
|
|
||||||
self._not_authenticated_message.actionTriggered.connect(self._messageCallback)
|
|
||||||
|
|
||||||
def _messageCallback(self, message_id=None, action_id="Retry"):
|
|
||||||
if action_id == "Request" or action_id == "Retry":
|
|
||||||
if self._authentication_failed_message:
|
|
||||||
self._authentication_failed_message.hide()
|
|
||||||
if self._not_authenticated_message:
|
|
||||||
self._not_authenticated_message.hide()
|
|
||||||
|
|
||||||
self._requestAuthentication()
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
super().connect()
|
|
||||||
self._setupMessages()
|
|
||||||
global_container = CuraApplication.getInstance().getGlobalContainerStack()
|
|
||||||
if global_container:
|
|
||||||
self._authentication_id = global_container.getMetaDataEntry("network_authentication_id", None)
|
|
||||||
self._authentication_key = global_container.getMetaDataEntry("network_authentication_key", None)
|
|
||||||
|
|
||||||
def close(self):
|
|
||||||
super().close()
|
|
||||||
if self._authentication_requested_message:
|
|
||||||
self._authentication_requested_message.hide()
|
|
||||||
if self._authentication_failed_message:
|
|
||||||
self._authentication_failed_message.hide()
|
|
||||||
if self._authentication_succeeded_message:
|
|
||||||
self._authentication_succeeded_message.hide()
|
|
||||||
self._sending_gcode = False
|
|
||||||
self._compressing_gcode = False
|
|
||||||
self._authentication_timer.stop()
|
|
||||||
|
|
||||||
## Send all material profiles to the printer.
|
|
||||||
def _sendMaterialProfiles(self):
|
|
||||||
Logger.log("i", "Sending material profiles to printer")
|
|
||||||
|
|
||||||
# TODO: Might want to move this to a job...
|
|
||||||
for container in ContainerRegistry.getInstance().findInstanceContainers(type="material"):
|
|
||||||
try:
|
|
||||||
xml_data = container.serialize()
|
|
||||||
if xml_data == "" or xml_data is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
names = ContainerManager.getInstance().getLinkedMaterials(container.getId())
|
|
||||||
if names:
|
|
||||||
# There are other materials that share this GUID.
|
|
||||||
if not container.isReadOnly():
|
|
||||||
continue # If it's not readonly, it's created by user, so skip it.
|
|
||||||
|
|
||||||
file_name = "none.xml"
|
|
||||||
|
|
||||||
self.postForm("materials", "form-data; name=\"file\";filename=\"%s\"" % file_name, xml_data.encode(), on_finished=None)
|
|
||||||
|
|
||||||
except NotImplementedError:
|
|
||||||
# If the material container is not the most "generic" one it can't be serialized an will raise a
|
|
||||||
# NotImplementedError. We can simply ignore these.
|
|
||||||
pass
|
|
||||||
|
|
||||||
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
|
|
||||||
if not self.activePrinter:
|
|
||||||
# No active printer. Unable to write
|
|
||||||
return
|
|
||||||
|
|
||||||
if self.activePrinter.state not in ["idle", ""]:
|
|
||||||
# Printer is not able to accept commands.
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._authentication_state != AuthState.Authenticated:
|
|
||||||
# Not authenticated, so unable to send job.
|
|
||||||
return
|
|
||||||
|
|
||||||
self.writeStarted.emit(self)
|
|
||||||
|
|
||||||
gcode_dict = getattr(CuraApplication.getInstance().getController().getScene(), "gcode_dict", [])
|
|
||||||
active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
|
||||||
gcode_list = gcode_dict[active_build_plate_id]
|
|
||||||
|
|
||||||
if not gcode_list:
|
|
||||||
# Unable to find g-code. Nothing to send
|
|
||||||
return
|
|
||||||
|
|
||||||
self._gcode = gcode_list
|
|
||||||
|
|
||||||
errors = self._checkForErrors()
|
|
||||||
if errors:
|
|
||||||
text = i18n_catalog.i18nc("@label", "Unable to start a new print job.")
|
|
||||||
informative_text = i18n_catalog.i18nc("@label",
|
|
||||||
"There is an issue with the configuration of your Ultimaker, which makes it impossible to start the print. "
|
|
||||||
"Please resolve this issues before continuing.")
|
|
||||||
detailed_text = ""
|
|
||||||
for error in errors:
|
|
||||||
detailed_text += error + "\n"
|
|
||||||
|
|
||||||
CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
|
|
||||||
text,
|
|
||||||
informative_text,
|
|
||||||
detailed_text,
|
|
||||||
buttons=QMessageBox.Ok,
|
|
||||||
icon=QMessageBox.Critical,
|
|
||||||
callback = self._messageBoxCallback
|
|
||||||
)
|
|
||||||
return # Don't continue; Errors must block sending the job to the printer.
|
|
||||||
|
|
||||||
# There might be multiple things wrong with the configuration. Check these before starting.
|
|
||||||
warnings = self._checkForWarnings()
|
|
||||||
|
|
||||||
if warnings:
|
|
||||||
text = i18n_catalog.i18nc("@label", "Are you sure you wish to print with the selected configuration?")
|
|
||||||
informative_text = i18n_catalog.i18nc("@label",
|
|
||||||
"There is a mismatch between the configuration or calibration of the printer and Cura. "
|
|
||||||
"For the best result, always slice for the PrintCores and materials that are inserted in your printer.")
|
|
||||||
detailed_text = ""
|
|
||||||
for warning in warnings:
|
|
||||||
detailed_text += warning + "\n"
|
|
||||||
|
|
||||||
CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Mismatched configuration"),
|
|
||||||
text,
|
|
||||||
informative_text,
|
|
||||||
detailed_text,
|
|
||||||
buttons=QMessageBox.Yes + QMessageBox.No,
|
|
||||||
icon=QMessageBox.Question,
|
|
||||||
callback=self._messageBoxCallback
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# No warnings or errors, so we're good to go.
|
|
||||||
self._startPrint()
|
|
||||||
|
|
||||||
# Notify the UI that a switch to the print monitor should happen
|
|
||||||
CuraApplication.getInstance().getController().setActiveStage("MonitorStage")
|
|
||||||
|
|
||||||
def _startPrint(self):
|
|
||||||
Logger.log("i", "Sending print job to printer.")
|
|
||||||
if self._sending_gcode:
|
|
||||||
self._error_message = Message(
|
|
||||||
i18n_catalog.i18nc("@info:status",
|
|
||||||
"Sending new jobs (temporarily) blocked, still sending the previous print job."))
|
|
||||||
self._error_message.show()
|
|
||||||
return
|
|
||||||
|
|
||||||
self._sending_gcode = True
|
|
||||||
|
|
||||||
self._send_gcode_start = time()
|
|
||||||
self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), 0, False, -1,
|
|
||||||
i18n_catalog.i18nc("@info:title", "Sending Data"))
|
|
||||||
self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), None, "")
|
|
||||||
self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
|
|
||||||
self._progress_message.show()
|
|
||||||
|
|
||||||
compressed_gcode = self._compressGCode()
|
|
||||||
if compressed_gcode is None:
|
|
||||||
# Abort was called.
|
|
||||||
return
|
|
||||||
|
|
||||||
file_name = "%s.gcode.gz" % CuraApplication.getInstance().getPrintInformation().jobName
|
|
||||||
self.postForm("print_job", "form-data; name=\"file\";filename=\"%s\"" % file_name, compressed_gcode,
|
|
||||||
on_finished=self._onPostPrintJobFinished)
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
def _progressMessageActionTriggered(self, message_id=None, action_id=None):
|
|
||||||
if action_id == "Abort":
|
|
||||||
Logger.log("d", "User aborted sending print to remote.")
|
|
||||||
self._progress_message.hide()
|
|
||||||
self._compressing_gcode = False
|
|
||||||
self._sending_gcode = False
|
|
||||||
CuraApplication.getInstance().getController().setActiveStage("PrepareStage")
|
|
||||||
|
|
||||||
def _onPostPrintJobFinished(self, reply):
|
|
||||||
self._progress_message.hide()
|
|
||||||
self._sending_gcode = False
|
|
||||||
|
|
||||||
def _onUploadPrintJobProgress(self, bytes_sent, bytes_total):
|
|
||||||
if bytes_total > 0:
|
|
||||||
new_progress = bytes_sent / bytes_total * 100
|
|
||||||
# Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
|
|
||||||
# timeout responses if this happens.
|
|
||||||
self._last_response_time = time()
|
|
||||||
if new_progress > self._progress_message.getProgress():
|
|
||||||
self._progress_message.show() # Ensure that the message is visible.
|
|
||||||
self._progress_message.setProgress(bytes_sent / bytes_total * 100)
|
|
||||||
else:
|
|
||||||
self._progress_message.setProgress(0)
|
|
||||||
|
|
||||||
self._progress_message.hide()
|
|
||||||
|
|
||||||
def _messageBoxCallback(self, button):
|
|
||||||
def delayedCallback():
|
|
||||||
if button == QMessageBox.Yes:
|
|
||||||
self._startPrint()
|
|
||||||
else:
|
|
||||||
CuraApplication.getInstance().getController().setActiveStage("PrepareStage")
|
|
||||||
# For some unknown reason Cura on OSX will hang if we do the call back code
|
|
||||||
# immediately without first returning and leaving QML's event system.
|
|
||||||
|
|
||||||
QTimer.singleShot(100, delayedCallback)
|
|
||||||
|
|
||||||
def _checkForErrors(self):
|
|
||||||
errors = []
|
|
||||||
print_information = CuraApplication.getInstance().getPrintInformation()
|
|
||||||
if not print_information.materialLengths:
|
|
||||||
Logger.log("w", "There is no material length information. Unable to check for errors.")
|
|
||||||
return errors
|
|
||||||
|
|
||||||
for index, extruder in enumerate(self.activePrinter.extruders):
|
|
||||||
# Due to airflow issues, both slots must be loaded, regardless if they are actually used or not.
|
|
||||||
if extruder.hotendID == "":
|
|
||||||
# No Printcore loaded.
|
|
||||||
errors.append(i18n_catalog.i18nc("@info:status", "No Printcore loaded in slot {slot_number}".format(slot_number=index + 1)))
|
|
||||||
|
|
||||||
if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
|
|
||||||
# The extruder is by this print.
|
|
||||||
if extruder.activeMaterial is None:
|
|
||||||
# No active material
|
|
||||||
errors.append(i18n_catalog.i18nc("@info:status", "No material loaded in slot {slot_number}".format(slot_number=index + 1)))
|
|
||||||
return errors
|
|
||||||
|
|
||||||
def _checkForWarnings(self):
|
|
||||||
warnings = []
|
|
||||||
print_information = CuraApplication.getInstance().getPrintInformation()
|
|
||||||
|
|
||||||
if not print_information.materialLengths:
|
|
||||||
Logger.log("w", "There is no material length information. Unable to check for warnings.")
|
|
||||||
return warnings
|
|
||||||
|
|
||||||
extruder_manager = ExtruderManager.getInstance()
|
|
||||||
|
|
||||||
for index, extruder in enumerate(self.activePrinter.extruders):
|
|
||||||
if index < len(print_information.materialLengths) and print_information.materialLengths[index] != 0:
|
|
||||||
# The extruder is by this print.
|
|
||||||
|
|
||||||
# TODO: material length check
|
|
||||||
|
|
||||||
# Check if the right Printcore is active.
|
|
||||||
variant = extruder_manager.getExtruderStack(index).findContainer({"type": "variant"})
|
|
||||||
if variant:
|
|
||||||
if variant.getName() != extruder.hotendID:
|
|
||||||
warnings.append(i18n_catalog.i18nc("@label", "Different PrintCore (Cura: {cura_printcore_name}, Printer: {remote_printcore_name}) selected for extruder {extruder_id}".format(cura_printcore_name = variant.getName(), remote_printcore_name = extruder.hotendID, extruder_id = index + 1)))
|
|
||||||
else:
|
|
||||||
Logger.log("w", "Unable to find variant.")
|
|
||||||
|
|
||||||
# Check if the right material is loaded.
|
|
||||||
local_material = extruder_manager.getExtruderStack(index).findContainer({"type": "material"})
|
|
||||||
if local_material:
|
|
||||||
if extruder.activeMaterial.guid != local_material.getMetaDataEntry("GUID"):
|
|
||||||
Logger.log("w", "Extruder %s has a different material (%s) as Cura (%s)", index + 1, extruder.activeMaterial.guid, local_material.getMetaDataEntry("GUID"))
|
|
||||||
warnings.append(i18n_catalog.i18nc("@label", "Different material (Cura: {0}, Printer: {1}) selected for extruder {2}").format(local_material.getName(), extruder.activeMaterial.name, index + 1))
|
|
||||||
else:
|
|
||||||
Logger.log("w", "Unable to find material.")
|
|
||||||
|
|
||||||
return warnings
|
|
||||||
|
|
||||||
def _update(self):
|
|
||||||
if not super()._update():
|
|
||||||
return
|
|
||||||
if self._authentication_state == AuthState.NotAuthenticated:
|
|
||||||
if self._authentication_id is None and self._authentication_key is None:
|
|
||||||
# This machine doesn't have any authentication, so request it.
|
|
||||||
self._requestAuthentication()
|
|
||||||
elif self._authentication_id is not None and self._authentication_key is not None:
|
|
||||||
# We have authentication info, but we haven't checked it out yet. Do so now.
|
|
||||||
self._verifyAuthentication()
|
|
||||||
elif self._authentication_state == AuthState.AuthenticationReceived:
|
|
||||||
# We have an authentication, but it's not confirmed yet.
|
|
||||||
self._checkAuthentication()
|
|
||||||
|
|
||||||
# We don't need authentication for requesting info, so we can go right ahead with requesting this.
|
|
||||||
self.get("printer", on_finished=self._onGetPrinterDataFinished)
|
|
||||||
self.get("print_job", on_finished=self._onGetPrintJobFinished)
|
|
||||||
|
|
||||||
def _resetAuthenticationRequestedMessage(self):
|
|
||||||
if self._authentication_requested_message:
|
|
||||||
self._authentication_requested_message.hide()
|
|
||||||
self._authentication_timer.stop()
|
|
||||||
self._authentication_counter = 0
|
|
||||||
|
|
||||||
def _onAuthenticationTimer(self):
|
|
||||||
self._authentication_counter += 1
|
|
||||||
self._authentication_requested_message.setProgress(
|
|
||||||
self._authentication_counter / self._max_authentication_counter * 100)
|
|
||||||
if self._authentication_counter > self._max_authentication_counter:
|
|
||||||
self._authentication_timer.stop()
|
|
||||||
Logger.log("i", "Authentication timer ended. Setting authentication to denied for printer: %s" % self._id)
|
|
||||||
self.setAuthenticationState(AuthState.AuthenticationDenied)
|
|
||||||
self._resetAuthenticationRequestedMessage()
|
|
||||||
self._authentication_failed_message.show()
|
|
||||||
|
|
||||||
def _verifyAuthentication(self):
|
|
||||||
Logger.log("d", "Attempting to verify authentication")
|
|
||||||
# This will ensure that the "_onAuthenticationRequired" is triggered, which will setup the authenticator.
|
|
||||||
self.get("auth/verify", on_finished=self._onVerifyAuthenticationCompleted)
|
|
||||||
|
|
||||||
def _onVerifyAuthenticationCompleted(self, reply):
|
|
||||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
|
||||||
if status_code == 401:
|
|
||||||
# Something went wrong; We somehow tried to verify authentication without having one.
|
|
||||||
Logger.log("d", "Attempted to verify auth without having one.")
|
|
||||||
self._authentication_id = None
|
|
||||||
self._authentication_key = None
|
|
||||||
self.setAuthenticationState(AuthState.NotAuthenticated)
|
|
||||||
elif status_code == 403 and self._authentication_state != AuthState.Authenticated:
|
|
||||||
# If we were already authenticated, we probably got an older message back all of the sudden. Drop that.
|
|
||||||
Logger.log("d",
|
|
||||||
"While trying to verify the authentication state, we got a forbidden response. Our own auth state was %s. ",
|
|
||||||
self._authentication_state)
|
|
||||||
self.setAuthenticationState(AuthState.AuthenticationDenied)
|
|
||||||
self._authentication_failed_message.show()
|
|
||||||
elif status_code == 200:
|
|
||||||
self.setAuthenticationState(AuthState.Authenticated)
|
|
||||||
|
|
||||||
def _checkAuthentication(self):
|
|
||||||
Logger.log("d", "Checking if authentication is correct for id %s and key %s", self._authentication_id, self._getSafeAuthKey())
|
|
||||||
self.get("auth/check/" + str(self._authentication_id), on_finished=self._onCheckAuthenticationFinished)
|
|
||||||
|
|
||||||
def _onCheckAuthenticationFinished(self, reply):
|
|
||||||
if str(self._authentication_id) not in reply.url().toString():
|
|
||||||
Logger.log("w", "Got an old id response.")
|
|
||||||
# Got response for old authentication ID.
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
data = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
|
||||||
except json.decoder.JSONDecodeError:
|
|
||||||
Logger.log("w", "Received an invalid authentication check from printer: Not valid JSON.")
|
|
||||||
return
|
|
||||||
|
|
||||||
if data.get("message", "") == "authorized":
|
|
||||||
Logger.log("i", "Authentication was approved")
|
|
||||||
self.setAuthenticationState(AuthState.Authenticated)
|
|
||||||
self._saveAuthentication()
|
|
||||||
|
|
||||||
# Double check that everything went well.
|
|
||||||
self._verifyAuthentication()
|
|
||||||
|
|
||||||
# Notify the user.
|
|
||||||
self._resetAuthenticationRequestedMessage()
|
|
||||||
self._authentication_succeeded_message.show()
|
|
||||||
elif data.get("message", "") == "unauthorized":
|
|
||||||
Logger.log("i", "Authentication was denied.")
|
|
||||||
self.setAuthenticationState(AuthState.AuthenticationDenied)
|
|
||||||
self._authentication_failed_message.show()
|
|
||||||
|
|
||||||
def _saveAuthentication(self) -> None:
|
|
||||||
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
|
||||||
if self._authentication_key is None:
|
|
||||||
Logger.log("e", "Authentication key is None, nothing to save.")
|
|
||||||
return
|
|
||||||
if self._authentication_id is None:
|
|
||||||
Logger.log("e", "Authentication id is None, nothing to save.")
|
|
||||||
return
|
|
||||||
if global_container_stack:
|
|
||||||
global_container_stack.setMetaDataEntry("network_authentication_key", self._authentication_key)
|
|
||||||
|
|
||||||
global_container_stack.setMetaDataEntry("network_authentication_id", self._authentication_id)
|
|
||||||
|
|
||||||
# Force save so we are sure the data is not lost.
|
|
||||||
CuraApplication.getInstance().saveStack(global_container_stack)
|
|
||||||
Logger.log("i", "Authentication succeeded for id %s and key %s", self._authentication_id,
|
|
||||||
self._getSafeAuthKey())
|
|
||||||
else:
|
|
||||||
Logger.log("e", "Unable to save authentication for id %s and key %s", self._authentication_id,
|
|
||||||
self._getSafeAuthKey())
|
|
||||||
|
|
||||||
def _onRequestAuthenticationFinished(self, reply):
|
|
||||||
try:
|
|
||||||
data = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
|
||||||
except json.decoder.JSONDecodeError:
|
|
||||||
Logger.log("w", "Received an invalid authentication request reply from printer: Not valid JSON.")
|
|
||||||
self.setAuthenticationState(AuthState.NotAuthenticated)
|
|
||||||
return
|
|
||||||
|
|
||||||
self.setAuthenticationState(AuthState.AuthenticationReceived)
|
|
||||||
self._authentication_id = data["id"]
|
|
||||||
self._authentication_key = data["key"]
|
|
||||||
Logger.log("i", "Got a new authentication ID (%s) and KEY (%s). Waiting for authorization.",
|
|
||||||
self._authentication_id, self._getSafeAuthKey())
|
|
||||||
|
|
||||||
def _requestAuthentication(self):
|
|
||||||
self._authentication_requested_message.show()
|
|
||||||
self._authentication_timer.start()
|
|
||||||
|
|
||||||
# Reset any previous authentication info. If this isn't done, the "Retry" action on the failed message might
|
|
||||||
# give issues.
|
|
||||||
self._authentication_key = None
|
|
||||||
self._authentication_id = None
|
|
||||||
|
|
||||||
self.post("auth/request",
|
|
||||||
json.dumps({"application": "Cura-" + CuraApplication.getInstance().getVersion(),
|
|
||||||
"user": self._getUserName()}),
|
|
||||||
on_finished=self._onRequestAuthenticationFinished)
|
|
||||||
|
|
||||||
self.setAuthenticationState(AuthState.AuthenticationRequested)
|
|
||||||
|
|
||||||
def _onAuthenticationRequired(self, reply, authenticator):
|
|
||||||
if self._authentication_id is not None and self._authentication_key is not None:
|
|
||||||
Logger.log("d",
|
|
||||||
"Authentication was required for printer: %s. Setting up authenticator with ID %s and key %s",
|
|
||||||
self._id, self._authentication_id, self._getSafeAuthKey())
|
|
||||||
authenticator.setUser(self._authentication_id)
|
|
||||||
authenticator.setPassword(self._authentication_key)
|
|
||||||
else:
|
|
||||||
Logger.log("d", "No authentication is available to use for %s, but we did got a request for it.", self._id)
|
|
||||||
|
|
||||||
def _onGetPrintJobFinished(self, reply):
|
|
||||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
|
||||||
|
|
||||||
if not self._printers:
|
|
||||||
return # Ignore the data for now, we don't have info about a printer yet.
|
|
||||||
printer = self._printers[0]
|
|
||||||
|
|
||||||
if status_code == 200:
|
|
||||||
try:
|
|
||||||
result = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
|
||||||
except json.decoder.JSONDecodeError:
|
|
||||||
Logger.log("w", "Received an invalid print job state message: Not valid JSON.")
|
|
||||||
return
|
|
||||||
if printer.activePrintJob is None:
|
|
||||||
print_job = PrintJobOutputModel(output_controller=self._output_controller)
|
|
||||||
printer.updateActivePrintJob(print_job)
|
|
||||||
else:
|
|
||||||
print_job = printer.activePrintJob
|
|
||||||
print_job.updateState(result["state"])
|
|
||||||
print_job.updateTimeElapsed(result["time_elapsed"])
|
|
||||||
print_job.updateTimeTotal(result["time_total"])
|
|
||||||
print_job.updateName(result["name"])
|
|
||||||
elif status_code == 404:
|
|
||||||
# No job found, so delete the active print job (if any!)
|
|
||||||
printer.updateActivePrintJob(None)
|
|
||||||
else:
|
|
||||||
Logger.log("w",
|
|
||||||
"Got status code {status_code} while trying to get printer data".format(status_code=status_code))
|
|
||||||
|
|
||||||
def materialHotendChangedMessage(self, callback):
|
|
||||||
CuraApplication.getInstance().messageBox(i18n_catalog.i18nc("@window:title", "Sync with your printer"),
|
|
||||||
i18n_catalog.i18nc("@label",
|
|
||||||
"Would you like to use your current printer configuration in Cura?"),
|
|
||||||
i18n_catalog.i18nc("@label",
|
|
||||||
"The PrintCores and/or materials on your printer differ from those within your current project. For the best result, always slice for the PrintCores and materials that are inserted in your printer."),
|
|
||||||
buttons=QMessageBox.Yes + QMessageBox.No,
|
|
||||||
icon=QMessageBox.Question,
|
|
||||||
callback=callback
|
|
||||||
)
|
|
||||||
|
|
||||||
def _onGetPrinterDataFinished(self, reply):
|
|
||||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
|
||||||
if status_code == 200:
|
|
||||||
try:
|
|
||||||
result = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
|
||||||
except json.decoder.JSONDecodeError:
|
|
||||||
Logger.log("w", "Received an invalid printer state message: Not valid JSON.")
|
|
||||||
return
|
|
||||||
|
|
||||||
if not self._printers:
|
|
||||||
# Quickest way to get the firmware version is to grab it from the zeroconf.
|
|
||||||
firmware_version = self._properties.get(b"firmware_version", b"").decode("utf-8")
|
|
||||||
self._printers = [PrinterOutputModel(output_controller=self._output_controller, number_of_extruders=self._number_of_extruders, firmware_version=firmware_version)]
|
|
||||||
self._printers[0].setCameraUrl(QUrl("http://" + self._address + ":8080/?action=stream"))
|
|
||||||
for extruder in self._printers[0].extruders:
|
|
||||||
extruder.activeMaterialChanged.connect(self.materialIdChanged)
|
|
||||||
extruder.hotendIDChanged.connect(self.hotendIdChanged)
|
|
||||||
self.printersChanged.emit()
|
|
||||||
|
|
||||||
# LegacyUM3 always has a single printer.
|
|
||||||
printer = self._printers[0]
|
|
||||||
printer.updateBedTemperature(result["bed"]["temperature"]["current"])
|
|
||||||
printer.updateTargetBedTemperature(result["bed"]["temperature"]["target"])
|
|
||||||
printer.updateState(result["status"])
|
|
||||||
|
|
||||||
try:
|
|
||||||
# If we're still handling the request, we should ignore remote for a bit.
|
|
||||||
if not printer.getController().isPreheatRequestInProgress():
|
|
||||||
printer.updateIsPreheating(result["bed"]["pre_heat"]["active"])
|
|
||||||
except KeyError:
|
|
||||||
# Older firmwares don't support preheating, so we need to fake it.
|
|
||||||
pass
|
|
||||||
|
|
||||||
head_position = result["heads"][0]["position"]
|
|
||||||
printer.updateHeadPosition(head_position["x"], head_position["y"], head_position["z"])
|
|
||||||
|
|
||||||
for index in range(0, self._number_of_extruders):
|
|
||||||
temperatures = result["heads"][0]["extruders"][index]["hotend"]["temperature"]
|
|
||||||
extruder = printer.extruders[index]
|
|
||||||
extruder.updateTargetHotendTemperature(temperatures["target"])
|
|
||||||
extruder.updateHotendTemperature(temperatures["current"])
|
|
||||||
|
|
||||||
material_guid = result["heads"][0]["extruders"][index]["active_material"]["guid"]
|
|
||||||
|
|
||||||
if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_guid:
|
|
||||||
# Find matching material (as we need to set brand, type & color)
|
|
||||||
containers = ContainerRegistry.getInstance().findInstanceContainers(type="material",
|
|
||||||
GUID=material_guid)
|
|
||||||
if containers:
|
|
||||||
color = containers[0].getMetaDataEntry("color_code")
|
|
||||||
brand = containers[0].getMetaDataEntry("brand")
|
|
||||||
material_type = containers[0].getMetaDataEntry("material")
|
|
||||||
name = containers[0].getName()
|
|
||||||
else:
|
|
||||||
# Unknown material.
|
|
||||||
color = "#00000000"
|
|
||||||
brand = "Unknown"
|
|
||||||
material_type = "Unknown"
|
|
||||||
name = "Unknown"
|
|
||||||
material = MaterialOutputModel(guid=material_guid, type=material_type,
|
|
||||||
brand=brand, color=color, name = name)
|
|
||||||
extruder.updateActiveMaterial(material)
|
|
||||||
|
|
||||||
try:
|
|
||||||
hotend_id = result["heads"][0]["extruders"][index]["hotend"]["id"]
|
|
||||||
except KeyError:
|
|
||||||
hotend_id = ""
|
|
||||||
printer.extruders[index].updateHotendID(hotend_id)
|
|
||||||
|
|
||||||
else:
|
|
||||||
Logger.log("w",
|
|
||||||
"Got status code {status_code} while trying to get printer data".format(status_code = status_code))
|
|
||||||
|
|
||||||
## Convenience function to "blur" out all but the last 5 characters of the auth key.
|
|
||||||
# This can be used to debug print the key, without it compromising the security.
|
|
||||||
def _getSafeAuthKey(self):
|
|
||||||
if self._authentication_key is not None:
|
|
||||||
result = self._authentication_key[-5:]
|
|
||||||
result = "********" + result
|
|
||||||
return result
|
|
||||||
|
|
||||||
return self._authentication_key
|
|
|
@ -1,96 +0,0 @@
|
||||||
# Copyright (c) 2019 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
|
||||||
from PyQt5.QtCore import QTimer
|
|
||||||
from UM.Version import Version
|
|
||||||
|
|
||||||
MYPY = False
|
|
||||||
if MYPY:
|
|
||||||
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
|
|
||||||
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
|
||||||
|
|
||||||
|
|
||||||
class LegacyUM3PrinterOutputController(PrinterOutputController):
|
|
||||||
def __init__(self, output_device):
|
|
||||||
super().__init__(output_device)
|
|
||||||
self._preheat_bed_timer = QTimer()
|
|
||||||
self._preheat_bed_timer.setSingleShot(True)
|
|
||||||
self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished)
|
|
||||||
self._preheat_printer = None
|
|
||||||
|
|
||||||
self.can_control_manually = False
|
|
||||||
self.can_send_raw_gcode = False
|
|
||||||
|
|
||||||
# Are we still waiting for a response about preheat?
|
|
||||||
# We need this so we can already update buttons, so it feels more snappy.
|
|
||||||
self._preheat_request_in_progress = False
|
|
||||||
|
|
||||||
def isPreheatRequestInProgress(self):
|
|
||||||
return self._preheat_request_in_progress
|
|
||||||
|
|
||||||
def setJobState(self, job: "PrintJobOutputModel", state: str):
|
|
||||||
data = "{\"target\": \"%s\"}" % state
|
|
||||||
self._output_device.put("print_job/state", data, on_finished=None)
|
|
||||||
|
|
||||||
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: float):
|
|
||||||
data = str(temperature)
|
|
||||||
self._output_device.put("printer/bed/temperature/target", data, on_finished = self._onPutBedTemperatureCompleted)
|
|
||||||
|
|
||||||
def _onPutBedTemperatureCompleted(self, reply):
|
|
||||||
if Version(self._preheat_printer.firmwareVersion) < Version("3.5.92"):
|
|
||||||
# If it was handling a preheat, it isn't anymore.
|
|
||||||
self._preheat_request_in_progress = False
|
|
||||||
|
|
||||||
def _onPutPreheatBedCompleted(self, reply):
|
|
||||||
self._preheat_request_in_progress = False
|
|
||||||
|
|
||||||
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed):
|
|
||||||
head_pos = printer._head_position
|
|
||||||
new_x = head_pos.x + x
|
|
||||||
new_y = head_pos.y + y
|
|
||||||
new_z = head_pos.z + z
|
|
||||||
data = "{\n\"x\":%s,\n\"y\":%s,\n\"z\":%s\n}" %(new_x, new_y, new_z)
|
|
||||||
self._output_device.put("printer/heads/0/position", data, on_finished=None)
|
|
||||||
|
|
||||||
def homeBed(self, printer):
|
|
||||||
self._output_device.put("printer/heads/0/position/z", "0", on_finished=None)
|
|
||||||
|
|
||||||
def _onPreheatBedTimerFinished(self):
|
|
||||||
self.setTargetBedTemperature(self._preheat_printer, 0)
|
|
||||||
self._preheat_printer.updateIsPreheating(False)
|
|
||||||
self._preheat_request_in_progress = True
|
|
||||||
|
|
||||||
def cancelPreheatBed(self, printer: "PrinterOutputModel"):
|
|
||||||
self.preheatBed(printer, temperature=0, duration=0)
|
|
||||||
self._preheat_bed_timer.stop()
|
|
||||||
printer.updateIsPreheating(False)
|
|
||||||
|
|
||||||
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
|
|
||||||
try:
|
|
||||||
temperature = round(temperature) # The API doesn't allow floating point.
|
|
||||||
duration = round(duration)
|
|
||||||
except ValueError:
|
|
||||||
return # Got invalid values, can't pre-heat.
|
|
||||||
|
|
||||||
if duration > 0:
|
|
||||||
data = """{"temperature": "%i", "timeout": "%i"}""" % (temperature, duration)
|
|
||||||
else:
|
|
||||||
data = """{"temperature": "%i"}""" % temperature
|
|
||||||
|
|
||||||
# Real bed pre-heating support is implemented from 3.5.92 and up.
|
|
||||||
|
|
||||||
if Version(printer.firmwareVersion) < Version("3.5.92"):
|
|
||||||
# No firmware-side duration support then, so just set target bed temp and set a timer.
|
|
||||||
self.setTargetBedTemperature(printer, temperature=temperature)
|
|
||||||
self._preheat_bed_timer.setInterval(duration * 1000)
|
|
||||||
self._preheat_bed_timer.start()
|
|
||||||
self._preheat_printer = printer
|
|
||||||
printer.updateIsPreheating(True)
|
|
||||||
return
|
|
||||||
|
|
||||||
self._output_device.put("printer/bed/pre_heat", data, on_finished = self._onPutPreheatBedCompleted)
|
|
||||||
printer.updateIsPreheating(True)
|
|
||||||
self._preheat_request_in_progress = True
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
import io
|
import io
|
||||||
from typing import Optional, Dict, Union, List, cast
|
from typing import Optional, Dict, Union, List, cast
|
||||||
|
@ -32,7 +32,7 @@ class MeshFormatHandler:
|
||||||
# \return A dict with the file format details, with the following keys:
|
# \return A dict with the file format details, with the following keys:
|
||||||
# {id: str, extension: str, description: str, mime_type: str, mode: int, hide_in_file_dialog: bool}
|
# {id: str, extension: str, description: str, mime_type: str, mode: int, hide_in_file_dialog: bool}
|
||||||
@property
|
@property
|
||||||
def preferred_format(self) -> Optional[Dict[str, Union[str, int, bool]]]:
|
def preferred_format(self) -> Dict[str, Union[str, int, bool]]:
|
||||||
return self._preferred_format
|
return self._preferred_format
|
||||||
|
|
||||||
## Gets the file writer for the given file handler and mime type.
|
## Gets the file writer for the given file handler and mime type.
|
||||||
|
@ -90,6 +90,7 @@ class MeshFormatHandler:
|
||||||
|
|
||||||
machine_file_formats = global_stack.getMetaDataEntry("file_formats").split(";")
|
machine_file_formats = global_stack.getMetaDataEntry("file_formats").split(";")
|
||||||
machine_file_formats = [file_type.strip() for file_type in machine_file_formats]
|
machine_file_formats = [file_type.strip() for file_type in machine_file_formats]
|
||||||
|
|
||||||
# Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format.
|
# Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format.
|
||||||
if "application/x-ufp" not in machine_file_formats and Version(firmware_version) >= Version("4.4"):
|
if "application/x-ufp" not in machine_file_formats and Version(firmware_version) >= Version("4.4"):
|
||||||
machine_file_formats = ["application/x-ufp"] + machine_file_formats
|
machine_file_formats = ["application/x-ufp"] + machine_file_formats
|
||||||
|
|
41
plugins/UM3NetworkPrinting/src/Messages/CloudFlowMessage.py
Normal file
41
plugins/UM3NetworkPrinting/src/Messages/CloudFlowMessage.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
import os
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QUrl
|
||||||
|
from PyQt5.QtGui import QDesktopServices
|
||||||
|
|
||||||
|
from UM import i18nCatalog
|
||||||
|
from UM.Message import Message
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
|
||||||
|
I18N_CATALOG = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
class CloudFlowMessage(Message):
|
||||||
|
|
||||||
|
def __init__(self, address: str) -> None:
|
||||||
|
|
||||||
|
image_path = os.path.join(
|
||||||
|
CuraApplication.getInstance().getPluginRegistry().getPluginPath("UM3NetworkPrinting") or "",
|
||||||
|
"resources", "svg", "cloud-flow-start.svg"
|
||||||
|
)
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
text=I18N_CATALOG.i18nc("@info:status",
|
||||||
|
"Send and monitor print jobs from anywhere using your Ultimaker account."),
|
||||||
|
lifetime=0,
|
||||||
|
dismissable=True,
|
||||||
|
option_state=False,
|
||||||
|
image_source=image_path,
|
||||||
|
image_caption=I18N_CATALOG.i18nc("@info:status Ultimaker Cloud should not be translated.",
|
||||||
|
"Connect to Ultimaker Cloud"),
|
||||||
|
)
|
||||||
|
self._address = address
|
||||||
|
self.addAction("", I18N_CATALOG.i18nc("@action", "Get started"), "", "")
|
||||||
|
self.actionTriggered.connect(self._onCloudFlowStarted)
|
||||||
|
|
||||||
|
def _onCloudFlowStarted(self, messageId: str, actionId: str) -> None:
|
||||||
|
QDesktopServices.openUrl(QUrl("http://{}/cloud_connect".format(self._address)))
|
||||||
|
self.hide()
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from UM import i18nCatalog
|
||||||
|
from UM.Message import Message
|
||||||
|
|
||||||
|
|
||||||
|
I18N_CATALOG = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
## Message shown when trying to connect to a legacy printer device.
|
||||||
|
class LegacyDeviceNoLongerSupportedMessage(Message):
|
||||||
|
|
||||||
|
# Singleton used to prevent duplicate messages of this type at the same time.
|
||||||
|
__is_visible = False
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(
|
||||||
|
text = I18N_CATALOG.i18nc("@info:status", "You are attempting to connect to a printer that is not "
|
||||||
|
"running Ultimaker Connect. Please update the printer to the "
|
||||||
|
"latest firmware."),
|
||||||
|
title = I18N_CATALOG.i18nc("@info:title", "Update your printer"),
|
||||||
|
lifetime = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
def show(self) -> None:
|
||||||
|
if LegacyDeviceNoLongerSupportedMessage.__is_visible:
|
||||||
|
return
|
||||||
|
super().show()
|
||||||
|
LegacyDeviceNoLongerSupportedMessage.__is_visible = True
|
||||||
|
|
||||||
|
def hide(self, send_signal = True) -> None:
|
||||||
|
super().hide(send_signal)
|
||||||
|
LegacyDeviceNoLongerSupportedMessage.__is_visible = False
|
|
@ -0,0 +1,50 @@
|
||||||
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QUrl
|
||||||
|
from PyQt5.QtGui import QDesktopServices
|
||||||
|
|
||||||
|
from UM import i18nCatalog
|
||||||
|
from UM.Message import Message
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..UltimakerNetworkedPrinterOutputDevice import UltimakerNetworkedPrinterOutputDevice
|
||||||
|
|
||||||
|
|
||||||
|
I18N_CATALOG = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
## Message shown when trying to connect to a printer that is not a host.
|
||||||
|
class NotClusterHostMessage(Message):
|
||||||
|
|
||||||
|
# Singleton used to prevent duplicate messages of this type at the same time.
|
||||||
|
__is_visible = False
|
||||||
|
|
||||||
|
def __init__(self, device: "UltimakerNetworkedPrinterOutputDevice") -> None:
|
||||||
|
super().__init__(
|
||||||
|
text = I18N_CATALOG.i18nc("@info:status", "You are attempting to connect to {0} but it is not "
|
||||||
|
"the host of a group. You can visit the web page to configure "
|
||||||
|
"it as a group host.", device.name),
|
||||||
|
title = I18N_CATALOG.i18nc("@info:title", "Not a group host"),
|
||||||
|
lifetime = 0,
|
||||||
|
dismissable = True
|
||||||
|
)
|
||||||
|
self._address = device.address
|
||||||
|
self.addAction("", I18N_CATALOG.i18nc("@action", "Configure group"), "", "")
|
||||||
|
self.actionTriggered.connect(self._onConfigureClicked)
|
||||||
|
|
||||||
|
def show(self) -> None:
|
||||||
|
if NotClusterHostMessage.__is_visible:
|
||||||
|
return
|
||||||
|
super().show()
|
||||||
|
NotClusterHostMessage.__is_visible = True
|
||||||
|
|
||||||
|
def hide(self, send_signal = True) -> None:
|
||||||
|
super().hide(send_signal)
|
||||||
|
NotClusterHostMessage.__is_visible = False
|
||||||
|
|
||||||
|
def _onConfigureClicked(self, messageId: str, actionId: str) -> None:
|
||||||
|
QDesktopServices.openUrl(QUrl("http://{}/print_jobs".format(self._address)))
|
||||||
|
self.hide()
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from UM import i18nCatalog
|
||||||
|
from UM.Message import Message
|
||||||
|
|
||||||
|
|
||||||
|
I18N_CATALOG = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
## Message shown when uploading a print job to a cluster is blocked because another upload is already in progress.
|
||||||
|
class PrintJobUploadBlockedMessage(Message):
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(
|
||||||
|
text = I18N_CATALOG.i18nc("@info:status", "Please wait until the current job has been sent."),
|
||||||
|
title = I18N_CATALOG.i18nc("@info:title", "Print error"),
|
||||||
|
lifetime = 10
|
||||||
|
)
|
|
@ -0,0 +1,18 @@
|
||||||
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from UM import i18nCatalog
|
||||||
|
from UM.Message import Message
|
||||||
|
|
||||||
|
|
||||||
|
I18N_CATALOG = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
## Message shown when uploading a print job to a cluster failed.
|
||||||
|
class PrintJobUploadErrorMessage(Message):
|
||||||
|
|
||||||
|
def __init__(self, message: str = None) -> None:
|
||||||
|
super().__init__(
|
||||||
|
text = message or I18N_CATALOG.i18nc("@info:text", "Could not upload the data to the printer."),
|
||||||
|
title = I18N_CATALOG.i18nc("@info:title", "Network error"),
|
||||||
|
lifetime = 10
|
||||||
|
)
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue