mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-11-02 20:52:20 -07:00
Merge branch 'master' into feature_intent
This commit is contained in:
commit
d38e60ce06
232 changed files with 25907 additions and 9339 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
75
plugins/PostProcessingPlugin/scripts/RetractContinue.py
Normal file
75
plugins/PostProcessingPlugin/scripts/RetractContinue.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
161
plugins/TrimeshReader/TrimeshReader.py
Normal file
161
plugins/TrimeshReader/TrimeshReader.py
Normal 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
|
||||
46
plugins/TrimeshReader/__init__.py
Normal file
46
plugins/TrimeshReader/__init__.py
Normal 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()}
|
||||
7
plugins/TrimeshReader/plugin.json
Normal file
7
plugins/TrimeshReader/plugin.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
79
plugins/XmlMaterialProfile/tests/TestXmlMaterialProfile.py
Normal file
79
plugins/XmlMaterialProfile/tests/TestXmlMaterialProfile.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue