Merge branch 'master' into feature_intent

This commit is contained in:
Diego Prado Gesto 2019-09-13 09:02:30 +02:00
commit d38e60ce06
232 changed files with 25907 additions and 9339 deletions

View file

@ -33,6 +33,8 @@ from cura.Settings.CuraContainerStack import _ContainerIndexes
from cura.CuraApplication import CuraApplication
from cura.Utils.Threading import call_on_qt_thread
from PyQt5.QtCore import QCoreApplication
from .WorkspaceDialog import WorkspaceDialog
i18n_catalog = i18nCatalog("cura")
@ -230,6 +232,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
else:
Logger.log("w", "Unknown definition container type %s for %s",
definition_container_type, definition_container_file)
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
Job.yieldThread()
if machine_definition_container_count != 1:
@ -256,6 +259,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
containers_found_dict["material"] = True
if not self._container_registry.isReadOnly(container_id): # Only non readonly materials can be in conflict
material_conflict = True
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
Job.yieldThread()
# Check if any quality_changes instance container is in conflict.
@ -325,7 +329,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# Ignore certain instance container types
Logger.log("w", "Ignoring instance container [%s] with type [%s]", container_id, container_type)
continue
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
Job.yieldThread()
if self._machine_info.quality_changes_info.global_info is None:
@ -402,7 +406,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
variant_id = parser["containers"][str(_ContainerIndexes.Variant)]
if variant_id not in ("empty", "empty_variant"):
self._machine_info.variant_info = instance_container_info_dict[variant_id]
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
Job.yieldThread()
# if the global stack is found, we check if there are conflicts in the extruder stacks
@ -657,6 +661,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults.
self._container_registry.addContainer(definition_container)
Job.yieldThread()
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
Logger.log("d", "Workspace loading is checking materials...")
# Get all the material files and check if they exist. If not, add them.
@ -706,6 +711,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
material_container.setDirty(True)
self._container_registry.addContainer(material_container)
Job.yieldThread()
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
# Handle quality changes if any
self._processQualityChanges(global_stack)

View file

@ -543,6 +543,15 @@ class CuraEngineBackend(QObject, Backend):
if error.getErrorCode() == Arcus.ErrorCode.BindFailedError and self._start_slice_job is not None:
self._start_slice_job.setIsCancelled(False)
# Check if there's any slicable object in the scene.
def hasSlicableObject(self) -> bool:
has_slicable = False
for node in DepthFirstIterator(self._scene.getRoot()):
if node.callDecoration("isSliceable"):
has_slicable = True
break
return has_slicable
## Remove old layer data (if any)
def _clearLayerData(self, build_plate_numbers: Set = None) -> None:
# Clear out any old gcode
@ -561,6 +570,10 @@ class CuraEngineBackend(QObject, Backend):
## Convenient function: mark everything to slice, emit state and clear layer data
def needsSlicing(self) -> None:
# CURA-6604: If there's no slicable object, do not (try to) trigger slice, which will clear all the current
# gcode. This can break Gcode file loading if it tries to remove it afterwards.
if not self.hasSlicableObject():
return
self.determineAutoSlicing()
self.stopSlicing()
self.markSliceAll()
@ -632,7 +645,10 @@ class CuraEngineBackend(QObject, Backend):
self.setState(BackendState.Done)
self.processingProgress.emit(1.0)
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically.
try:
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically.
except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
gcode_list = []
for index, line in enumerate(gcode_list):
replaced = line.replace("{print_time}", str(self._application.getPrintInformation().currentPrintTime.getDisplayString(DurationFormat.Format.ISO8601)))
replaced = replaced.replace("{filament_amount}", str(self._application.getPrintInformation().materialLengths))

View file

@ -4,11 +4,11 @@
import configparser
from typing import List, Optional, Tuple
from UM.PluginRegistry import PluginRegistry
from UM.Logger import Logger
from UM.Settings.ContainerFormatError import ContainerFormatError
from UM.Settings.InstanceContainer import InstanceContainer # The new profile to make.
from cura.CuraApplication import CuraApplication
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
from cura.ReaderWriters.ProfileReader import ProfileReader
import zipfile
@ -92,6 +92,14 @@ class CuraProfileReader(ProfileReader):
except Exception as e:
Logger.log("e", "Error while trying to parse profile: %s", str(e))
return None
global_stack = CuraApplication.getInstance().getGlobalContainerStack()
if global_stack is None:
return None
active_quality_definition = getMachineDefinitionIDForQualitySearch(global_stack.definition)
if profile.getMetaDataEntry("definition") != active_quality_definition:
profile.setMetaDataEntry("definition", active_quality_definition)
return profile
## Upgrade a serialized profile to the current profile format.

View file

@ -27,6 +27,6 @@ class GCodeGzReader(MeshReader):
file_data = file.read()
uncompressed_gcode = gzip.decompress(file_data).decode("utf-8")
PluginRegistry.getInstance().getPluginObject("GCodeReader").preReadFromStream(uncompressed_gcode)
result = PluginRegistry.getInstance().getPluginObject("GCodeReader").readFromStream(uncompressed_gcode)
result = PluginRegistry.getInstance().getPluginObject("GCodeReader").readFromStream(uncompressed_gcode, file_name)
return result

View file

@ -3,7 +3,7 @@
import math
import re
from typing import Dict, List, NamedTuple, Optional, Union
from typing import Dict, List, NamedTuple, Optional, Union, Set
import numpy
@ -38,6 +38,8 @@ class FlavorParser:
self._message = None # type: Optional[Message]
self._layer_number = 0
self._extruder_number = 0
# All extruder numbers that have been seen
self._extruders_seen = {0} # type: Set[int]
self._clearValues()
self._scene_node = None
# X, Y, Z position, F feedrate and E extruder values are stored
@ -66,7 +68,7 @@ class FlavorParser:
if n < 0:
return None
n += len(code)
pattern = re.compile("[;\s]")
pattern = re.compile("[;\\s]")
match = pattern.search(line, n)
m = match.start() if match is not None else -1
try:
@ -292,7 +294,12 @@ class FlavorParser:
extruder.getProperty("machine_nozzle_offset_y", "value")]
return result
def processGCodeStream(self, stream: str) -> Optional[CuraSceneNode]:
#
# CURA-6643
# This function needs the filename so it can be set to the SceneNode. Otherwise, if you load a GCode file and press
# F5, that gcode SceneNode will be removed because it doesn't have a file to be reloaded from.
#
def processGCodeStream(self, stream: str, filename: str) -> Optional["CuraSceneNode"]:
Logger.log("d", "Preparing to load GCode")
self._cancelled = False
# We obtain the filament diameter from the selected extruder to calculate line widths
@ -418,6 +425,7 @@ class FlavorParser:
if line.startswith("T"):
T = self._getInt(line, "T")
if T is not None:
self._extruders_seen.add(T)
self._createPolygon(self._current_layer_thickness, current_path, self._extruder_offsets.get(self._extruder_number, [0, 0]))
current_path.clear()
@ -453,6 +461,7 @@ class FlavorParser:
scene_node.addDecorator(decorator)
gcode_list_decorator = GCodeListDecorator()
gcode_list_decorator.setGcodeFileName(filename)
gcode_list_decorator.setGCodeList(gcode_list)
scene_node.addDecorator(gcode_list_decorator)
@ -467,10 +476,9 @@ class FlavorParser:
if self._layer_number == 0:
Logger.log("w", "File doesn't contain any valid layers")
settings = CuraApplication.getInstance().getGlobalContainerStack()
if settings is not None and not settings.getProperty("machine_center_is_zero", "value"):
machine_width = settings.getProperty("machine_width", "value")
machine_depth = settings.getProperty("machine_depth", "value")
if not global_stack.getProperty("machine_center_is_zero", "value"):
machine_width = global_stack.getProperty("machine_width", "value")
machine_depth = global_stack.getProperty("machine_depth", "value")
scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
Logger.log("d", "GCode loading finished")

View file

@ -2,6 +2,8 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Union, List, TYPE_CHECKING
from UM.FileHandler.FileReader import FileReader
from UM.Mesh.MeshReader import MeshReader
from UM.i18n import i18nCatalog
@ -9,8 +11,14 @@ from UM.Application import Application
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
catalog = i18nCatalog("cura")
from .FlavorParser import FlavorParser
from . import MarlinFlavorParser, RepRapFlavorParser
if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode
from cura.Scene.CuraSceneNode import CuraSceneNode
# Class for loading and parsing G-code files
class GCodeReader(MeshReader):
@ -30,7 +38,7 @@ class GCodeReader(MeshReader):
)
self._supported_extensions = [".gcode", ".g"]
self._flavor_reader = None
self._flavor_reader = None # type: Optional[FlavorParser]
Application.getInstance().getPreferences().addPreference("gcodereader/show_caution", True)
@ -54,10 +62,16 @@ class GCodeReader(MeshReader):
file_data = file.read()
return self.preReadFromStream(file_data, args, kwargs)
def readFromStream(self, stream):
return self._flavor_reader.processGCodeStream(stream)
def readFromStream(self, stream: str, filename: str) -> Optional["CuraSceneNode"]:
if self._flavor_reader is None:
return None
return self._flavor_reader.processGCodeStream(stream, filename)
def _read(self, file_name):
def _read(self, file_name: str) -> Union["SceneNode", List["SceneNode"]]:
with open(file_name, "r", encoding = "utf-8") as file:
file_data = file.read()
return self.readFromStream(file_data)
result = [] # type: List[SceneNode]
node = self.readFromStream(file_data, file_name)
if node is not None:
result.append(node)
return result

View file

@ -5,6 +5,9 @@ import configparser # An input for some functions we're testing.
import os.path # To find the integration test .ini files.
import pytest # To register tests with.
import unittest.mock # To mock the application, plug-in and container registry out.
import os.path
import sys
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
import UM.Application # To mock the application out.
import UM.PluginRegistry # To mock the plug-in registry out.
@ -12,11 +15,13 @@ import UM.Settings.ContainerRegistry # To mock the container registry out.
import UM.Settings.InstanceContainer # To intercept the serialised data from the read() function.
import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module.
from LegacyProfileReader import LegacyProfileReader # The module we're testing.
@pytest.fixture
def legacy_profile_reader():
return LegacyProfileReader()
try:
return LegacyProfileReaderModule.LegacyProfileReader()
except TypeError:
return LegacyProfileReaderModule.LegacyProfileReader.LegacyProfileReader()
test_prepareDefaultsData = [
{

View file

@ -69,7 +69,7 @@ class DisplayFilenameAndLayerOnLCD(Script):
else:
lcd_text = "M117 Printing Layer "
else:
lcd_text = "M117 Printing " + name + " - Layer "
lcd_text = "M117 Printing " + name + " - Layer "
i = self.getSettingValueByKey("startNum")
for layer in data:
display_text = lcd_text + str(i) + " " + name

View file

@ -106,10 +106,17 @@ class PauseAtHeight(Script):
"standby_temperature":
{
"label": "Standby Temperature",
"description": "Change the temperature during the pause",
"description": "Change the temperature during the pause.",
"unit": "°C",
"type": "int",
"default_value": 0
},
"display_text":
{
"label": "Display Text",
"description": "Text that should appear on the display while paused. If left empty, there will not be any message.",
"type": "str",
"default_value": ""
}
}
}"""
@ -144,6 +151,7 @@ class PauseAtHeight(Script):
firmware_retract = Application.getInstance().getGlobalContainerStack().getProperty("machine_firmware_retract", "value")
control_temperatures = Application.getInstance().getGlobalContainerStack().getProperty("machine_nozzle_temp_enabled", "value")
initial_layer_height = Application.getInstance().getGlobalContainerStack().getProperty("layer_height_0", "value")
display_text = self.getSettingValueByKey("display_text")
is_griffin = False
@ -265,7 +273,7 @@ class PauseAtHeight(Script):
if not is_griffin:
# Retraction
prepend_gcode += self.putValue(M = 83) + "\n"
prepend_gcode += self.putValue(M = 83) + " ; switch to relative E values for any needed retraction\n"
if retraction_amount != 0:
if firmware_retract: #Can't set the distance directly to what the user wants. We have to choose ourselves.
retraction_count = 1 if control_temperatures else 3 #Retract more if we don't control the temperature.
@ -275,25 +283,28 @@ class PauseAtHeight(Script):
prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = retraction_speed * 60) + "\n"
# Move the head away
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + "\n"
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + " ; move up a millimeter to get out of the way\n"
# This line should be ok
prepend_gcode += self.putValue(G = 1, X = park_x, Y = park_y, F = 9000) + "\n"
if current_z < 15:
prepend_gcode += self.putValue(G = 1, Z = 15, F = 300) + "\n"
prepend_gcode += self.putValue(G = 1, Z = 15, F = 300) + " ; too close to bed--move to at least 15mm\n"
if control_temperatures:
# Set extruder standby temperature
prepend_gcode += self.putValue(M = 104, S = standby_temperature) + "; standby temperature\n"
prepend_gcode += self.putValue(M = 104, S = standby_temperature) + " ; standby temperature\n"
if display_text:
prepend_gcode += "M117 " + display_text + "\n"
# Wait till the user continues printing
prepend_gcode += self.putValue(M = 0) + ";Do the actual pause\n"
prepend_gcode += self.putValue(M = 0) + " ; Do the actual pause\n"
if not is_griffin:
if control_temperatures:
# Set extruder resume temperature
prepend_gcode += self.putValue(M = 109, S = int(target_temperature.get(current_t, 0))) + "; resume temperature\n"
prepend_gcode += self.putValue(M = 109, S = int(target_temperature.get(current_t, 0))) + " ; resume temperature\n"
# Push the filament back,
if retraction_amount != 0:
@ -309,8 +320,10 @@ class PauseAtHeight(Script):
prepend_gcode += self.putValue(G = 1, E = -retraction_amount, F = retraction_speed * 60) + "\n"
# Move the head back
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + "\n"
if current_z < 15:
prepend_gcode += self.putValue(G = 1, Z = current_z + 1, F = 300) + "\n"
prepend_gcode += self.putValue(G = 1, X = x, Y = y, F = 9000) + "\n"
prepend_gcode += self.putValue(G = 1, Z = current_z, F = 300) + " ; move back down to resume height\n"
if retraction_amount != 0:
if firmware_retract: #Can't set the distance directly to what the user wants. We have to choose ourselves.
retraction_count = 1 if control_temperatures else 3 #Retract more if we don't control the temperature.
@ -319,7 +332,7 @@ class PauseAtHeight(Script):
else:
prepend_gcode += self.putValue(G = 1, E = retraction_amount, F = retraction_speed * 60) + "\n"
prepend_gcode += self.putValue(G = 1, F = 9000) + "\n"
prepend_gcode += self.putValue(M = 82) + "\n"
prepend_gcode += self.putValue(M = 82) + " ; switch back to absolute E values\n"
# reset extrude value to pre pause value
prepend_gcode += self.putValue(G = 92, E = current_e) + "\n"

View file

@ -0,0 +1,75 @@
# Copyright (c) 2019 Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
import math
from ..Script import Script
## Continues retracting during all travel moves.
class RetractContinue(Script):
def getSettingDataString(self):
return """{
"name": "Retract Continue",
"key": "RetractContinue",
"metadata": {},
"version": 2,
"settings":
{
"extra_retraction_speed":
{
"label": "Extra Retraction Ratio",
"description": "How much does it retract during the travel move, by ratio of the travel length.",
"type": "float",
"default_value": 0.05
}
}
}"""
def execute(self, data):
current_e = 0
current_x = 0
current_y = 0
extra_retraction_speed = self.getSettingValueByKey("extra_retraction_speed")
for layer_number, layer in enumerate(data):
lines = layer.split("\n")
for line_number, line in enumerate(lines):
if self.getValue(line, "G") in {0, 1}: # Track X,Y location.
current_x = self.getValue(line, "X", current_x)
current_y = self.getValue(line, "Y", current_y)
if self.getValue(line, "G") == 1:
if self.getValue(line, "E"):
new_e = self.getValue(line, "E")
if new_e >= current_e: # Not a retraction.
continue
# A retracted travel move may consist of multiple commands, due to combing.
# This continues retracting over all of these moves and only unretracts at the end.
delta_line = 1
dx = current_x # Track the difference in X for this move only to compute the length of the travel.
dy = current_y
while line_number + delta_line < len(lines) and self.getValue(lines[line_number + delta_line], "G") != 1:
travel_move = lines[line_number + delta_line]
if self.getValue(travel_move, "G") != 0:
delta_line += 1
continue
travel_x = self.getValue(travel_move, "X", dx)
travel_y = self.getValue(travel_move, "Y", dy)
f = self.getValue(travel_move, "F", "no f")
length = math.sqrt((travel_x - dx) * (travel_x - dx) + (travel_y - dy) * (travel_y - dy)) # Length of the travel move.
new_e -= length * extra_retraction_speed # New retraction is by ratio of this travel move.
if f == "no f":
new_travel_move = "G1 X{travel_x} Y{travel_y} E{new_e}".format(travel_x = travel_x, travel_y = travel_y, new_e = new_e)
else:
new_travel_move = "G1 F{f} X{travel_x} Y{travel_y} E{new_e}".format(f = f, travel_x = travel_x, travel_y = travel_y, new_e = new_e)
lines[line_number + delta_line] = new_travel_move
delta_line += 1
dx = travel_x
dy = travel_y
current_e = new_e
new_layer = "\n".join(lines)
data[layer_number] = new_layer
return data

View file

@ -77,10 +77,10 @@ class TimeLapse(Script):
gcode_to_append = ";TimeLapse Begin\n"
if park_print_head:
gcode_to_append += self.putValue(G = 1, F = feed_rate, X = x_park, Y = y_park) + ";Park print head\n"
gcode_to_append += self.putValue(M = 400) + ";Wait for moves to finish\n"
gcode_to_append += trigger_command + ";Snap Photo\n"
gcode_to_append += self.putValue(G = 4, P = pause_length) + ";Wait for camera\n"
gcode_to_append += self.putValue(G = 1, F = feed_rate, X = x_park, Y = y_park) + " ;Park print head\n"
gcode_to_append += self.putValue(M = 400) + " ;Wait for moves to finish\n"
gcode_to_append += trigger_command + " ;Snap Photo\n"
gcode_to_append += self.putValue(G = 4, P = pause_length) + " ;Wait for camera\n"
gcode_to_append += ";TimeLapse End\n"
for layer in data:
# Check that a layer is being printed

View file

@ -386,7 +386,7 @@ class SimulationView(CuraView):
self._max_thickness = max(float(p.lineThicknesses.max()), self._max_thickness)
try:
self._min_thickness = min(float(p.lineThicknesses[numpy.nonzero(p.lineThicknesses)].min()), self._min_thickness)
except:
except ValueError:
# Sometimes, when importing a GCode the line thicknesses are zero and so the minimum (avoiding
# the zero) can't be calculated
Logger.log("i", "Min thickness can't be calculated because all the values are zero")
@ -468,6 +468,9 @@ class SimulationView(CuraView):
Application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
self.calculateMaxLayers()
self.calculateMaxPathsOnLayer(self._current_layer_num)
# FIX: on Max OS X, somehow QOpenGLContext.currentContext() can become None during View switching.
# This can happen when you do the following steps:
# 1. Start Cura

View file

@ -139,6 +139,10 @@ class SolidView(View):
shade_factor * int(material_color[5:7], 16) / 255,
1.0
]
# Color the currently selected face-id. (Disable for now.)
#face = Selection.getHoverFace()
uniforms["hover_face"] = -1 #if not face or node != face[0] else face[1]
except ValueError:
pass

View file

@ -0,0 +1,161 @@
# Copyright (c) 2019 Ultimaker B.V., fieldOfView
# Cura is released under the terms of the LGPLv3 or higher.
# The _toMeshData function is taken from the AMFReader class which was built by fieldOfView.
from typing import Any, List, Union, TYPE_CHECKING
import numpy # To create the mesh data.
import os.path # To create the mesh name for the resulting mesh.
import trimesh # To load the files into a Trimesh.
from UM.Mesh.MeshData import MeshData, calculateNormalsFromIndexedVertices # To construct meshes from the Trimesh data.
from UM.Mesh.MeshReader import MeshReader # The plug-in type we're extending.
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType # To add file types that we can open.
from UM.Scene.GroupDecorator import GroupDecorator # Added to the parent node if we load multiple nodes at once.
from cura.CuraApplication import CuraApplication
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator # Added to the resulting scene node.
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator # Added to group nodes if we load multiple nodes at once.
from cura.Scene.CuraSceneNode import CuraSceneNode # To create a node in the scene after reading the file.
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator # Added to the resulting scene node.
if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode
## Class that leverages Trimesh to import files.
class TrimeshReader(MeshReader):
def __init__(self) -> None:
super().__init__()
self._supported_extensions = [".ctm", ".dae", ".gltf", ".glb", ".ply", ".zae"]
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-ctm",
comment = "Open Compressed Triangle Mesh",
suffixes = ["ctm"]
)
)
MimeTypeDatabase.addMimeType(
MimeType(
name = "model/vnd.collada+xml",
comment = "COLLADA Digital Asset Exchange",
suffixes = ["dae"]
)
)
MimeTypeDatabase.addMimeType(
MimeType(
name = "model/gltf-binary",
comment = "glTF Binary",
suffixes = ["glb"]
)
)
MimeTypeDatabase.addMimeType(
MimeType(
name = "model/gltf+json",
comment = "glTF Embedded JSON",
suffixes = ["gltf"]
)
)
# Trimesh seems to have a bug when reading .off files.
#MimeTypeDatabase.addMimeType(
# MimeType(
# name = "application/x-off",
# comment = "Geomview Object File Format",
# suffixes = ["off"]
# )
#)
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-ply", # Wikipedia lists the MIME type as "text/plain" but that won't do as it's not unique to PLY files.
comment = "Stanford Triangle Format",
suffixes = ["ply"]
)
)
MimeTypeDatabase.addMimeType(
MimeType(
name = "model/vnd.collada+xml+zip",
comment = "Compressed COLLADA Digital Asset Exchange",
suffixes = ["zae"]
)
)
## Reads a file using Trimesh.
# \param file_name The file path. This is assumed to be one of the file
# types that Trimesh can read. It will not be checked again.
# \return A scene node that contains the file's contents.
def _read(self, file_name: str) -> Union["SceneNode", List["SceneNode"]]:
# CURA-6739
# GLTF files are essentially JSON files. If you directly give a file name to trimesh.load(), it will
# try to figure out the format, but for GLTF, it loads it as a binary file with flags "rb", and the json.load()
# doesn't like it. For some reason, this seems to happen with 3.5.7, but not 3.7.1. Below is a workaround to
# pass a file object that has been opened with "r" instead "rb" to load a GLTF file.
if file_name.lower().endswith(".gltf"):
mesh_or_scene = trimesh.load(open(file_name, "r", encoding = "utf-8"), file_type = "gltf")
else:
mesh_or_scene = trimesh.load(file_name)
meshes = [] # type: List[Union[trimesh.Trimesh, trimesh.Scene, Any]]
if isinstance(mesh_or_scene, trimesh.Trimesh):
meshes = [mesh_or_scene]
elif isinstance(mesh_or_scene, trimesh.Scene):
meshes = [mesh for mesh in mesh_or_scene.geometry.values()]
active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
nodes = [] # type: List[SceneNode]
for mesh in meshes:
if not isinstance(mesh, trimesh.Trimesh): # Trimesh can also receive point clouds, 2D paths, 3D paths or metadata. Skip those.
continue
mesh.merge_vertices()
mesh.remove_unreferenced_vertices()
mesh.fix_normals()
mesh_data = self._toMeshData(mesh)
file_base_name = os.path.basename(file_name)
new_node = CuraSceneNode()
new_node.setMeshData(mesh_data)
new_node.setSelectable(True)
new_node.setName(file_base_name if len(meshes) == 1 else "{file_base_name} {counter}".format(file_base_name = file_base_name, counter = str(len(nodes) + 1)))
new_node.addDecorator(BuildPlateDecorator(active_build_plate))
new_node.addDecorator(SliceableObjectDecorator())
nodes.append(new_node)
if len(nodes) == 1:
return nodes[0]
# Add all nodes to a group so they stay together.
group_node = CuraSceneNode()
group_node.addDecorator(GroupDecorator())
group_node.addDecorator(ConvexHullDecorator())
group_node.addDecorator(BuildPlateDecorator(active_build_plate))
for node in nodes:
node.setParent(group_node)
return group_node
## Converts a Trimesh to Uranium's MeshData.
# \param tri_node A Trimesh containing the contents of a file that was
# just read.
# \return Mesh data from the Trimesh in a way that Uranium can understand
# it.
def _toMeshData(self, tri_node: trimesh.base.Trimesh) -> MeshData:
tri_faces = tri_node.faces
tri_vertices = tri_node.vertices
indices = []
vertices = []
index_count = 0
face_count = 0
for tri_face in tri_faces:
face = []
for tri_index in tri_face:
vertices.append(tri_vertices[tri_index])
face.append(index_count)
index_count += 1
indices.append(face)
face_count += 1
vertices = numpy.asarray(vertices, dtype = numpy.float32)
indices = numpy.asarray(indices, dtype = numpy.int32)
normals = calculateNormalsFromIndexedVertices(vertices, indices, face_count)
mesh_data = MeshData(vertices = vertices, indices = indices, normals = normals)
return mesh_data

View file

@ -0,0 +1,46 @@
# Copyright (c) 2019 Ultimaker
# Cura is released under the terms of the LGPLv3 or higher.
from . import TrimeshReader
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("uranium")
def getMetaData():
return {
"mesh_reader": [
{
"extension": "ctm",
"description": i18n_catalog.i18nc("@item:inlistbox", "Open Compressed Triangle Mesh")
},
{
"extension": "dae",
"description": i18n_catalog.i18nc("@item:inlistbox", "COLLADA Digital Asset Exchange")
},
{
"extension": "glb",
"description": i18n_catalog.i18nc("@item:inlistbox", "glTF Binary")
},
{
"extension": "gltf",
"description": i18n_catalog.i18nc("@item:inlistbox", "glTF Embedded JSON")
},
# Trimesh seems to have a bug when reading OFF files.
#{
# "extension": "off",
# "description": i18n_catalog.i18nc("@item:inlistbox", "Geomview Object File Format")
#},
{
"extension": "ply",
"description": i18n_catalog.i18nc("@item:inlistbox", "Stanford Triangle Format")
},
{
"extension": "zae",
"description": i18n_catalog.i18nc("@item:inlistbox", "Compressed COLLADA Digital Asset Exchange")
}
]
}
def register(app):
return {"mesh_reader": TrimeshReader.TrimeshReader()}

View file

@ -0,0 +1,7 @@
{
"name": "Trimesh Reader",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides support for reading model files.",
"api": "6.0.0"
}

View file

@ -39,4 +39,4 @@ class UFPReader(MeshReader):
# Open the GCodeReader to parse the data
gcode_reader = PluginRegistry.getInstance().getPluginObject("GCodeReader") # type: ignore
gcode_reader.preReadFromStream(gcode_stream) # type: ignore
return gcode_reader.readFromStream(gcode_stream) # type: ignore
return gcode_reader.readFromStream(gcode_stream, file_name) # type: ignore

View file

@ -20,7 +20,7 @@ Item
property var printJob: null
width: childrenRect.width
height: 18 * screenScaleFactor // TODO: Theme!
height: UM.Theme.getSize("monitor_text_line").height
UM.ProgressBar
{
@ -31,7 +31,7 @@ Item
left: parent.left
}
value: printJob ? printJob.progress : 0
width: UM.Theme.getSize("monitor_column").width
width: UM.Theme.getSize("monitor_progress_bar").width
}
Label
@ -40,16 +40,16 @@ Item
anchors
{
left: progressBar.right
leftMargin: 18 * screenScaleFactor // TODO: Theme!
leftMargin: UM.Theme.getSize("monitor_margin").width
verticalCenter: parent.verticalCenter
}
text: printJob ? Math.round(printJob.progress * 100) + "%" : "0%"
color: printJob && printJob.isActive ? UM.Theme.getColor("monitor_text_primary") : UM.Theme.getColor("monitor_text_disabled")
width: contentWidth
font: UM.Theme.getFont("medium") // 14pt, regular
font: UM.Theme.getFont("default") // 12pt, regular
// FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme!
height: UM.Theme.getSize("monitor_text_line").height
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
}
@ -59,11 +59,11 @@ Item
anchors
{
left: percentLabel.right
leftMargin: 18 * screenScaleFactor // TODO: Theme!
leftMargin: UM.Theme.getSize("monitor_margin").width
verticalCenter: parent.verticalCenter
}
color: UM.Theme.getColor("monitor_text_primary")
font: UM.Theme.getFont("medium") // 14pt, regular
font: UM.Theme.getFont("default") // 12pt, regular
text:
{
if (!printJob)
@ -103,7 +103,7 @@ Item
width: contentWidth
// FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme!
height: UM.Theme.getSize("monitor_text_line").height
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
}

View file

@ -1,10 +1,11 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Dict, List, Optional
from typing import Dict, List, Optional
from PyQt5.QtCore import QTimer
from UM import i18nCatalog
from UM.Logger import Logger # To log errors talking to the API.
from UM.Signal import Signal
from cura.API import Account
from cura.CuraApplication import CuraApplication
@ -13,6 +14,7 @@ from cura.Settings.GlobalStack import GlobalStack
from .CloudApiClient import CloudApiClient
from .CloudOutputDevice import CloudOutputDevice
from ..Models.Http.CloudClusterResponse import CloudClusterResponse
from ..Messages.CloudPrinterDetectedMessage import CloudPrinterDetectedMessage
## The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters.
@ -36,7 +38,7 @@ class CloudOutputDeviceManager:
# Persistent dict containing the remote clusters for the authenticated user.
self._remote_clusters = {} # type: Dict[str, CloudOutputDevice]
self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account
self._api = CloudApiClient(self._account, on_error=lambda error: print(error))
self._api = CloudApiClient(self._account, on_error = lambda error: Logger.log("e", str(error)))
self._account.loginStateChanged.connect(self._onLoginStateChanged)
# Create a timer to update the remote cluster list
@ -108,6 +110,7 @@ class CloudOutputDeviceManager:
)
self._remote_clusters[device.getId()] = device
self.discoveredDevicesChanged.emit()
self._checkIfNewClusterWasAdded(device.clusterData.cluster_id)
self._connectToActiveMachine()
def _onDiscoveredDeviceUpdated(self, cluster_data: CloudClusterResponse) -> None:
@ -171,7 +174,18 @@ class CloudOutputDeviceManager:
machine.setName(device.name)
machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
machine.setMetaDataEntry("group_name", device.name)
device.connect()
machine.addConfiguredConnectionType(device.connectionType.value)
CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device)
if not device.isConnected():
device.connect()
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
if device.key not in output_device_manager.getOutputDeviceIds():
output_device_manager.addOutputDevice(device)
## Checks if Cura has a machine stack (printer) for the given cluster ID and shows a message if it hasn't.
def _checkIfNewClusterWasAdded(self, cluster_id: str) -> None:
container_registry = CuraApplication.getInstance().getContainerRegistry()
cloud_machines = container_registry.findContainersMetadata(**{self.META_CLUSTER_ID: "*"}) # all cloud machines
if not any(machine[self.META_CLUSTER_ID] == cluster_id for machine in cloud_machines):
CloudPrinterDetectedMessage().show()

View file

@ -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 a new printer was added to your account but not yet in Cura.
class CloudPrinterDetectedMessage(Message):
# Singleton used to prevent duplicate messages of this type at the same time.
__is_visible = False
def __init__(self) -> None:
super().__init__(
title=I18N_CATALOG.i18nc("@info:title", "New cloud printers found"),
text=I18N_CATALOG.i18nc("@info:message", "New printers have been found connected to your account, "
"you can find them in your list of discovered printers."),
lifetime=10,
dismissable=True
)
def show(self) -> None:
if CloudPrinterDetectedMessage.__is_visible:
return
super().show()
CloudPrinterDetectedMessage.__is_visible = True
def hide(self, send_signal = True) -> None:
super().hide(send_signal)
CloudPrinterDetectedMessage.__is_visible = False

View file

@ -1,5 +1,7 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration
@ -10,8 +12,11 @@ class ClusterPrinterMaterialStationSlot(ClusterPrintCoreConfiguration):
# \param slot_index: The index of the slot in the material station (ranging 0 to 5).
# \param compatible: Whether the configuration is compatible with the print core.
# \param material_remaining: How much material is remaining on the spool (between 0 and 1, or -1 for missing data).
def __init__(self, slot_index: int, compatible: bool, material_remaining: float, **kwargs):
# \param material_empty: Whether the material spool is too empty to be used.
def __init__(self, slot_index: int, compatible: bool, material_remaining: float,
material_empty: Optional[bool] = False, **kwargs):
self.slot_index = slot_index
self.compatible = compatible
self.material_remaining = material_remaining
self.material_empty = material_empty
super().__init__(**kwargs)

View file

@ -66,7 +66,11 @@ class ClusterPrinterStatus(BaseModel):
## Creates a new output model.
# \param controller - The controller of the model.
def createOutputModel(self, controller: PrinterOutputController) -> PrinterOutputModel:
model = PrinterOutputModel(controller, len(self.configuration), firmware_version = self.firmware_version)
# FIXME
# Note that we're using '2' here as extruder count. We have hardcoded this for now to prevent issues where the
# amount of extruders coming back from the API is actually lower (which it can be if a printer was just added
# to a cluster). This should be fixed in the future, probably also on the cluster API side.
model = PrinterOutputModel(controller, 2, firmware_version = self.firmware_version)
self.updateOutputModel(model)
return model
@ -80,6 +84,11 @@ class ClusterPrinterStatus(BaseModel):
model.updateBuildplate(self.build_plate.type if self.build_plate else "glass")
model.setCameraUrl(QUrl("http://{}:8080/?action=stream".format(self.ip_address)))
if not model.printerConfiguration:
# Prevent accessing printer configuration when not available.
# This sometimes happens when a printer was just added to a group and Cura is connected to that group.
return
# Set the possible configurations based on whether a Material Station is present or not.
if self.material_station and self.material_station.material_slots:
self._updateAvailableConfigurations(model)
@ -115,7 +124,7 @@ class ClusterPrinterStatus(BaseModel):
# We filter out any slot that is not supported by the extruder index, print core type or if the material is empty.
@staticmethod
def _isSupportedConfiguration(slot: ClusterPrinterMaterialStationSlot, extruder_index: int) -> bool:
return slot.extruder_index == extruder_index and slot.compatible
return slot.extruder_index == extruder_index and slot.compatible and not slot.material_empty
## Create an empty material slot with a fake empty material.
@staticmethod

View file

@ -135,7 +135,7 @@ class ClusterApiClient:
result = model_class(**response) # type: ClusterApiClientModel
on_finished_item = cast(Callable[[ClusterApiClientModel], Any], on_finished)
on_finished_item(result)
except JSONDecodeError:
except (JSONDecodeError, TypeError):
Logger.log("e", "Could not parse response from network: %s", str(response))
## Creates a callback function so that it includes the parsing of the response into the correct model.

View file

@ -1,5 +1,6 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Dict, List, Callable, Any
from PyQt5.QtGui import QDesktopServices
@ -8,6 +9,7 @@ from PyQt5.QtNetwork import QNetworkReply
from UM.FileHandler.FileHandler import FileHandler
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
@ -122,9 +124,6 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
self.writeStarted.emit(self)
# Make sure the printer is aware of all new materials as the new print job might contain one.
self.sendMaterialProfiles()
# 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)
@ -170,5 +169,5 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice):
## Get the API client instance.
def _getApiClient(self) -> ClusterApiClient:
if not self._cluster_api:
self._cluster_api = ClusterApiClient(self.address, on_error=lambda error: print(error))
self._cluster_api = ClusterApiClient(self.address, on_error = lambda error: Logger.log("e", str(error)))
return self._cluster_api

View file

@ -1,5 +1,6 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Dict, Optional, Callable, List
from UM import i18nCatalog
@ -66,7 +67,7 @@ class LocalClusterOutputDeviceManager:
## Add a networked printer manually by address.
def addManualDevice(self, address: str, callback: Optional[Callable[[bool, str], None]] = None) -> None:
api_client = ClusterApiClient(address, lambda error: print(error))
api_client = ClusterApiClient(address, lambda error: Logger.log("e", str(error)))
api_client.getSystem(lambda status: self._onCheckManualDeviceResponse(address, status, callback))
## Remove a manually added networked printer.
@ -135,10 +136,13 @@ class LocalClusterOutputDeviceManager:
ultimaker_machines = container_registry.findContainersMetadata(type="machine", manufacturer="Ultimaker B.V.")
found_machine_type_identifiers = {} # type: Dict[str, str]
for machine in ultimaker_machines:
machine_bom_number = machine.get("firmware_update_info", {}).get("id", None)
machine_type = machine.get("id", None)
if machine_bom_number and machine_type:
found_machine_type_identifiers[str(machine_bom_number)] = machine_type
machine_bom_numbers = machine.get("bom_numbers", [])
if machine_type and machine_bom_numbers:
for bom_number in machine_bom_numbers:
# This produces a n:1 mapping of bom numbers to machine types
# allowing the S5R1 and S5R2 hardware to use a single S5 definition.
found_machine_type_identifiers[str(bom_number)] = machine_type
return found_machine_type_identifiers
## Add a new device.
@ -236,7 +240,11 @@ class LocalClusterOutputDeviceManager:
machine.setName(device.name)
machine.setMetaDataEntry(self.META_NETWORK_KEY, device.key)
machine.setMetaDataEntry("group_name", device.name)
device.connect()
machine.addConfiguredConnectionType(device.connectionType.value)
CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device)
if not device.isConnected():
device.connect()
output_device_manager = CuraApplication.getInstance().getOutputDeviceManager()
if device.key not in output_device_manager.getOutputDeviceIds():
output_device_manager.addOutputDevice(device)

View file

@ -104,7 +104,6 @@ class SendMaterialJob(Job):
parts.append(self.device.createFormPart("name=\"signature_file\"; filename=\"{file_name}\""
.format(file_name = signature_file_name), f.read()))
Logger.log("d", "Syncing material %s with cluster.", material_id)
# FIXME: move form posting to API client
self.device.postFormWithParts(target = "/cluster-api/v1/materials/", parts = parts,
on_finished = self._sendingFinished)
@ -117,7 +116,6 @@ class SendMaterialJob(Job):
body = reply.readAll().data().decode('utf8')
if "not added" in body:
# For some reason the cluster returns a 200 sometimes even when syncing failed.
Logger.log("w", "Error while syncing material: %s", body)
return
# Inform the user that materials have been synced. This message only shows itself when not already visible.
# Because of the guards above it is not shown when syncing failed (which is not always an actual problem).

View file

@ -1,5 +1,8 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
import sys
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
import configparser #To check whether the appropriate exceptions are raised.
import pytest #To register tests with.

View file

@ -3,7 +3,9 @@
import configparser #To check whether the appropriate exceptions are raised.
import pytest #To register tests with.
import os.path
import sys
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
import VersionUpgrade26to27 #The module we're testing.
## Creates an instance of the upgrader to test with.

View file

@ -1,6 +1,8 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
import sys
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
import configparser #To parse the resulting config files.
import pytest #To register tests with.

View file

@ -1,6 +1,8 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
import sys
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
import configparser #To parse the resulting config files.
import pytest #To register tests with.

View file

@ -119,10 +119,10 @@ class VersionUpgrade42to43(VersionUpgrade):
if key in parser["values"]:
del parser["values"][key]
if "support_infill_angles" in parser["values"]:
old_value = float(parser["values"]["support_infill_angles"])
new_value = [int(round(old_value))]
parser["values"]["support_infill_angles"] = str(new_value)
if "support_infill_angles" in parser["values"]:
old_value = float(parser["values"]["support_infill_angles"])
new_value = [int(round(old_value))]
parser["values"]["support_infill_angles"] = str(new_value)
result = io.StringIO()
parser.write(result)

View file

@ -6,7 +6,7 @@ import io
import json #To parse the product-to-id mapping file.
import os.path #To find the product-to-id mapping.
import sys
from typing import Any, Dict, List, Optional, Tuple, cast, Set
from typing import Any, Dict, List, Optional, Tuple, cast, Set, Union
import xml.etree.ElementTree as ET
from UM.Resources import Resources
@ -19,7 +19,10 @@ from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from cura.CuraApplication import CuraApplication
from cura.Machines.VariantType import VariantType
from .XmlMaterialValidator import XmlMaterialValidator
try:
from .XmlMaterialValidator import XmlMaterialValidator
except (ImportError, SystemError):
import XmlMaterialValidator # type: ignore # This fixes the tests not being able to import.
## Handles serializing and deserializing material containers from an XML file
@ -40,11 +43,11 @@ class XmlMaterialProfile(InstanceContainer):
#
# \param xml_version: The version number found in an XML file.
# \return The corresponding setting_version.
@classmethod
def xmlVersionToSettingVersion(cls, xml_version: str) -> int:
@staticmethod
def xmlVersionToSettingVersion(xml_version: str) -> int:
if xml_version == "1.3":
return CuraApplication.SettingVersion
return 0 #Older than 1.3.
return 0 # Older than 1.3.
def getInheritedFiles(self):
return self._inherited_files
@ -409,7 +412,8 @@ class XmlMaterialProfile(InstanceContainer):
self._combineElement(self._expandMachinesXML(result), self._expandMachinesXML(second))
return result
def _createKey(self, element):
@staticmethod
def _createKey(element):
key = element.tag.split("}")[-1]
if "key" in element.attrib:
key += " key:" + element.attrib["key"]
@ -425,15 +429,15 @@ class XmlMaterialProfile(InstanceContainer):
# Recursively merges XML elements. Updates either the text or children if another element is found in first.
# If it does not exist, copies it from second.
def _combineElement(self, first, second):
@staticmethod
def _combineElement(first, second):
# Create a mapping from tag name to element.
mapping = {}
for element in first:
key = self._createKey(element)
key = XmlMaterialProfile._createKey(element)
mapping[key] = element
for element in second:
key = self._createKey(element)
key = XmlMaterialProfile._createKey(element)
if len(element): # Check if element has children.
try:
if "setting" in element.tag and not "settings" in element.tag:
@ -443,7 +447,7 @@ class XmlMaterialProfile(InstanceContainer):
for child in element:
mapping[key].append(child)
else:
self._combineElement(mapping[key], element) # Multiple elements, handle those.
XmlMaterialProfile._combineElement(mapping[key], element) # Multiple elements, handle those.
except KeyError:
mapping[key] = element
first.append(element)
@ -833,9 +837,9 @@ class XmlMaterialProfile(InstanceContainer):
ContainerRegistry.getInstance().addContainer(container_to_add)
@classmethod
def _getSettingsDictForNode(cls, node) -> Tuple[dict, dict]:
node_mapped_settings_dict = dict()
node_unmapped_settings_dict = dict()
def _getSettingsDictForNode(cls, node) -> Tuple[Dict[str, Any], Dict[str, Any]]:
node_mapped_settings_dict = dict() # type: Dict[str, Any]
node_unmapped_settings_dict = dict() # type: Dict[str, Any]
# Fetch settings in the "um" namespace
um_settings = node.iterfind("./um:setting", cls.__namespaces)
@ -1130,8 +1134,8 @@ class XmlMaterialProfile(InstanceContainer):
builder.data(data)
builder.end(tag_name)
@classmethod
def _profile_name(cls, material_name, color_name):
@staticmethod
def _profile_name(material_name, color_name):
if material_name is None:
return "Unknown Material"
if color_name != "Generic":
@ -1139,8 +1143,8 @@ class XmlMaterialProfile(InstanceContainer):
else:
return material_name
@classmethod
def getPossibleDefinitionIDsFromName(cls, name):
@staticmethod
def getPossibleDefinitionIDsFromName(name):
name_parts = name.lower().split(" ")
merged_name_parts = []
for part in name_parts:
@ -1178,8 +1182,8 @@ class XmlMaterialProfile(InstanceContainer):
return product_to_id_map
## Parse the value of the "material compatible" property.
@classmethod
def _parseCompatibleValue(cls, value: str):
@staticmethod
def _parseCompatibleValue(value: str):
return value in {"yes", "unknown"}
## Small string representation for debugging.
@ -1208,7 +1212,7 @@ class XmlMaterialProfile(InstanceContainer):
"break position": "material_break_retracted_position",
"break speed": "material_break_speed",
"break temperature": "material_break_temperature"
}
} # type: Dict[str, str]
__unmapped_settings = [
"hardware compatible",
"hardware recommended"

View file

@ -6,6 +6,7 @@
"Ultimaker 2+": "ultimaker2_plus",
"Ultimaker 3": "ultimaker3",
"Ultimaker 3 Extended": "ultimaker3_extended",
"Ultimaker S3": "ultimaker_s3",
"Ultimaker S5": "ultimaker_s5",
"Ultimaker Original": "ultimaker_original",
"Ultimaker Original+": "ultimaker_original_plus",

View file

@ -0,0 +1,79 @@
from unittest.mock import patch, MagicMock
import sys
import os
# Prevents error: "PyCapsule_GetPointer called with incorrect name" with conflicting SIP configurations between Arcus and PyQt: Import Arcus and Savitar first!
import Savitar # Dont remove this line
import Arcus # No really. Don't. It needs to be there!
from UM.Qt.QtApplication import QtApplication # QtApplication import is required, even though it isn't used.
import pytest
import XmlMaterialProfile
def createXmlMaterialProfile(material_id):
try:
return XmlMaterialProfile.XmlMaterialProfile.XmlMaterialProfile(material_id)
except AttributeError:
return XmlMaterialProfile.XmlMaterialProfile(material_id)
def test_setName():
material_1 = createXmlMaterialProfile("herpderp")
material_2 = createXmlMaterialProfile("OMGZOMG")
material_1.getMetaData()["base_file"] = "herpderp"
material_2.getMetaData()["base_file"] = "herpderp"
container_registry = MagicMock()
container_registry.isReadOnly = MagicMock(return_value = False)
container_registry.findInstanceContainers = MagicMock(return_value = [material_1, material_2])
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value = container_registry)):
material_1.setName("beep!")
assert material_1.getName() == "beep!"
assert material_2.getName() == "beep!"
def test_setDirty():
material_1 = createXmlMaterialProfile("herpderp")
material_2 = createXmlMaterialProfile("OMGZOMG")
material_1.getMetaData()["base_file"] = "herpderp"
material_2.getMetaData()["base_file"] = "herpderp"
container_registry = MagicMock()
container_registry.isReadOnly = MagicMock(return_value=False)
container_registry.findContainers = MagicMock(return_value=[material_1, material_2])
# Sanity check. Since we did a hacky thing to set the metadata, the container should not be dirty.
# But this test assumes that it works like that, so we need to validate that.
assert not material_1.isDirty()
assert not material_2.isDirty()
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)):
material_2.setDirty(True)
assert material_1.isDirty()
assert material_2.isDirty()
# Setting the base material dirty does not set it's child as dirty.
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)):
material_1.setDirty(False)
assert not material_1.isDirty()
assert material_2.isDirty()
def test_serializeNonBaseMaterial():
material_1 = createXmlMaterialProfile("herpderp")
material_1.getMetaData()["base_file"] = "omgzomg"
container_registry = MagicMock()
container_registry.isReadOnly = MagicMock(return_value=False)
container_registry.findContainers = MagicMock(return_value=[material_1])
with patch("UM.Settings.ContainerRegistry.ContainerRegistry.getInstance", MagicMock(return_value=container_registry)):
with pytest.raises(NotImplementedError):
# This material is not a base material, so it can't be serialized!
material_1.serialize()